Nullifiers and double-spend
A nullifier is the one-way fingerprint that lets a deposit be spent exactly once without revealing which deposit it was. It is how the pool prevents double-spending while keeping withdrawals unlinkable.
The problem
A withdrawal proof shows that you own some leaf in the tree, on purpose without saying which one. That privacy creates a risk: nothing in the proof alone stops you from submitting it twice and draining two notes from one deposit. The pool needs a way to mark a note as spent that does not break the anonymity it just provided.
The nullifier hash
Every deposit carries a random nullifier. When you withdraw, the proof publishes only its hash:
nullifierHash = Poseidon(nullifier)The hash is revealed; the nullifier itself stays private. The circuit proves that the revealed hash corresponds to the same nullifier baked into the committed leaf, so you cannot present a hash that is not really yours.
Single-use, enforced on-chain
On withdrawal the program creates a small account at a program derived address keyed by the nullifier hash. The account is created with a plain initialization that fails if the address already exists. The first withdrawal succeeds and claims that address; any second attempt with the same nullifier hits an account that is already there and reverts.
first withdraw : init nullifier PDA -> ok, funds paid out
replay : init nullifier PDA -> already exists -> revertWhy it stays private
The published nullifier hash is unlinkable to the deposit because Poseidon is one-way and the deposit leaf committed to the nullifier indirectly, through Poseidon(nullifier, secret). An observer sees a fresh hash and a payout to a fresh address. They cannot connect it to any particular leaf, and they cannot derive the nullifier to grief you. The same idea protects stealth claims so that a reused payment link cannot be replayed.
Nullifiers are one half of the pool design; the membership proof is the other. See the fixed-denomination pool for how the two combine in a single withdrawal.