Skip to main content

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:

  1. Confirm your SDK and circuits belong to the same deployment set.
  2. Confirm your pool address matches the verifier those circuits were compiled against.
  3. See Sepolia V9 addresses.

Local state drift recovery

If NoteNotFoundError or PoolVault_NullifierAlreadySpent hits after one transaction lands and the follow-up flow fails:

  1. session.discoverNotes() to re-reconcile.
  2. If notes are still wrong, clear local cache: localStorage keys with prefix privacy-pool: (the default namespace set in LocalStorageService).
  3. Next discoverNotes() after the clear rescans from genesis (the cursor resets to 0x0), so pass fromBlock with 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 aspUrl so the SDK uses the snapshot fast-path.
  • Pass fromBlock to session.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.