@0xbow/privacy-pools-core-sdk 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -23
- package/dist/esm/{fetchArtifacts.esm-B6qveiM8.js → fetchArtifacts.esm-B0qaot8v.js} +2 -2
- package/dist/esm/{fetchArtifacts.esm-B6qveiM8.js.map → fetchArtifacts.esm-B0qaot8v.js.map} +1 -1
- package/dist/esm/{fetchArtifacts.node-BPQQPsnb.js → fetchArtifacts.node-PzijuwVc.js} +2 -2
- package/dist/esm/{fetchArtifacts.node-BPQQPsnb.js.map → fetchArtifacts.node-PzijuwVc.js.map} +1 -1
- package/dist/esm/{index-CRtEyHEf.js → index-BjOXETm6.js} +316 -316
- package/dist/esm/{index-CRtEyHEf.js.map → index-BjOXETm6.js.map} +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/index.d.mts +81 -0
- package/dist/node/{fetchArtifacts.esm-z-KXbilc.js → fetchArtifacts.esm-B6uU6QdA.js} +2 -2
- package/dist/node/{fetchArtifacts.esm-z-KXbilc.js.map → fetchArtifacts.esm-B6uU6QdA.js.map} +1 -1
- package/dist/node/{fetchArtifacts.node-DvqhqpW9.js → fetchArtifacts.node-CZRy6KmV.js} +2 -2
- package/dist/node/{fetchArtifacts.node-DvqhqpW9.js.map → fetchArtifacts.node-CZRy6KmV.js.map} +1 -1
- package/dist/node/{index-BsmEKESv.js → index-b-U_m4Mi.js} +337 -337
- package/dist/node/{index-BsmEKESv.js.map → index-b-U_m4Mi.js.map} +1 -1
- package/dist/node/index.mjs +1 -1
- package/dist/types/circuits/artifactHashes.d.ts +19 -0
- package/dist/types/core/account.service.d.ts +79 -0
- package/dist/types/core/tmp.d.ts +1 -0
- package/dist/types/{fetchArtifacts.esm-DF01Zpo3.js → fetchArtifacts.esm-BKxGrC6w.js} +1 -1
- package/dist/types/{fetchArtifacts.node-BO6FBCAw.js → fetchArtifacts.node-kXMUDgNn.js} +1 -1
- package/dist/types/{index-CH7gk4sK.js → index-BwyNuaY0.js} +336 -336
- package/dist/types/index.js +1 -1
- package/dist/types/types/account.d.ts +2 -0
- package/package.json +1 -1
- package/src/circuits/artifactHashes.ts +74 -0
- package/src/circuits/circuits.impl.ts +8 -0
- package/src/core/account.service.ts +329 -35
- package/src/core/data.service.ts +3 -9
- package/src/core/tmp.ts +4 -0
- package/src/crypto.ts +5 -6
- package/src/types/account.ts +3 -1
- package/dist/types/keys.d.ts +0 -18
- package/src/keys.ts +0 -42
package/dist/types/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { o as AccountError, A as AccountService, B as BlockchainProvider, p as CircuitName, k as Circuits, C as CommitmentService, n as ContractError, l as ContractInteractionsService, a as DEFAULT_LOG_FETCH_CONFIG, D as DataService, E as ErrorCode, I as InvalidRpcUrl, P as PrivacyPoolSDK, m as ProofError, S as SDKError, W as WithdrawalService, f as bigintToHash, i as bigintToHex, j as calculateContext, b as generateDepositSecrets, g as generateMasterKeys, e as generateMerkleProof, c as generateWithdrawalSecrets, d as getCommitment, h as hashPrecommitment } from './index-
|
|
1
|
+
export { o as AccountError, A as AccountService, B as BlockchainProvider, p as CircuitName, k as Circuits, C as CommitmentService, n as ContractError, l as ContractInteractionsService, a as DEFAULT_LOG_FETCH_CONFIG, D as DataService, E as ErrorCode, I as InvalidRpcUrl, P as PrivacyPoolSDK, m as ProofError, S as SDKError, W as WithdrawalService, f as bigintToHash, i as bigintToHex, j as calculateContext, b as generateDepositSecrets, g as generateMasterKeys, e as generateMerkleProof, c as generateWithdrawalSecrets, d as getCommitment, h as hashPrecommitment } from './index-BwyNuaY0.js';
|
|
2
2
|
import 'viem/accounts';
|
|
3
3
|
import 'buffer';
|
|
4
4
|
import 'http';
|
|
@@ -6,6 +6,7 @@ export interface PoolAccount {
|
|
|
6
6
|
deposit: AccountCommitment;
|
|
7
7
|
children: AccountCommitment[];
|
|
8
8
|
ragequit?: RagequitEvent;
|
|
9
|
+
isMigrated?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface AccountCommitment {
|
|
11
12
|
hash: Hash;
|
|
@@ -16,6 +17,7 @@ export interface AccountCommitment {
|
|
|
16
17
|
blockNumber: bigint;
|
|
17
18
|
timestamp?: bigint;
|
|
18
19
|
txHash: Hex;
|
|
20
|
+
isMigration?: boolean;
|
|
19
21
|
}
|
|
20
22
|
export interface PrivacyPoolAccount {
|
|
21
23
|
masterKeys: [masterNullifier: Secret, masterSecret: Secret];
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CircuitName, CircuitNameString } from "./circuits.interface.js";
|
|
2
|
+
|
|
3
|
+
type ArtifactType = "wasm" | "vkey" | "zkey";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expected SHA-256 hex digests for every downloaded circuit artifact.
|
|
7
|
+
*
|
|
8
|
+
* vkey and zkey hashes are derived from the trusted-setup ceremony outputs
|
|
9
|
+
* committed in packages/circuits/trusted-setup/final-keys/.
|
|
10
|
+
*
|
|
11
|
+
* wasm hashes are derived from the compiled circuit outputs
|
|
12
|
+
* in packages/circuits/build/.
|
|
13
|
+
*
|
|
14
|
+
* Every artifact downloaded by the SDK MUST have a hash entry here.
|
|
15
|
+
* verifyArtifactIntegrity throws if a hash is missing — refusing to
|
|
16
|
+
* load unverified artifacts is the correct security posture.
|
|
17
|
+
*/
|
|
18
|
+
export const ARTIFACT_HASHES: Record<
|
|
19
|
+
CircuitNameString,
|
|
20
|
+
Partial<Record<ArtifactType, string>>
|
|
21
|
+
> = {
|
|
22
|
+
[CircuitName.Commitment]: {
|
|
23
|
+
wasm: "254d2130607182fd6fd1aee67971526b13cfe178c88e360da96dce92663828d8",
|
|
24
|
+
vkey: "7d48b4eb3dedc12fb774348287b587f0c18c3c7254cd60e9cf0f8b3636a570d8",
|
|
25
|
+
zkey: "494ae92d64098fda2a5649690ddc5821fcd7449ca5fe8ef99ee7447544d7e1f3",
|
|
26
|
+
},
|
|
27
|
+
[CircuitName.Withdraw]: {
|
|
28
|
+
wasm: "36cda22791def3d520a55c0fc808369cd5849532a75fab65686e666ed3d55c10",
|
|
29
|
+
vkey: "666bd0983b20c1611543b04f7712e067fbe8cad69f07ada8a310837ff398d21e",
|
|
30
|
+
zkey: "2a893b42174c813566e5c40c715a8b90cd49fc4ecf384e3a6024158c3d6de677",
|
|
31
|
+
},
|
|
32
|
+
[CircuitName.MerkleTree]: {},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Freeze the manifest so runtime code cannot swap out trusted hashes.
|
|
36
|
+
for (const circuitHashes of Object.values(ARTIFACT_HASHES)) {
|
|
37
|
+
if (circuitHashes != null) {
|
|
38
|
+
Object.freeze(circuitHashes);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Object.freeze(ARTIFACT_HASHES);
|
|
43
|
+
|
|
44
|
+
export async function sha256Hex(data: Uint8Array): Promise<string> {
|
|
45
|
+
const hashBuffer = await globalThis.crypto.subtle.digest(
|
|
46
|
+
"SHA-256",
|
|
47
|
+
data as ArrayBufferView<ArrayBuffer>,
|
|
48
|
+
);
|
|
49
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
50
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function verifyArtifactIntegrity(
|
|
55
|
+
circuitName: CircuitNameString,
|
|
56
|
+
artifactType: ArtifactType,
|
|
57
|
+
data: Uint8Array,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const expectedHash = ARTIFACT_HASHES[circuitName]?.[artifactType];
|
|
60
|
+
if (expectedHash === undefined) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`No integrity hash registered for ${circuitName}.${artifactType}. ` +
|
|
63
|
+
`Refusing to load unverified artifact.`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const actualHash = await sha256Hex(data);
|
|
68
|
+
if (actualHash !== expectedHash) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Integrity check failed for ${circuitName}.${artifactType}: ` +
|
|
71
|
+
`expected ${expectedHash}, got ${actualHash}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
VersionString,
|
|
11
11
|
} from "./circuits.interface.js";
|
|
12
12
|
import { importFetchVersionedArtifact } from "./fetchArtifacts.js";
|
|
13
|
+
import { verifyArtifactIntegrity } from "./artifactHashes.js";
|
|
13
14
|
|
|
14
15
|
interface CircuitOptions {
|
|
15
16
|
baseUrl?: string;
|
|
@@ -141,6 +142,13 @@ export class Circuits implements CircuitsInterface {
|
|
|
141
142
|
this._fetchVersionedArtifact(["artifacts", assetName.vkey].join("/")),
|
|
142
143
|
this._fetchVersionedArtifact(["artifacts", assetName.zkey].join("/")),
|
|
143
144
|
]);
|
|
145
|
+
|
|
146
|
+
await Promise.all([
|
|
147
|
+
verifyArtifactIntegrity(circuitName, "wasm", wasm),
|
|
148
|
+
verifyArtifactIntegrity(circuitName, "vkey", vkey),
|
|
149
|
+
verifyArtifactIntegrity(circuitName, "zkey", zkey),
|
|
150
|
+
]);
|
|
151
|
+
|
|
144
152
|
return { wasm, vkey, zkey };
|
|
145
153
|
}
|
|
146
154
|
|
|
@@ -2,6 +2,7 @@ import { poseidon } from "maci-crypto/build/ts/hashing.js";
|
|
|
2
2
|
import { Hash, Secret } from "../types/commitment.js";
|
|
3
3
|
import { Hex, bytesToNumber } from "viem";
|
|
4
4
|
import { mnemonicToAccount } from "viem/accounts";
|
|
5
|
+
import { generateMasterKeys } from "../crypto.js";
|
|
5
6
|
import { mapLimit } from "async";
|
|
6
7
|
import { DataService } from "./data.service.js";
|
|
7
8
|
import {
|
|
@@ -71,7 +72,7 @@ export class AccountService {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
|
-
* Initializes a new account from a mnemonic phrase.
|
|
75
|
+
* Initializes a new account from a mnemonic phrase for the legacy account.
|
|
75
76
|
*
|
|
76
77
|
* @param mnemonic - The mnemonic phrase to derive keys from
|
|
77
78
|
* @returns A new PrivacyPoolAccount with derived master keys
|
|
@@ -85,9 +86,8 @@ export class AccountService {
|
|
|
85
86
|
* @throws {AccountError} If account initialization fails
|
|
86
87
|
* @private
|
|
87
88
|
*/
|
|
88
|
-
|
|
89
|
+
protected static _initializeLegacyAccount(mnemonic: string): PrivacyPoolAccount {
|
|
89
90
|
try {
|
|
90
|
-
this.logger.debug("Initializing account with mnemonic");
|
|
91
91
|
|
|
92
92
|
const masterNullifierSeed = bytesToNumber(
|
|
93
93
|
mnemonicToAccount(mnemonic, { accountIndex: 0 }).getHdKey().privateKey!
|
|
@@ -113,6 +113,40 @@ export class AccountService {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Initializes a new account from a mnemonic phrase.
|
|
118
|
+
*
|
|
119
|
+
* @param mnemonic - The mnemonic phrase to derive keys from
|
|
120
|
+
* @returns A new PrivacyPoolAccount with derived master keys
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* This method derives two master keys from the mnemonic:
|
|
124
|
+
* 1. A master nullifier key from account index 0
|
|
125
|
+
* 2. A master secret key from account index 1
|
|
126
|
+
* These keys are used to deterministically generate nullifiers and secrets for deposits and withdrawals.
|
|
127
|
+
*
|
|
128
|
+
* @throws {AccountError} If account initialization fails
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
private _initializeAccount(mnemonic: string): PrivacyPoolAccount {
|
|
132
|
+
try {
|
|
133
|
+
this.logger.debug("Initializing account with mnemonic");
|
|
134
|
+
|
|
135
|
+
const { masterNullifier, masterSecret } = generateMasterKeys(mnemonic);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
masterKeys: [masterNullifier, masterSecret],
|
|
139
|
+
poolAccounts: new Map(),
|
|
140
|
+
creationTimestamp: 0n,
|
|
141
|
+
lastUpdateTimestamp: 0n,
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw AccountError.accountInitializationFailed(
|
|
145
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
116
150
|
/**
|
|
117
151
|
* Generates a deterministic nullifier for a deposit.
|
|
118
152
|
*
|
|
@@ -212,7 +246,7 @@ export class AccountService {
|
|
|
212
246
|
|
|
213
247
|
for (const account of accounts) {
|
|
214
248
|
// Skip accounts that have been ragequit
|
|
215
|
-
if (account.ragequit) {
|
|
249
|
+
if (account.ragequit || account.isMigrated) {
|
|
216
250
|
continue;
|
|
217
251
|
}
|
|
218
252
|
|
|
@@ -426,6 +460,75 @@ export class AccountService {
|
|
|
426
460
|
return newCommitment;
|
|
427
461
|
}
|
|
428
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Adds a new commitment to the account after migrate
|
|
465
|
+
*
|
|
466
|
+
* @param parentCommitment - The commitment that was spent
|
|
467
|
+
* @param value - The remaining value after spending
|
|
468
|
+
* @param nullifier - The nullifier used for migrate
|
|
469
|
+
* @param secret - The secret used for migrate
|
|
470
|
+
* @param blockNumber - The block number of the withdrawal
|
|
471
|
+
* @param txHash - The transaction hash of the withdrawal
|
|
472
|
+
* @returns The new commitment
|
|
473
|
+
*
|
|
474
|
+
* @remarks
|
|
475
|
+
* This method finds the account containing the parent commitment, creates a new
|
|
476
|
+
* commitment with the provided parameters, and adds it to the account's children.
|
|
477
|
+
* The new commitment inherits the label from the parent commitment.
|
|
478
|
+
*
|
|
479
|
+
* @throws {AccountError} If no account is found for the commitment
|
|
480
|
+
*/
|
|
481
|
+
public addMigrationCommitment(
|
|
482
|
+
parentCommitment: AccountCommitment,
|
|
483
|
+
value: bigint,
|
|
484
|
+
nullifier: Secret,
|
|
485
|
+
secret: Secret,
|
|
486
|
+
blockNumber: bigint,
|
|
487
|
+
txHash: Hex
|
|
488
|
+
): AccountCommitment {
|
|
489
|
+
let foundAccount: PoolAccount | undefined;
|
|
490
|
+
let foundScope: bigint | undefined;
|
|
491
|
+
|
|
492
|
+
for (const [scope, accounts] of this.account.poolAccounts.entries()) {
|
|
493
|
+
foundAccount = accounts.find((account) => {
|
|
494
|
+
if (account.deposit.hash === parentCommitment.hash) return true;
|
|
495
|
+
return account.children.some(
|
|
496
|
+
(child) => child.hash === parentCommitment.hash
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (foundAccount) {
|
|
501
|
+
foundScope = scope;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!foundAccount || !foundScope) {
|
|
507
|
+
throw AccountError.commitmentNotFound(parentCommitment.hash);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const precommitment = this._hashPrecommitment(nullifier, secret);
|
|
511
|
+
const newCommitment: AccountCommitment = {
|
|
512
|
+
hash: this._hashCommitment(value, parentCommitment.label, precommitment),
|
|
513
|
+
value,
|
|
514
|
+
label: parentCommitment.label,
|
|
515
|
+
nullifier,
|
|
516
|
+
secret,
|
|
517
|
+
blockNumber,
|
|
518
|
+
txHash,
|
|
519
|
+
isMigration: true
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
foundAccount.children.push(newCommitment);
|
|
523
|
+
foundAccount.isMigrated = true;
|
|
524
|
+
|
|
525
|
+
this.logger.info(
|
|
526
|
+
`Added new commitment with value ${value} to account with label ${parentCommitment.label}`
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
return newCommitment;
|
|
530
|
+
}
|
|
531
|
+
|
|
429
532
|
/**
|
|
430
533
|
* Adds a ragequit event to an existing pool account
|
|
431
534
|
*
|
|
@@ -646,14 +749,15 @@ export class AccountService {
|
|
|
646
749
|
*/
|
|
647
750
|
private _processDepositEvents(
|
|
648
751
|
scope: Hash,
|
|
649
|
-
depositEvents: Map<Hash, DepositEvent
|
|
752
|
+
depositEvents: Map<Hash, DepositEvent>,
|
|
753
|
+
startIndex: bigint = 0n,
|
|
650
754
|
): void {
|
|
651
755
|
const MAX_CONSECUTIVE_MISSES = 10; // Large enough to avoid tx failures
|
|
652
756
|
|
|
653
757
|
const foundIndices = new Set<bigint>();
|
|
654
758
|
let consecutiveMisses = 0;
|
|
655
759
|
|
|
656
|
-
for (let index =
|
|
760
|
+
for (let index = startIndex; ; index++) {
|
|
657
761
|
// Generate nullifier, secret, and precommitment for this index
|
|
658
762
|
const { nullifier, secret, precommitment } = this.createDepositSecrets(
|
|
659
763
|
scope,
|
|
@@ -727,9 +831,9 @@ export class AccountService {
|
|
|
727
831
|
// Process each account in parallel for better performance
|
|
728
832
|
for (const account of accounts) {
|
|
729
833
|
let currentCommitment = account.deposit;
|
|
730
|
-
let index = BigInt(
|
|
834
|
+
let index = BigInt(account.children.length);
|
|
731
835
|
|
|
732
|
-
// Continue processing withdrawals until no more are found
|
|
836
|
+
// Continue processing withdrawals until no more are found sequentially
|
|
733
837
|
while (true) {
|
|
734
838
|
// Generate nullifier for this withdrawal
|
|
735
839
|
const nullifierHash = poseidon([currentCommitment.nullifier]) as Hash;
|
|
@@ -740,22 +844,48 @@ export class AccountService {
|
|
|
740
844
|
break;
|
|
741
845
|
}
|
|
742
846
|
|
|
847
|
+
const remainingValue = currentCommitment.value - withdrawal.withdrawn;
|
|
848
|
+
|
|
743
849
|
// Generate secret for this withdrawal
|
|
744
850
|
const nullifier = this._genWithdrawalNullifier(account.label, index);
|
|
745
851
|
const secret = this._genWithdrawalSecret(account.label, index);
|
|
852
|
+
const precommitment = this._hashPrecommitment(nullifier, secret);
|
|
853
|
+
const accountCommitment = this._hashCommitment(remainingValue, currentCommitment.label, precommitment)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
// If the locally-computed hash doesn't match the on-chain commitment,
|
|
857
|
+
// the withdrawal was performed with different keys (e.g. migration from
|
|
858
|
+
// legacy to safe keys). Mark the child as unspendable from this account.
|
|
859
|
+
if (accountCommitment !== withdrawal.newCommitment) {
|
|
860
|
+
this.logger.info(
|
|
861
|
+
`Withdrawal commitment hash mismatch — marking as unspendable (migrated with different keys)`,
|
|
862
|
+
{ label: currentCommitment.label, expected: withdrawal.newCommitment, computed: accountCommitment }
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
// Add the withdrawal commitment to the account
|
|
866
|
+
const migrationCommitment = this.addMigrationCommitment(
|
|
867
|
+
currentCommitment,
|
|
868
|
+
remainingValue,
|
|
869
|
+
nullifier,
|
|
870
|
+
secret,
|
|
871
|
+
withdrawal.blockNumber,
|
|
872
|
+
withdrawal.transactionHash
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
currentCommitment = migrationCommitment;
|
|
876
|
+
} else {
|
|
877
|
+
// Add the withdrawal commitment to the account
|
|
878
|
+
const withdrawalCommitment = this.addWithdrawalCommitment(
|
|
879
|
+
currentCommitment,
|
|
880
|
+
remainingValue,
|
|
881
|
+
nullifier,
|
|
882
|
+
secret,
|
|
883
|
+
withdrawal.blockNumber,
|
|
884
|
+
withdrawal.transactionHash
|
|
885
|
+
);
|
|
746
886
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
currentCommitment,
|
|
750
|
-
currentCommitment.value - withdrawal.withdrawn,
|
|
751
|
-
nullifier,
|
|
752
|
-
secret,
|
|
753
|
-
withdrawal.blockNumber,
|
|
754
|
-
withdrawal.transactionHash
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
// Update current commitment to the newly created one
|
|
758
|
-
currentCommitment = newCommitment;
|
|
887
|
+
currentCommitment = withdrawalCommitment;
|
|
888
|
+
}
|
|
759
889
|
|
|
760
890
|
// Increment index for next potential withdrawal
|
|
761
891
|
index++;
|
|
@@ -799,6 +929,88 @@ export class AccountService {
|
|
|
799
929
|
}
|
|
800
930
|
}
|
|
801
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Discovers commitments that were migrated from legacy accounts via 0-value withdrawal.
|
|
934
|
+
*
|
|
935
|
+
* @param scope - The scope of the pool
|
|
936
|
+
* @param legacyAccounts - The legacy pool accounts for this scope
|
|
937
|
+
* @param withdrawalEvents - The map of withdrawal events (keyed by spentNullifier)
|
|
938
|
+
*
|
|
939
|
+
* @remarks
|
|
940
|
+
* When a legacy account performs a 0-value withdrawal to rotate keys (migration),
|
|
941
|
+
* the resulting on-chain commitment is created with safe keys. This method finds
|
|
942
|
+
* those commitments by:
|
|
943
|
+
* 1. Identifying legacy accounts with the `isMigrated` flag (set by `addMigrationCommitment`)
|
|
944
|
+
* 2. Computing the expected commitment hash using safe keys at withdrawal index 0
|
|
945
|
+
* 3. Verifying the hash exists in on-chain withdrawal events
|
|
946
|
+
* 4. Adding verified commitments as new safe pool accounts
|
|
947
|
+
*
|
|
948
|
+
* @private
|
|
949
|
+
*/
|
|
950
|
+
private _discoverMigratedCommitments(
|
|
951
|
+
scope: Hash,
|
|
952
|
+
legacyAccounts: PoolAccount[],
|
|
953
|
+
withdrawalEvents: Map<Hash, WithdrawalEvent>
|
|
954
|
+
): void {
|
|
955
|
+
// Build reverse lookup: newCommitment hash → WithdrawalEvent
|
|
956
|
+
const newCommitmentMap = new Map<Hash, WithdrawalEvent>();
|
|
957
|
+
for (const event of withdrawalEvents.values()) {
|
|
958
|
+
newCommitmentMap.set(event.newCommitment, event);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
for (const legacyAccount of legacyAccounts) {
|
|
962
|
+
// Skip if not flagged as migrated (set by addMigrationCommitment)
|
|
963
|
+
if (!legacyAccount.isMigrated) continue;
|
|
964
|
+
|
|
965
|
+
const migrationChild = legacyAccount.children.find(c => c.isMigration);
|
|
966
|
+
if (!migrationChild) continue;
|
|
967
|
+
|
|
968
|
+
const label = legacyAccount.label;
|
|
969
|
+
|
|
970
|
+
// The migration child's value is the remaining value carried forward.
|
|
971
|
+
// Zero-value migrations (full withdrawal + key rotation) are valid and
|
|
972
|
+
// must still be registered so that poolAccounts.length reflects the
|
|
973
|
+
// correct slot count for deposit index alignment in step C.
|
|
974
|
+
const remainingValue = migrationChild.value;
|
|
975
|
+
|
|
976
|
+
// Generate safe nullifier/secret at withdrawal index 0
|
|
977
|
+
const nullifier = this._genWithdrawalNullifier(label, 0n);
|
|
978
|
+
const secret = this._genWithdrawalSecret(label, 0n);
|
|
979
|
+
|
|
980
|
+
// Compute expected commitment hash
|
|
981
|
+
const precommitment = this._hashPrecommitment(nullifier, secret);
|
|
982
|
+
const expectedHash = this._hashCommitment(remainingValue, label, precommitment);
|
|
983
|
+
|
|
984
|
+
// Verify hash exists in withdrawal events' newCommitment
|
|
985
|
+
const withdrawalEvent = newCommitmentMap.get(expectedHash);
|
|
986
|
+
if (!withdrawalEvent) continue;
|
|
987
|
+
|
|
988
|
+
// Verified — add as a new safe pool account
|
|
989
|
+
const newAccount = this.addPoolAccount(
|
|
990
|
+
scope,
|
|
991
|
+
remainingValue,
|
|
992
|
+
nullifier,
|
|
993
|
+
secret,
|
|
994
|
+
label,
|
|
995
|
+
withdrawalEvent.blockNumber,
|
|
996
|
+
withdrawalEvent.transactionHash,
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
this.addWithdrawalCommitment(
|
|
1000
|
+
newAccount.deposit,
|
|
1001
|
+
remainingValue,
|
|
1002
|
+
nullifier,
|
|
1003
|
+
secret,
|
|
1004
|
+
withdrawalEvent.blockNumber,
|
|
1005
|
+
withdrawalEvent.transactionHash,
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
this.logger.info(
|
|
1009
|
+
`Discovered migrated commitment for label ${label} with value ${remainingValue}`,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
802
1014
|
/**
|
|
803
1015
|
* Initializes an AccountService instance with events for a given set of pools
|
|
804
1016
|
*
|
|
@@ -830,7 +1042,7 @@ export class AccountService {
|
|
|
830
1042
|
service: AccountService;
|
|
831
1043
|
},
|
|
832
1044
|
pools: PoolInfo[]
|
|
833
|
-
): Promise<{ account: AccountService; errors: PoolEventsError[] }> {
|
|
1045
|
+
): Promise<{ account: AccountService; legacyAccount?: AccountService; errors: PoolEventsError[] }> {
|
|
834
1046
|
// Log the start of the history retrieval process
|
|
835
1047
|
const logger = new Logger({ prefix: "Account" });
|
|
836
1048
|
logger.info(`Fetching events for pools`, { poolLength: pools.length });
|
|
@@ -844,32 +1056,114 @@ export class AccountService {
|
|
|
844
1056
|
uniqueScopes.add(pool.scope);
|
|
845
1057
|
}
|
|
846
1058
|
|
|
1059
|
+
// Retry path (non-migration): reuse the existing service's account and
|
|
1060
|
+
// only process pools whose scopes haven't been fully processed yet.
|
|
1061
|
+
// Already-processed scopes are skipped to avoid duplicate deposits and
|
|
1062
|
+
// withdrawal misclassification.
|
|
1063
|
+
//
|
|
1064
|
+
// This path performs simple deposit/withdrawal/ragequit processing only
|
|
1065
|
+
// — no migration discovery. For migration-aware retries, the caller
|
|
1066
|
+
// should re-invoke with { mnemonic } scoped to only the failed pools;
|
|
1067
|
+
// the mnemonic path builds both safe and legacy accounts from scratch
|
|
1068
|
+
// with no shared references.
|
|
1069
|
+
if (!('mnemonic' in source)) {
|
|
1070
|
+
const account = new AccountService(
|
|
1071
|
+
dataService,
|
|
1072
|
+
{ account: source.service.account }
|
|
1073
|
+
);
|
|
1074
|
+
const processedScopes = source.service.account.poolAccounts;
|
|
1075
|
+
const newPools = pools.filter((p) => !processedScopes.has(p.scope));
|
|
1076
|
+
|
|
1077
|
+
const errors = await account._processEvents(newPools);
|
|
1078
|
+
return { account, errors };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Mnemonic path: phased processing with migration discovery
|
|
1082
|
+
const account = new AccountService(dataService, { mnemonic: source.mnemonic });
|
|
1083
|
+
const legacyPrivacyPoolAccount = AccountService._initializeLegacyAccount(source.mnemonic);
|
|
1084
|
+
const legacyAccount = new AccountService(dataService, { account: legacyPrivacyPoolAccount });
|
|
1085
|
+
|
|
1086
|
+
const errors = await account._processEvents(pools, legacyAccount);
|
|
1087
|
+
return { account, legacyAccount, errors };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Fetches and processes events for a set of pools.
|
|
1092
|
+
*
|
|
1093
|
+
* When a legacyAccount is provided, the full migration-aware pipeline runs
|
|
1094
|
+
* for each scope:
|
|
1095
|
+
* 1. Legacy account: process deposits and withdrawals (to detect migrations)
|
|
1096
|
+
* 2. Safe account: discover migrated commitments from the legacy accounts
|
|
1097
|
+
* 3. Safe account (this): process deposits (starting after migrated accounts)
|
|
1098
|
+
* 4. Safe account: process withdrawals (now includes migrated accounts)
|
|
1099
|
+
* 5. Both accounts: process ragequits
|
|
1100
|
+
*
|
|
1101
|
+
* Migration discovery (step 2) must run before safe deposit scanning (step 3)
|
|
1102
|
+
* so that the migrated account count can be used as the starting index.
|
|
1103
|
+
* Post-migration deposits use poolAccounts.length as their index, which
|
|
1104
|
+
* sits right after the migrated slots; scanning from 0 would hit
|
|
1105
|
+
* MAX_CONSECUTIVE_MISSES on the legacy-key indices and never reach them.
|
|
1106
|
+
*
|
|
1107
|
+
* Without a legacyAccount, only steps 3, 4, and 5 run (simple processing).
|
|
1108
|
+
*
|
|
1109
|
+
* Per-scope errors are caught and returned rather than thrown, and any
|
|
1110
|
+
* partial state left by a mid-scope failure is cleaned from both accounts
|
|
1111
|
+
* so that a subsequent retry starts fresh for that scope.
|
|
1112
|
+
*/
|
|
1113
|
+
private async _processEvents(
|
|
1114
|
+
pools: PoolInfo[],
|
|
1115
|
+
legacyAccount?: AccountService,
|
|
1116
|
+
): Promise<PoolEventsError[]> {
|
|
847
1117
|
const errors: PoolEventsError[] = [];
|
|
848
|
-
const account = new AccountService(
|
|
849
|
-
dataService,
|
|
850
|
-
"mnemonic" in source
|
|
851
|
-
? { mnemonic: source.mnemonic }
|
|
852
|
-
: { account: source.service.account }
|
|
853
|
-
);
|
|
854
1118
|
|
|
855
|
-
const events = await
|
|
1119
|
+
const events = await this.getEvents(pools);
|
|
856
1120
|
|
|
857
1121
|
for (const [scope, result] of events.entries()) {
|
|
858
1122
|
if ("reason" in result) {
|
|
859
1123
|
errors.push(result);
|
|
860
1124
|
} else {
|
|
861
|
-
|
|
862
|
-
|
|
1125
|
+
try {
|
|
1126
|
+
// a. Legacy: process deposits + withdrawals
|
|
1127
|
+
if (legacyAccount) {
|
|
1128
|
+
legacyAccount._processDepositEvents(scope, result.depositEvents);
|
|
1129
|
+
legacyAccount._processWithdrawalEvents(scope, result.withdrawalEvents);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// b. Safe: discover migrated commitments from legacy accounts.
|
|
1133
|
+
// Must run before safe deposit scanning so that the migrated
|
|
1134
|
+
// account count can serve as the starting index for step (c),
|
|
1135
|
+
// avoiding a gap of consecutive misses over legacy-key indices.
|
|
1136
|
+
if (legacyAccount) {
|
|
1137
|
+
const legacyAccounts = legacyAccount.account.poolAccounts.get(scope) ?? [];
|
|
1138
|
+
this._discoverMigratedCommitments(scope, legacyAccounts, result.withdrawalEvents);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// c. Safe: process deposits, starting after any migrated accounts.
|
|
1142
|
+
// New deposits created after migration use poolAccounts.length as
|
|
1143
|
+
// their index, so they sit right after the migrated slots.
|
|
1144
|
+
const depositStartIndex = BigInt(this.account.poolAccounts.get(scope)?.length ?? 0);
|
|
1145
|
+
this._processDepositEvents(scope, result.depositEvents, depositStartIndex);
|
|
863
1146
|
|
|
864
|
-
|
|
865
|
-
|
|
1147
|
+
// d. Safe: process withdrawals (now includes migrated accounts)
|
|
1148
|
+
this._processWithdrawalEvents(scope, result.withdrawalEvents);
|
|
866
1149
|
|
|
867
|
-
|
|
868
|
-
|
|
1150
|
+
// e. Both: process ragequits
|
|
1151
|
+
if (legacyAccount) {
|
|
1152
|
+
legacyAccount._processRagequitEvents(scope, result.ragequitEvents);
|
|
1153
|
+
}
|
|
1154
|
+
this._processRagequitEvents(scope, result.ragequitEvents);
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
this.account.poolAccounts.delete(scope);
|
|
1157
|
+
legacyAccount?.account.poolAccounts.delete(scope);
|
|
1158
|
+
errors.push({
|
|
1159
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
1160
|
+
scope,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
869
1163
|
}
|
|
870
1164
|
}
|
|
871
1165
|
|
|
872
|
-
return
|
|
1166
|
+
return errors;
|
|
873
1167
|
}
|
|
874
1168
|
|
|
875
1169
|
/**
|
package/src/core/data.service.ts
CHANGED
|
@@ -158,7 +158,7 @@ export class DataService {
|
|
|
158
158
|
depositor: depositor.toLowerCase(),
|
|
159
159
|
commitment: commitment as Hash,
|
|
160
160
|
label: label as Hash,
|
|
161
|
-
value: value
|
|
161
|
+
value: value ?? BigInt(0),
|
|
162
162
|
precommitment: precommitment as Hash,
|
|
163
163
|
blockNumber: BigInt(typedLog.blockNumber),
|
|
164
164
|
transactionHash: typedLog.transactionHash,
|
|
@@ -252,13 +252,7 @@ export class DataService {
|
|
|
252
252
|
_newCommitment: newCommitment,
|
|
253
253
|
} = typedLog.args;
|
|
254
254
|
|
|
255
|
-
if (
|
|
256
|
-
!value ||
|
|
257
|
-
!spentNullifier ||
|
|
258
|
-
!newCommitment ||
|
|
259
|
-
!typedLog.blockNumber ||
|
|
260
|
-
!typedLog.transactionHash
|
|
261
|
-
) {
|
|
255
|
+
if (value == null || !spentNullifier || !newCommitment || !typedLog.blockNumber || !typedLog.transactionHash) {
|
|
262
256
|
throw DataError.invalidLog("withdrawal", "missing required fields");
|
|
263
257
|
}
|
|
264
258
|
|
|
@@ -374,7 +368,7 @@ export class DataService {
|
|
|
374
368
|
ragequitter: ragequitter.toLowerCase(),
|
|
375
369
|
commitment: commitment as Hash,
|
|
376
370
|
label: label as Hash,
|
|
377
|
-
value: value
|
|
371
|
+
value: value ?? BigInt(0),
|
|
378
372
|
blockNumber: BigInt(typedLog.blockNumber),
|
|
379
373
|
transactionHash: typedLog.transactionHash,
|
|
380
374
|
};
|
package/src/core/tmp.ts
ADDED
package/src/crypto.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { mnemonicToAccount } from "viem/accounts";
|
|
2
|
-
import { bytesToNumber } from "viem/utils";
|
|
3
2
|
import { poseidon } from "maci-crypto/build/ts/hashing.js";
|
|
4
3
|
import { LeanIMT, LeanIMTMerkleProof } from "@zk-kit/lean-imt";
|
|
5
4
|
import {
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
Withdrawal,
|
|
14
13
|
MasterKeys,
|
|
15
14
|
} from "./types/index.js";
|
|
16
|
-
import { encodeAbiParameters, Hex, keccak256, numberToHex } from "viem";
|
|
15
|
+
import { bytesToBigInt, encodeAbiParameters, Hex, keccak256, numberToHex } from "viem";
|
|
17
16
|
import { SNARK_SCALAR_FIELD } from "./constants.js";
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -45,16 +44,16 @@ export function generateMasterKeys(mnemonic: string): MasterKeys {
|
|
|
45
44
|
);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
const key1 =
|
|
47
|
+
const key1 = bytesToBigInt(
|
|
49
48
|
mnemonicToAccount(mnemonic, { accountIndex: 0 }).getHdKey().privateKey!,
|
|
50
49
|
);
|
|
51
50
|
|
|
52
|
-
const key2 =
|
|
51
|
+
const key2 = bytesToBigInt(
|
|
53
52
|
mnemonicToAccount(mnemonic, { accountIndex: 1 }).getHdKey().privateKey!,
|
|
54
53
|
);
|
|
55
54
|
|
|
56
|
-
const masterNullifier = poseidon([
|
|
57
|
-
const masterSecret = poseidon([
|
|
55
|
+
const masterNullifier = poseidon([key1]) as Secret;
|
|
56
|
+
const masterSecret = poseidon([key2]) as Secret;
|
|
58
57
|
|
|
59
58
|
return { masterNullifier, masterSecret };
|
|
60
59
|
}
|