ZUL://DOCSPRIVACY LAYER 2 — SETTLED ON SOLANA
INDEX
SHIELDED POOL — /docs/shielded/nullifiers

Nullifiers

Spending a private note has a paradox to solve: the chain must reject double-spends of a note it is not allowed to identify. Nullifiers are the standard resolution, and ZUL uses them in their cleanest form.

Definition

NULLIFIER
nf = Poseidon(commitment, leaf_index, s)

// s is derived from the owner's secret key —
// only the owner can compute a note's nullifier,
// and nothing about (commitment, leaf_index) can be
// recovered from nf without s

Every spend publishes the nullifiers of its input notes. The node keeps a global nullifier set; a transaction whose nullifier already exists is rejected before execution. That is the entire double-spend story — no amounts, no identities, one set-membership check.

Why this preserves privacy

  • Unlinkable — the nullifier is a one-way function of the note. Observers see nfbut cannot walk it back to a commitment in the tree without the owner's secret.
  • Deterministic — a given note has exactly one nullifier, so two spends of the same note collide no matter who builds them.
  • Proof-bound — the transfer circuitproves the published nullifier was computed correctly from a real note in the tree, so you cannot publish a junk nullifier to "burn" someone else's funds.

What an observer learns

From a transfer, exactly this: some previously-created notes were spent, and some new notes were created. The counts are public (the explorer shows +commitments / −nullifiers per pool transaction); the mapping between them is not — a 2-in/2-out join-split reveals no pairing between inputs and outputs.

EXPLORER

The nullifier total on /explorer/shielded is the count of notes ever spent. Subtracting it from total commitments bounds the number of live notes — that bound is public; which notes are live is not.