test: account recovery test

This commit is contained in:
moebius
2025-02-24 18:28:51 +01:00
parent 2d4627ba55
commit ef39ca9d7a
3 changed files with 241 additions and 189 deletions

View File

@@ -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);
});
});
});

View File

@@ -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,

View 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,
// });
});
});