Failure modes
Concrete errors you will see, mapped to actual cause and the recovery path. Curated from real PoC integration sessions. Not exhaustive, so add as you find new ones.
Builder & config
InvalidBuilderConfig: Invalid input: expected string, received undefined
- Cause: a required string field in
PoolSessionBuilder.fromConfigis undefined. The message prefixes the failing field aspath: messagefrom the first Zod issue. - Fix: read the path in the message; for the full issue list, wrap your call site and dump the config before passing it in. Common culprits: empty
ASP_PUBLIC_KEYenv var, undefinedRELAYER_ADDRESS,protocolKeys.viewingPublicKeyempty when keys haven't been derived yet.
Transfer + relay
RecipientViewingKeyUnregistered
- Cause: the recipient's address has no viewing key registered on the on-chain Keystore. An unregistered recipient throws by default.
- Fix: pass
recipientDiscoveryData: {evmAddress, allowOutOfBandFallback: true}to opt into out-of-band note delivery. The receipt captures the noteSecret either way, so OoB is fine for most flows.
RelayerRejected: PoolVault_InvalidTransactProof()
- Cause: your local proving keys (zkeys) don't match the on-chain verifying key. Usually happens when the SDK, circuits, and on-chain verifier are from different deployment sets.
- Fix: align all three to the same deployment set. See Sepolia V9 addresses.
PoolVault_NullifierAlreadySpent() (selector 0x66f527f1)
- Cause: the chosen input note's nullifier is already on-chain. Local note state can lag after a failed or partial relay flow.
- Fix: before picking inputs, verify each candidate's nullifier against
PoolVault.spentNullifiers(nullifier)viaeth_call. Nullifier formula:Poseidon(privateNullifyingKey, commitment).
PoolVault_InvalidProcessor()
- Cause:
transactParams.processordoes not match the address that actually submittedPoolVault.transact. - Fix: bind the proof to the right processor: the one your relayer designates (
RelayerInfo.processorAddress; the deployed Sepolia relayer uses thePrivacyPoolRelaycontract for both transfers and withdrawals), or your own wallet for self-relay.
PoolVault_ProofContextMismatch()
- Cause: the proof's context hash no longer matches the encoded
TransactParamsandNoteData. - Fix: do not mutate the prepared payload after proving. If processor, note data, fee data, or payout routing changes, rebuild the proof.
Too many values for input signal keystoreSiblings
- Cause: circuits were compiled with a different
keystoreMaxDepththan the SDK's witness generator expects. The SDK and circuits are from different deployment sets with different tree depths. - Fix: align the SDK and circuits to the same deployment set.
Discovery
NoteNotFoundError: Note not found: 0x…
- Cause: the cycle / spend code picked an input commitment that the SDK's local NoteManager doesn't have. Usually state drift between a server-side preflight (reading the
indexed_notescache) and the client-side NoteManager. - Fix: call
session.discoverNotes()right before picking inputs to reconcile local state.
Discovery hangs / never completes
- Cause: SDK paginates
eth_getLogsat 10k blocks/page from thefromBlockpassed todiscoverNotes()(defaulting to the NoteManager sync cursor) to head. On a fresh NoteManager withoutfromBlock, scans start from genesis: thousands of RPC calls, hits free-tier rate limits. - Fix: pass
fromBlockas the deploy block of the current pool:discoverNotes({ fromBlock: deployBlockHex }). Or configureaspUrlso the SDK's HttpASPDataProvider pulls the encrypted Note stream from/{chainId}/public/note-events(gap-filling the tail over RPC) instead of chunking the log scan itself.
Persistence
Treasury balance refuses to decrease across page reload
- Cause: a
SPENTnote in the in-memory NoteManager was not persisted before reload, typically after an interrupted write. The nextdiscoverNotesthen resurrects the spent note asACTIVE. - Fix: call
await session.discoverNotes()to reconcile. It flushes note updates internally through the public session API, so you do not need to reach the lower-level note-manager directly. The transact path also flushes on success, so this only surfaces after an interrupted write. If the local index is corrupt rather than stale, reset it by clearing the configured persistent storage and re-running discovery.
Build / install
Next.js ENOENT referencing an old SDK tarball hash
- Cause: after swapping the vendored SDK tarball, Next.js's
.next/cache holds compiled references to the old.pnpm/identity, but pnpm reshuffled and the path no longer exists. This is specific to the Next.js-based Payroll PoC, not the current v2 frontend. - Fix: in the Payroll PoC, kill the dev server,
rm -rf .next, and restart after every SDK swap. The current v2 frontend is an Expo app, so the equivalent reset there is clearing the Metro bundler cache withexpo start -c.