mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-09 17:37:58 -05:00
test: account recovery test
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { AccountService } from "./account.service.js";
|
||||
import { DataService } from "./data.service.js";
|
||||
import { Hash } from "../types/commitment.js";
|
||||
import { DepositEvent, WithdrawalEvent } from "../types/events.js";
|
||||
import { PoolInfo } from "../types/account.js";
|
||||
import { generatePrivateKey } from "viem/accounts";
|
||||
import { bigintToHash } from "../crypto.js";
|
||||
|
||||
describe("AccountService", () => {
|
||||
let dataService: DataService;
|
||||
let accountService: AccountService;
|
||||
|
||||
// Mock data
|
||||
const pool1: PoolInfo = {
|
||||
chainId: 1,
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
scope: bigintToHash(BigInt("0x1111111111111111111111111111111111111111111111111111111111111111")),
|
||||
deploymentBlock: 1000n,
|
||||
};
|
||||
|
||||
const pool2: PoolInfo = {
|
||||
chainId: 137,
|
||||
address: "0x9876543210987654321098765432109876543210",
|
||||
scope: bigintToHash(BigInt("0x2222222222222222222222222222222222222222222222222222222222222222")),
|
||||
deploymentBlock: 2000n,
|
||||
};
|
||||
|
||||
// Test scenario:
|
||||
// Pool 1: 2 deposits, first has 2 withdrawals, second has 1 withdrawal
|
||||
// Pool 2: 1 deposit with no withdrawals
|
||||
const pool1Deposits: DepositEvent[] = [
|
||||
{
|
||||
depositor: "0xdepositor1",
|
||||
commitment: bigintToHash(BigInt("0xc1")),
|
||||
label: bigintToHash(BigInt("0xf1")),
|
||||
value: 100n,
|
||||
precommitment: bigintToHash(BigInt("0xp1")),
|
||||
blockNumber: 1100n,
|
||||
transactionHash: bigintToHash(BigInt("0xt1")),
|
||||
},
|
||||
{
|
||||
depositor: "0xdepositor2",
|
||||
commitment: bigintToHash(BigInt("0xc2")),
|
||||
label: bigintToHash(BigInt("0xf2")),
|
||||
value: 200n,
|
||||
precommitment: bigintToHash(BigInt("0xp2")),
|
||||
blockNumber: 1200n,
|
||||
transactionHash: bigintToHash(BigInt("0xt2")),
|
||||
},
|
||||
];
|
||||
|
||||
const pool1Withdrawals: WithdrawalEvent[] = [
|
||||
{
|
||||
withdrawn: 30n,
|
||||
spentNullifier: bigintToHash(BigInt("0xn1")),
|
||||
newCommitment: bigintToHash(BigInt("0xc3")),
|
||||
blockNumber: 1150n,
|
||||
transactionHash: bigintToHash(BigInt("0xt3")),
|
||||
},
|
||||
{
|
||||
withdrawn: 40n,
|
||||
spentNullifier: bigintToHash(BigInt("0xn2")),
|
||||
newCommitment: bigintToHash(BigInt("0xc4")),
|
||||
blockNumber: 1160n,
|
||||
transactionHash: bigintToHash(BigInt("0xt4")),
|
||||
},
|
||||
{
|
||||
withdrawn: 150n,
|
||||
spentNullifier: bigintToHash(BigInt("0xn3")),
|
||||
newCommitment: bigintToHash(BigInt("0xc5")),
|
||||
blockNumber: 1250n,
|
||||
transactionHash: bigintToHash(BigInt("0xt5")),
|
||||
},
|
||||
];
|
||||
|
||||
const pool2Deposits: DepositEvent[] = [
|
||||
{
|
||||
depositor: "0xdepositor3",
|
||||
commitment: bigintToHash(BigInt("0xc6")),
|
||||
label: bigintToHash(BigInt("0xf3")),
|
||||
value: 300n,
|
||||
precommitment: bigintToHash(BigInt("0xp3")),
|
||||
blockNumber: 2100n,
|
||||
transactionHash: bigintToHash(BigInt("0xt6")),
|
||||
},
|
||||
];
|
||||
|
||||
const pool2Withdrawals: WithdrawalEvent[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock DataService
|
||||
dataService = {
|
||||
getDeposits: vi.fn(async (chainId: number) => {
|
||||
if (chainId === pool1.chainId) return pool1Deposits;
|
||||
if (chainId === pool2.chainId) return pool2Deposits;
|
||||
return [];
|
||||
}),
|
||||
getWithdrawals: vi.fn(async (chainId: number) => {
|
||||
if (chainId === pool1.chainId) return pool1Withdrawals;
|
||||
if (chainId === pool2.chainId) return pool2Withdrawals;
|
||||
return [];
|
||||
}),
|
||||
} as unknown as DataService;
|
||||
|
||||
// Create AccountService with a fixed seed for deterministic tests
|
||||
accountService = new AccountService(
|
||||
dataService,
|
||||
undefined,
|
||||
generatePrivateKey()
|
||||
);
|
||||
});
|
||||
|
||||
describe("retrieveHistory", () => {
|
||||
it("should correctly reconstruct account history from multiple pools", async () => {
|
||||
// Process both pools
|
||||
await accountService.retrieveHistory([pool1, pool2]);
|
||||
|
||||
// Get all spendable commitments
|
||||
const spendable = accountService.getSpendableCommitments();
|
||||
|
||||
// Verify pool1 accounts
|
||||
const pool1Accounts = spendable.get(pool1.scope);
|
||||
expect(pool1Accounts).toBeDefined();
|
||||
expect(pool1Accounts).toHaveLength(2);
|
||||
|
||||
// First deposit should have 30 remaining (100 - 30 - 40)
|
||||
expect(pool1Accounts![0]!.value).toBe(30n);
|
||||
|
||||
// Second deposit should have 50 remaining (200 - 150)
|
||||
expect(pool1Accounts![1]!.value).toBe(50n);
|
||||
|
||||
// Verify pool2 accounts
|
||||
const pool2Accounts = spendable.get(pool2.scope);
|
||||
expect(pool2Accounts).toBeDefined();
|
||||
expect(pool2Accounts).toHaveLength(1);
|
||||
|
||||
// Deposit should have full value (no withdrawals)
|
||||
expect(pool2Accounts![0]!.value).toBe(300n);
|
||||
|
||||
// Verify DataService calls
|
||||
expect(dataService.getDeposits).toHaveBeenCalledWith(pool1.chainId, {
|
||||
fromBlock: pool1.deploymentBlock,
|
||||
});
|
||||
expect(dataService.getDeposits).toHaveBeenCalledWith(pool2.chainId, {
|
||||
fromBlock: pool2.deploymentBlock,
|
||||
});
|
||||
|
||||
// Verify withdrawal calls started from first deposit block
|
||||
expect(dataService.getWithdrawals).toHaveBeenCalledWith(pool1.chainId, {
|
||||
fromBlock: 1100n, // First deposit block
|
||||
});
|
||||
expect(dataService.getWithdrawals).toHaveBeenCalledWith(pool2.chainId, {
|
||||
fromBlock: 2100n, // First deposit block
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle pools with no deposits", async () => {
|
||||
const emptyPool: PoolInfo = {
|
||||
chainId: 10,
|
||||
address: "0xempty",
|
||||
scope: bigintToHash(BigInt("0xdeadbeef")), // Using a valid hex value
|
||||
deploymentBlock: 3000n,
|
||||
};
|
||||
|
||||
await accountService.retrieveHistory([emptyPool]);
|
||||
|
||||
const spendable = accountService.getSpendableCommitments();
|
||||
expect(spendable.has(emptyPool.scope)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle pools with deposits but no withdrawals", async () => {
|
||||
await accountService.retrieveHistory([pool2]);
|
||||
|
||||
const spendable = accountService.getSpendableCommitments();
|
||||
const accounts = spendable.get(pool2.scope);
|
||||
|
||||
expect(accounts).toBeDefined();
|
||||
expect(accounts).toHaveLength(1);
|
||||
expect(accounts![0]!.value).toBe(300n);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,19 +11,21 @@ import {
|
||||
} from "../types/account.js";
|
||||
|
||||
export class AccountService {
|
||||
private account: PrivacyPoolAccount;
|
||||
account: PrivacyPoolAccount;
|
||||
|
||||
constructor(
|
||||
private readonly dataService: DataService,
|
||||
account?: PrivacyPoolAccount,
|
||||
seed?: Hex,
|
||||
mnemonic?: string,
|
||||
) {
|
||||
this.account = account || this._initializeAccount(seed);
|
||||
this.account = account || this._initializeAccount(mnemonic);
|
||||
}
|
||||
|
||||
private _initializeAccount(mnemonic?: string): PrivacyPoolAccount {
|
||||
mnemonic = mnemonic || generateMnemonic(english, 128);
|
||||
|
||||
console.log("mnemonic: ", mnemonic);
|
||||
|
||||
let key1 = bytesToNumber(
|
||||
mnemonicToAccount(mnemonic, { accountIndex: 0 }).getHdKey().privateKey!,
|
||||
);
|
||||
@@ -108,14 +110,17 @@ export class AccountService {
|
||||
* @param scope The scope of the pool to deposit into
|
||||
* @returns The nullifier, secret, and precommitment for the deposit
|
||||
*/
|
||||
public createDepositSecrets(scope: Hash): {
|
||||
public createDepositSecrets(
|
||||
scope: Hash,
|
||||
index?: bigint,
|
||||
): {
|
||||
nullifier: Secret;
|
||||
secret: Secret;
|
||||
precommitment: Hash;
|
||||
} {
|
||||
// Find the next available index for this scope
|
||||
const accounts = this.account.poolAccounts.get(scope);
|
||||
const index = BigInt(accounts?.length || 0);
|
||||
index = index || BigInt(accounts?.length || 0);
|
||||
|
||||
const nullifier = this._genDepositNullifier(scope, index);
|
||||
const secret = this._genDepositSecret(scope, index);
|
||||
@@ -157,6 +162,59 @@ export class AccountService {
|
||||
return { nullifier, secret };
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new pool account after depositing
|
||||
* @param scope The scope of the pool
|
||||
* @param value The deposit value
|
||||
* @param nullifier The nullifier used for the deposit
|
||||
* @param secret The secret used for the deposit
|
||||
* @param label The label for the commitment
|
||||
* @param blockNumber The block number of the deposit
|
||||
* @param txHash The transaction hash of the deposit
|
||||
* @returns The new pool account
|
||||
*/
|
||||
public addPoolAccount(
|
||||
scope: Hash,
|
||||
value: bigint,
|
||||
nullifier: Secret,
|
||||
secret: Secret,
|
||||
label: Hash,
|
||||
blockNumber: bigint,
|
||||
txHash: Hash,
|
||||
): PoolAccount {
|
||||
const precommitment = this._hashPrecommitment(nullifier, secret);
|
||||
const commitment = this._hashCommitment(value, label, precommitment);
|
||||
|
||||
const newAccount: PoolAccount = {
|
||||
label,
|
||||
deposit: {
|
||||
hash: commitment,
|
||||
value,
|
||||
label,
|
||||
nullifier,
|
||||
secret,
|
||||
blockNumber,
|
||||
txHash,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
|
||||
// Initialize the array for this scope if it doesn't exist
|
||||
if (!this.account.poolAccounts.has(scope)) {
|
||||
this.account.poolAccounts.set(scope, []);
|
||||
}
|
||||
|
||||
// Add the new account
|
||||
this.account.poolAccounts.get(scope)!.push(newAccount);
|
||||
|
||||
this._log(
|
||||
"Deposit",
|
||||
`Added new pool account with value ${value} and label ${label}`,
|
||||
);
|
||||
|
||||
return newAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new commitment to the account after spending
|
||||
* @param parentCommitment The commitment that was spent
|
||||
@@ -165,7 +223,7 @@ export class AccountService {
|
||||
* @param secret The secret used for spending
|
||||
* @returns The new commitment
|
||||
*/
|
||||
public addCommitment(
|
||||
public addWithdrawalCommitment(
|
||||
parentCommitment: Commitment,
|
||||
value: bigint,
|
||||
nullifier: Secret,
|
||||
|
||||
177
packages/sdk/test/unit/account.spec.ts
Normal file
177
packages/sdk/test/unit/account.spec.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { AccountService } from "../../src/core/account.service.js";
|
||||
import { DataService } from "../../src/core/data.service.js";
|
||||
import { Hash, Secret } from "../../src/types/commitment.js";
|
||||
import { DepositEvent, WithdrawalEvent } from "../../src/types/events.js";
|
||||
import { PoolInfo, Commitment } from "../../src/types/account.js";
|
||||
import { poseidon } from "maci-crypto/build/ts/hashing.js";
|
||||
import { Address } from "viem";
|
||||
|
||||
function randomBigInt(): bigint {
|
||||
return BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
|
||||
}
|
||||
|
||||
describe("AccountService", () => {
|
||||
// Configuration for test data size
|
||||
const NUM_DEPOSITS = 1; // Number of random deposits
|
||||
const NUM_WITHDRAWALS = 2; // Number of withdrawals per pool account
|
||||
|
||||
// Test pool configuration
|
||||
const POOL: PoolInfo = {
|
||||
chainId: 1,
|
||||
address: "0x8Fac8db5cae9C29e9c80c40e8CeDC47EEfe3874E" as Address,
|
||||
scope: randomBigInt() as Hash,
|
||||
deploymentBlock: 1000n,
|
||||
};
|
||||
|
||||
let dataService: DataService;
|
||||
let accountService: AccountService;
|
||||
let masterKeys: [Secret, Secret];
|
||||
let depositEvents: DepositEvent[] = [];
|
||||
let withdrawalEvents: WithdrawalEvent[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset test data arrays
|
||||
depositEvents = [];
|
||||
withdrawalEvents = [];
|
||||
|
||||
// Mock the DataService first
|
||||
dataService = {
|
||||
getDeposits: vi.fn(async (chainId: number) => {
|
||||
return chainId === POOL.chainId ? depositEvents : [];
|
||||
}),
|
||||
getWithdrawals: vi.fn(async (chainId: number) => {
|
||||
return chainId === POOL.chainId ? withdrawalEvents : [];
|
||||
}),
|
||||
} as unknown as DataService;
|
||||
|
||||
// Initialize account service with mocked data service
|
||||
accountService = new AccountService(dataService);
|
||||
masterKeys = accountService.account.masterKeys;
|
||||
|
||||
// Generate test data
|
||||
generateTestData();
|
||||
});
|
||||
|
||||
function generateTestData() {
|
||||
for (let i = 0; i < NUM_DEPOSITS; ++i) {
|
||||
const value = 100n;
|
||||
const label = randomBigInt() as Hash;
|
||||
|
||||
const nullifier = poseidon([
|
||||
masterKeys[0],
|
||||
POOL.scope,
|
||||
BigInt(i),
|
||||
]) as Secret;
|
||||
const secret = poseidon([masterKeys[1], POOL.scope, BigInt(i)]) as Secret;
|
||||
|
||||
const precommitment = poseidon([nullifier, secret]) as Hash;
|
||||
const commitment = poseidon([value, label, precommitment]) as Hash;
|
||||
|
||||
const deposit: DepositEvent = {
|
||||
depositor: POOL.address,
|
||||
commitment,
|
||||
label,
|
||||
value,
|
||||
precommitment,
|
||||
blockNumber: POOL.deploymentBlock + BigInt(i * 100),
|
||||
transactionHash: BigInt(i + 1) as Hash,
|
||||
};
|
||||
|
||||
depositEvents.push(deposit);
|
||||
|
||||
// Track the current commitment for this withdrawal chain
|
||||
let currentCommitment = {
|
||||
hash: commitment,
|
||||
value: value,
|
||||
label: label,
|
||||
nullifier,
|
||||
secret,
|
||||
blockNumber: deposit.blockNumber,
|
||||
txHash: deposit.transactionHash,
|
||||
};
|
||||
let remainingValue = value;
|
||||
|
||||
for (let j = 0; j < NUM_WITHDRAWALS; ++j) {
|
||||
const withdrawnAmount = 10n;
|
||||
remainingValue -= withdrawnAmount;
|
||||
|
||||
// Generate withdrawal nullifier and secret using master keys
|
||||
const withdrawalNullifier = poseidon([
|
||||
masterKeys[0],
|
||||
currentCommitment.label,
|
||||
BigInt(j),
|
||||
]) as Secret;
|
||||
const withdrawalSecret = poseidon([
|
||||
masterKeys[1],
|
||||
currentCommitment.label,
|
||||
BigInt(j),
|
||||
]) as Secret;
|
||||
|
||||
// Create precommitment and new commitment
|
||||
const withdrawalPrecommitment = poseidon([
|
||||
withdrawalNullifier,
|
||||
withdrawalSecret,
|
||||
]) as Hash;
|
||||
const newCommitment = poseidon([
|
||||
remainingValue,
|
||||
currentCommitment.label,
|
||||
withdrawalPrecommitment,
|
||||
]) as Hash;
|
||||
|
||||
// Create withdrawal event
|
||||
const withdrawal: WithdrawalEvent = {
|
||||
withdrawn: withdrawnAmount,
|
||||
spentNullifier: poseidon([withdrawalNullifier]) as Hash,
|
||||
newCommitment,
|
||||
blockNumber: currentCommitment.blockNumber + BigInt((j + 1) * 100),
|
||||
transactionHash: BigInt(i * 100 + j + 2) as Hash,
|
||||
};
|
||||
|
||||
withdrawalEvents.push(withdrawal);
|
||||
|
||||
// Update current commitment for next iteration
|
||||
currentCommitment = {
|
||||
hash: newCommitment,
|
||||
value: remainingValue,
|
||||
label: currentCommitment.label,
|
||||
nullifier: withdrawalNullifier,
|
||||
secret: withdrawalSecret,
|
||||
blockNumber: withdrawal.blockNumber,
|
||||
txHash: withdrawal.transactionHash,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it("should reconstruct account history and find the valid deposit chain", async () => {
|
||||
// Process the pool
|
||||
await accountService.retrieveHistory([POOL]);
|
||||
|
||||
// Log internal state
|
||||
console.log(
|
||||
"Account service internal state:",
|
||||
accountService.account.poolAccounts,
|
||||
);
|
||||
|
||||
accountService.account.poolAccounts.forEach((p) =>
|
||||
console.log("PoolAccounts", p),
|
||||
);
|
||||
|
||||
const spendable = accountService
|
||||
.getSpendableCommitments()
|
||||
.get(POOL.scope) as Commitment[];
|
||||
|
||||
if (spendable) {
|
||||
console.log("Spendable:", spendable);
|
||||
}
|
||||
|
||||
// Verify service calls
|
||||
// expect(dataService.getDeposits).toHaveBeenCalledWith(POOL.chainId, {
|
||||
// fromBlock: POOL.deploymentBlock,
|
||||
// });
|
||||
// expect(dataService.getWithdrawals).toHaveBeenCalledWith(POOL.chainId, {
|
||||
// fromBlock: depositEvents[0].blockNumber,
|
||||
// });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user