Jat
CONCEPTS

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 -> revert
Note
Using a strict initialization rather than an idempotent one is deliberate. It turns the runtime account model itself into the double-spend guard, with no counter to keep and no operator to trust.

Why 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.