Shield Pool
A Poseidon-Merkle commitment pool that severs the deterministic link between deposit and withdrawal. Deposits hash into a tree; withdrawals prove set membership via Groth16 without revealing which leaf is being spent.
why a pool
On a transparent chain, every payment links a sender to a recipient. Even with stealth recipients, repeated patterns cluster wallets together. A shielded pool interposes a one-way cryptographic barrier: the public data of a withdraw cannot be tied to any particular deposit, only to some deposit in the active set.
commitments and nullifiers
A deposit inserts a commitment into the Merkle tree. A withdraw publishes a nullifier hash — a deterministic function of the deposit secret — that prevents double-spend without revealing which commitment is being spent. Both functions are Poseidon over BN254with circom-default parameters; the Solana program uses the runtime's native poseidon syscall to compute Merkle internal nodes.
nullifier_hash = Poseidon(ns, leaf_index)
Both ns and bfare 32-byte random values reduced into the BN254 base field. They never leave the depositor's device. The commitment alone is not enough to derive either; reconstructing them requires the depositor to sign a Groth16 proof.
Merkle tree
Each pool maintains a Merkle tree of height 15 (32,768 leaves on v2). The current root and three historical roots are stored on-chain so that proofs generated against a slightly stale view of the tree still verify. Deposits append at next_leaf_index and rotate the historical-root ring buffer.
Height 15 is the casual-privacy sweet spot on Solana: 32k capacity per pool keeps deposits cheap (CU bumped to ~600k via setComputeUnitLimit) while staying well above any realistic v2 traffic ceiling.
withdraw flow
- The depositor (now ready to withdraw) reconstructs the full leaves list by replaying
DepositCommittedevents from the program's log history. - The depositor builds a Merkle path for their own leaf and computes
nullifier_hash. - The depositor generates a Groth16 proof that they know (
ns,bf,leaf_index, and the path) consistent with the public Merkle root and the published nullifier hash. - The proof + public inputs are submitted to the relayer. The relayer wraps them into the on-chain
withdrawinstruction and pays gas + per-nullifier rent. - The on-chain verifier checks the Groth16 pairing equation, asserts the root is one of the pool's known roots, and atomically creates a
nullifier_recordPDA seeded by the nullifier hash. A repeat attempt collides on PDA init and reverts.
recipient binding
The proof binds to a hash of the recipient's token account. A malicious relayer cannot redirect funds at the last moment; any account substitution invalidates the proof. The on-chain verifier recomputes the recipient hash from the actual recipient account and compares it against the public input.
pool authority
The pool authority can call freeze_pool to halt deposits and withdrawals during an active investigation. The authority cannot move user funds; the vault is owned by a program-derived address that only the on-chain code controls. Freeze policy is documented at /docs/compliance.
The shield pool hides which commitment was spent. It does not hide that someone withdrew from the pool, nor the size of the pool, nor the denomination. Practical privacy depends on the active anonymity set; cold-start pools have weak unlinkability.