What is a Stealth Pool Note?
A Stealth Pool Note is a UTXO-style commitment leaf in the mixer’s Indexed Merkle Tree. The SDK surfaces these commitments as “notes”, and the act of consuming one is a “burn”. A note encodes:- Amount — how many tokens are locked.
- Destination address — who is authorised to burn.
- Secret randomness — private entropy known only to the writer (and, for receiver-burnable notes, the recipient).
The Stealth Pool: How It Works
The Stealth Pool is a shared Indexed Merkle Tree stored on-chain. Each leaf in the tree is a note commitment.Write a note
You call one of the four
…StealthPoolNote…Creator factory functions (see below). The SDK computes a commitment from (amount, destination, randomness) and inserts it as a new leaf into the tree. Your tokens are locked in the pool custody account.At this point, anyone can see that a note was written and the tree grew by one leaf — but cannot see the amount, destination, or any other detail.Build the anonymity set
As more users write notes into the same tree, your commitment becomes one of many. The larger the set, the harder it is to link the write to the eventual burn. Trees hold up to 1,048,576 leaves (depth-20 Indexed Merkle Tree).
Scan
The SDK queries the indexer for note ciphertexts, attempts to decrypt each with your viewing keys, and groups successful decrypts by
(burn kind, source). The scanner is zero-arg — it walks every active tree and tracks its own cursor in client.utxoDataStore. See Scanning.Burn
You present a zero-knowledge proof that proves:
- You know the secret inputs behind a commitment that exists in the tree.
- You haven’t burnt it before (nullifier is unspent).
The Four Note Variants
A note is parameterised on two axes — unlocker (who can burn it) × source (where the funds came from):- Self-burnable from ATA —
getATAIntoSelfBurnableStealthPoolNoteCreatorFunction— sender keeps the unlocker; funded from the writer’s ATA. Single tx, no MPC. - Self-burnable from ETA —
getETAIntoSelfBurnableStealthPoolNoteCreatorFunction— sender keeps the unlocker; funded from the writer’s ETA. Two-tx MPC pipeline. - Receiver-burnable from ATA —
getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction— unlocker encrypted against the recipient’suserCommitment; funded from the writer’s ATA. - Receiver-burnable from ETA —
getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction— same recipient model, funded from the writer’s ETA. Two-tx MPC pipeline.
Registration prerequisites differ by variant
This is one of the most common integration footguns.- Self-burnable creates encrypt the unlocker against the sender’s master-seed-derived key. Sender needs only
isUserAccountX25519KeyRegistered. Recipient needs nothing. - Receiver-burnable creates encrypt against the recipient’s
userCommitment. The recipient must have completed all three registration sub-steps on-chain (account init, X25519 key, user commitment). Otherwise the create simulation fails before any tx is broadcast.
getUserAccountQuerierFunction on the recipient address; if any flag is missing, either abort with a clear “ask the recipient to register on Umbra” error, or fall back to a self-burnable create (the sender stays the unlocker; recipient needs zero on-chain state — useful for one-shot transfers).
Nullifiers: Preventing Double-Spends
Each note has a corresponding nullifier — a deterministic hash derived from its private inputs. When a note is burnt, its nullifier is stored in an on-chain treap (a self-balancing sorted tree). Before allowing a burn, the on-chain program checks that:- The nullifier has not been seen before.
- The ZK proof is valid for a commitment in the current Merkle tree.
NullifierAlreadyBurnt in the failure reason if you re-submit a burn that has already landed — the SDK’s burner factory treats this as success and surfaces a clean “Note already burnt” string.
Ciphertext Discovery
After a note is written, an X25519 + AES-GCM ciphertext is published on-chain (and indexed off-chain). For self-burnable notes the ciphertext is addressed to the writer; for receiver-burnable notes it is addressed to the recipient. The ciphertext payload contains:- Amount (8 bytes).
- Destination address (32 bytes).
- Generation index (16 bytes).
- Domain separator identifying the note variant (12 bytes).
Your private key never leaves your device. Decryption happens in the SDK using locally derived X25519 keys.
Anonymity Set Size
The privacy guarantee of the pool depends on how many other notes exist in the same tree at the time you burn. A tree with only one leaf offers no privacy — it’s obvious which commitment is being burnt. In practice:- Wait for more users to write notes before burning.
- Burning into a different address from the write address increases privacy.
- Combining a burn into an ETA (rather than an ATA) hides the destination amount further.
Trees Fill Up
Each Merkle tree has a maximum of 1,048,576 leaves. When a tree is full, the on-chain write service starts a new tree at the next sequential index. Notes from different trees have separate anonymity sets. The scanner walks every active tree automatically — you do not need to specify a tree index. (The old(treeIndex, start, end) arguments are gone; cursor state lives in client.utxoDataStore and is per-tree under the hood.)