The shielded pool
Notes, commitments, nullifiers, and the shield / transfer / unshield lifecycle — with value conservation.
The shielded pool is a set of hidden balances. Your funds live as notes; the pool only ever sees opaque hashes of them. Spending a note reveals a nullifier (so it can't be spent twice) and creates new commitments (the outputs) — but never the amounts or owners.
Notes, commitments, nullifiers
A note is the unit of value. It is never stored on-chain in the clear.
privKey random field element (the spending secret)
pubKey = Poseidon(privKey)
note = (amount, pubKey, blinding)
commitment = Poseidon(amount, pubKey, blinding)
nullifier = Poseidon(commitment, leafIndex, privKey)pubKey = Poseidon(privKey)ties a note to a spending key without revealing it.commitmenthides amount + owner + blinding; it is what gets inserted into the pool's Merkle tree.nullifier = Poseidon(commitment, leafIndex, privKey)is published when a note is spent. It is:- deterministic for the owner (they hold
privKeyand know theirleafIndex); - unlinkable to the commitment for anyone else — you cannot compute it
without
privKey, and it is not invertible back to the commitment; - double-spend-safe — the same note at its unique tree position always yields the same nullifier, so a re-spend collides on-chain;
- ownership-binding — it needs the
privKeywhosePoseidon(privKey)is the note'spubKey, so you cannot nullify a note you do not own.
- deterministic for the owner (they hold
Notes are held client-side and are your funds — see Note management.
The Merkle tree
Commitments are appended to an incremental Poseidon(2) Merkle tree of depth
20 — capacity 1,048,576 notes. Empty leaves are 0, with precomputed
per-level zero-subtree hashes. Depth 20 is the Tornado-classic size; production
can raise it to 26 (~67M) or 32 (~4.3B) by changing a single circuit parameter.
The three operations
Every operation is one on-chain transaction carrying a 2-in / 2-out join-split proof (see Zero-knowledge).
- Shield — deposit public lamports into the pool.
publicAmountIn > 0,publicAmountOut == 0. The deposit is public; everything after it is not. - Transfer — move value between shielded identities. Both public amounts are
0; sender and amount are hidden. Change returns to the sender as a new note. - Unshield — withdraw to a public address.
publicAmountOut > 0; the destination is bound in the proof. Optionally routed through a relayer so the user's wallet never signs.
Value conservation
Value can never be created or destroyed. The circuit enforces:
Σ inAmount + publicAmountIn == Σ outAmount + publicAmountOut + feewith a 64-bit range check on every amount so no term can wrap the field. On the chain side this yields the pool's central invariant:
POOL_VAULT.lamports == Σ shielded-in − Σ shielded-outNothing enters or leaves the vault without a verified proof. For an unshield with
a relayer fee, the value leaving the vault is publicAmountOut + fee — the
recipient receives publicAmountOut, the relayer receives fee, and the
invariant still holds.