Overview
Burning a Stealth Pool Note presents a ZK proof on-chain that you know the secret inputs behind a Merkle tree commitment, burns the nullifier to prevent double-spending, and releases the tokens. You choose where the tokens go:- Into an EncryptedTokenAccount — tokens remain private after burning. Default for receiver-burnable.
- Into an AssociatedTokenAccount — tokens become visible in the destination ATA. Self-burnable only in V18.
V18 burner factories
All three live under@umbra-privacy/sdk/burn.
Receiver-burnable → ETA
Self-burnable → ETA
Self-burnable → ATA
Receiver-burnable → ATA is supported on-chain (
claim_to_receiver in the anchor program) but is not yet exposed in the SDK. Use receiver-burnable → ETA, then a regular withdrawal, until the SDK ships the direct path.Function signature
All three burners share the same call signature:Parameters
Notes returned by the scanner — pick them out of
result.{etaToStealthPoolSelfBurnable, etaToStealthPoolReceiverBurnable, ataToStealthPoolSelfBurnable, ataToStealthPoolReceiverBurnable}. The burner fetches per-batch Merkle proofs internally; do not pre-attach proofs.32 bytes of opaque metadata stored with the burn. Defaults to all zeros. Must be a pre-hashed or pre-encrypted 32-byte value.
Optional priority fee in micro-lamports per Arcium computation unit, applied to the relayer-submitted transactions.
Deps
Per-batch Merkle proof fetcher. Use
client.fetchBatchMerkleProof (auto-built when indexerApiEndpoint is set on the client).Circuit-specific Groth16 prover (one per burn variant). Provers ship at
@umbra-privacy/sdk/zk-prover.Relayer adapter. The three property names are V18 — they are TypeScript aliases (
BurnSubmitterFunction = ClaimSubmitterFunction, etc.), so a relayer client built via getUmbraRelayer({ apiEndpoint }) plugs in directly under the renamed property names.Return Value
Receiver-into-ETA and self-into-ETA both returnBurnStealthPoolNoteIntoETAResult. Self-into-ATA returns BurnStealthPoolNoteIntoATAResult. Both shapes:
completed / callback_received indicate success. The signature you want to surface to the user is callbackSignature ?? txSignature. failureReason containing "NullifierAlreadyBurnt" means the note was already burnt — the SDK treats this as idempotent success.
Umbra Confidential SPL token — lottery points: When burning into an ETA, the Arcium MPC generates a
rescue_encrypted_lottery_ticket_delta and adds it to your ETA’s running point totals. This field is present in the raw relayer batch response but is not yet surfaced in BurnBatchResult. See Umbra Confidential SPL Token for the current SDK status.Batching behaviour
- Receiver-burnable → ETA batches natively: groups notes by
destinationAddress, chunks to ≤5 per proof. Pass the whole array; do not reimplement chunking. - Self-burnable → ETA / ATA has
MAX_NOTES_PER_PROOF = 1. The SDK loops internally — caller still passes an array; the result has one batch per note.
Full example: scan + burn
ZK Proof Generation
Burns generate a Groth16 ZK proof that proves:- You know the secret inputs to one of the commitments in the Merkle tree.
- The corresponding nullifier has not been burnt before.
Stale Merkle Proofs
Merkle proofs can become stale if the tree root changes between when the proof is fetched and when the burn is submitted. In V18 the burner fetches the proof per batch, immediately before submission, so this is much less likely than under V13’s scan-time proof bundling. If a burn fails with a root mismatch, simply re-run it — the burner refetches the proof on each attempt.Do not call
enrichWithMerkleProof yourself in normal use. The burner handles it. The helper is exported from @umbra-privacy/sdk/burn only for callers building a fully custom burn pipeline that bypasses the orchestrator factories.Dropped relayer callback
The relayer’s MPC callback can drop (network partition, compute-budget exhaustion, Arcium downtime). If you re-submit the samerequest_id while the nullifier is still reserved upstream, the relayer returns HTTP 409 with code DUPLICATE_OFFSET — wait, re-check on-chain that the nullifier has not landed, and retry.
The burner factory handles this automatically: polling, detecting NullifierAlreadyBurnt as idempotent success, and surfacing a clean status. If you are building a custom burn pipeline, see Errors — Idempotent burn retry.
Error Handling
Nullifier Reuse
Once a note is burnt, its nullifier is recorded in an on-chain treap. Any attempt to burn the same note again will be rejected by the program withNullifierAlreadyBurnt. You do not need to track this client-side — the on-chain check is authoritative, and the SDK’s burner treats this status as idempotent success.
See Error Handling for the full error reference.