Debugging
When something goes wrong end-to-end, here's where to look first.
Decode a PoolVault revert
Reverts come back as 4-byte selectors. Decode:
cast sig "PoolVault_NullifierAlreadySpent()" # → 0x66f527f1
cast sig "PoolVault_InvalidTransactProof()" # → 0x...
# Or brute-force against a list:
for E in PoolVault_NullifierAlreadySpent PoolVault_InvalidProcessor PoolVault_InvalidTransactProof PoolVault_ProofContextMismatch; do
echo "$E:" $(cast sig "${E}()")
done
Trace relayer 4xx
When the relayer rejects a submission, session.relayTransfer throws a RelayerRejected whose message carries the relayer's HTTP status (e.g. "HTTP 400: Bad Request"). The SDK surfaces the status, status text, and up to 500 characters of the response body in err.message. Check err.message first, since the relayer's body usually names the rejection reason. RelayerRejected is type-only in the public package, so branch on err.name.
try {
await session.relayTransfer(prepared.relayOptions[0]);
} catch (err) {
if (err instanceof Error && err.name === "RelayerRejected") {
console.error("Relayer rejected:", err.message);
}
throw err;
}
Verify proving-key vs verifier match
If you see PoolVault_InvalidTransactProof on a transact that should otherwise be valid:
- Confirm your SDK and circuits belong to the same deployment set.
- Confirm your pool address matches the verifier those circuits were compiled against.
- See Sepolia V9 addresses.
Local state drift recovery
If NoteNotFoundError or PoolVault_NullifierAlreadySpent hits after one transaction lands and the follow-up flow fails:
session.discoverNotes()to re-reconcile.- If notes are still wrong, clear local cache:
localStoragekeys with prefixprivacy-pool:(the default namespace set inLocalStorageService). - Next
discoverNotes()after the clear rescans from genesis (the cursor resets to0x0), so passfromBlockwith the pool's deploy block to bound it.
Builder validation errors
InvalidBuilderConfig messages carry the failing field as path: message from the first Zod issue. When several fields are wrong, only the first is shown, so wrap your call site and dump the config object to see the rest.
RPC rate limits
Symptom: discovery hangs for many minutes, eventual 429 errors in the network tab. Fixes:
- Configure
aspUrlso the SDK uses the snapshot fast-path. - Pass
fromBlocktosession.discoverNotes({ fromBlock })so scans start from the pool's deploy block instead of genesis. - Switch to a paid RPC tier.
Stale bundler cache after an SDK swap
After swapping an SDK tarball, pnpm install reshuffles the .pnpm/ identities, but a stale build cache can still hold compiled references to the old paths. In the Next.js-based Payroll PoC the symptom is ENOENT: ...privacy-pools-v2-sdk@{old-hash}.tgz..., fixed by clearing the .next/ cache:
rm -rf .next
pnpm dev
The current v2 frontend is an Expo app rather than Next.js, so the equivalent reset there is clearing the Metro bundler cache with expo start -c.