Skip to main content

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.fromConfig is undefined. The message prefixes the failing field as path: message from 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_KEY env var, undefined RELAYER_ADDRESS, protocolKeys.viewingPublicKey empty 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) via eth_call. Nullifier formula: Poseidon(privateNullifyingKey, commitment).

PoolVault_InvalidProcessor()

  • Cause: transactParams.processor does not match the address that actually submitted PoolVault.transact.
  • Fix: bind the proof to the right processor: the one your relayer designates (RelayerInfo.processorAddress; the deployed Sepolia relayer uses the PrivacyPoolRelay contract 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 TransactParams and NoteData.
  • 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 keystoreMaxDepth than 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_notes cache) 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_getLogs at 10k blocks/page from the fromBlock passed to discoverNotes() (defaulting to the NoteManager sync cursor) to head. On a fresh NoteManager without fromBlock, scans start from genesis: thousands of RPC calls, hits free-tier rate limits.
  • Fix: pass fromBlock as the deploy block of the current pool: discoverNotes({ fromBlock: deployBlockHex }). Or configure aspUrl so 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 SPENT note in the in-memory NoteManager was not persisted before reload, typically after an interrupted write. The next discoverNotes then resurrects the spent note as ACTIVE.
  • 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 with expo start -c.