> For AI agents: the documentation index is at https://docs.umbraprivacy.com/llms.txt, and every page is available as Markdown at its URL with .md appended.

# Nullifiers (/docs/state/nullifiers)

A nullifier is the value a spend publishes to cancel a note. Each note has exactly one, only its owner can compute it, and nothing about it points back to the note's commitment, so the chain learns that some note was spent and never which one.

## A cancelled cheque

Think of a cheque book where every cheque carries a serial number printed on a detachable stub. The stubs are kept in a locked drawer. Paying with a cheque means tearing off its stub and handing it in; the bank files the stub and refuses any second stub with the same number. Anyone can see the growing pile of stubs, but a stub alone does not say which cheque it came from.

A nullifier is that stub. The spend proof shows it was torn from a real note in the tree, owned by the spender, and the chain refuses any nullifier it has seen before.

## How a nullifier is derived

- Nullifier key: the owner's nullifier key, or the program owner commitment
- Insertion position: the leaf's global position, chosen by the chain
- Nullifier blinding factor: a random field of the note

circuit-form Poseidon under the owner kind's domain → Nullifier: one field element, public

*An identity-owned note uses the owner's nullifier key and domain 68; a program-owned note uses its program owner commitment and domain 69; a dummy slot uses the sender's nullifier key and domain 71.*

The circuit computes the nullifier with the circuit form of Poseidon over three inputs: a key, the note's insertion position and its nullifier blinding factor. For an identity-owned note the key is the owner's nullifier key: one of the keys committed inside the owner's identity commitment (sometimes called the user commitment), which the circuit opens and checks against the identity tree. For a program-owned note the key is the note's owner commitment, a hash of the owning address. The domain follows the owner the same way: 68 for an identity-owned note and 69 for a program-owned one. A dummy input slot, which holds no note, hashes the sender's own nullifier key under domain 71. A fee note, which only a fee collection spends, uses domain 70. See [hash functions](https://docs.umbraprivacy.com/docs/cryptography/hashing).

The circuit recomputes each input's nullifier from the note it has just proved is in the tree, and requires it to equal the nullifier the transaction publishes. A spender therefore cannot publish a made-up nullifier for a real note, or a real nullifier for a note they do not own.

### Why the position

The position is the one fact about a note that the chain, not the note's creator, decides. Every leaf in every pool index has a different position, so tying the nullifier to it gives each leaf exactly one nullifier. Two leaves that happened to wrap the same commitment would still have different nullifiers, and each could be spent once. The position is inside the leaf the spend proves, so a spender cannot substitute another one.

### Why the domain follows the slot's kind

A dummy input slot proves nothing about the tree, so its position and nullifier blinding factor are whatever the sender chooses, and its key is the sender's own nullifier key. An identity chooses that key at registration, and the registration circuit checks only that it is not zero. The key sits inside the identity commitment, so the chain cannot see that two identities registered the same one, and nothing stops an identity from registering a key equal to some address's program owner commitment, which anyone can compute from the address.

So if a dummy hashed under a spendable note's domain, a sender holding that note's key could derive the note's exact nullifier from a dummy slot, given the note's position and nullifier blinding factor. Someone who learned your nullifier key, and who knows one of your notes' position and blinding factor (its sender does), could register an identity under your key and publish that nullifier in a zero-value transaction of their own. Your note could then never be spent, nor leave by the escape hatch, which checks the same nullifier. The same holds for a program-owned note and anyone who registered its owner commitment as their key.

Each kind therefore has its own domain: 68 identity-owned, 69 program-owned, 70 fee and 71 dummy. A slot is exactly one of dummy, identity-owned or program-owned, and the circuit derives the domain from that kind, so a dummy's nullifier equals a spendable note's only if Poseidon collides across domains. A real slot still cannot reach someone else's nullifier: it must prove a leaf its own identity owns, and the chain writes each leaf's position into it. The Lean library proves the domains apart under its collision-resistance model (`TransactionCircuit.NullifierDomain`).

## Properties

- **One per note.** The inputs are fixed once the note is in the tree, so its nullifier is fixed too.
- **Only the owner can compute it.** For an identity-owned note, deriving the nullifier needs the owner's nullifier key. The note's creator knows every field of the note and still cannot tell when the recipient spends it.
- **Unlinkable to the commitment.** The position and blinding factor a nullifier is derived from are never published in a value transaction, and without the nullifier key nobody can match a nullifier to a leaf. An [escape hatch](https://docs.umbraprivacy.com/docs/operations/escape-hatch) is the exception: it publishes the spent note's position.
- **Canonical.** A nullifier is a field element below the BN254 modulus. The program refuses a transaction whose nullifiers are not, and the staging buffer refuses them again, because a value and the same value plus the modulus would otherwise count as two different keys for one note.

A nullifier does reveal when a spend happened and which pool index it was in, since each pool index keeps its own nullifier set. A note can only be spent against the trees of the pool index its leaf is in, so one set per pool index is enough.

## Every slot publishes one

A value transaction always publishes three nullifiers, one per input slot, even when some slots hold dummies. For a dummy input the circuit still derives a nullifier, using the sender's nullifier key, the position the prover supplies and a fresh random nullifier blinding factor. It skips the tree membership check for a dummy, but not the nullifier. The chain stages all three nullifiers the same way, and nobody can tell which were dummies.

The circuit also requires the three nullifiers to be pairwise different, dummies included. That rules out spending one note twice in the same transaction, which the staging buffer would not catch until the second copy. The Lean model of the transaction circuit proves that a transaction passing this check names each nullifier exactly once.

Because dummies publish real nullifiers, every value transaction takes three places in the nullifier staging buffer, whatever it spends. A dummy nullifier that collided with an earlier one would be refused like a double spend, so the SDK draws a fresh random blinding factor for every padded slot and refuses to use zero.

## Program-owned notes are linkable

The key of a program-owned note is its owner commitment, which anyone can compute from the owning address. Whoever also knows the note's nullifier blinding factor and position can compute its nullifier, and that includes whoever created the note. The creator of a program-owned note can therefore recognise the moment it is spent.

The spend gives more away than that. A transaction that spends program-owned notes needs the owning address to sign it, and it makes that address and a mask of which input slots are program-owned part of the proof's public input. Anyone watching the chain sees which address spent notes and how many. Use identity-owned notes wherever the spender does not need to be a Solana address.

## Where nullifiers go

The three nullifiers are slots 5 to 7 of the transaction proof's public input, and they appear in the transaction payload. When the transaction is accepted, the program stages them in the spend pool index's nullifier staging buffer, which the code calls the turnstile, and the forester later inserts them into the nullifier tree. How those two structures together stop a second spend is on the [double-spend](https://docs.umbraprivacy.com/docs/state/double-spend) page. The error a transaction gets for a non-canonical nullifier is in the reference section below.

## Reference

**Errors**

| Code | Error                      | Meaning                                                                                     |
| ---- | -------------------------- | ------------------------------------------------------------------------------------------- |
| `65` | `NonCanonicalFieldElement` | A payload value that must be a BN254 scalar-field element is at or above the field modulus. |

Read at build time from `libs/typescript/errors/error-manifest.json`.

## Related

- [Preventing double spends](https://docs.umbraprivacy.com/docs/state/double-spend): The freshness proof, the staging buffer and the watermark.
- [Notes](https://docs.umbraprivacy.com/docs/state/notes): The fields a nullifier is derived from.
- [Your keys](https://docs.umbraprivacy.com/docs/account/keys): The nullifier key and where it comes from.
- [The forester](https://docs.umbraprivacy.com/docs/state/forester): What moves staged nullifiers into the tree.


Last updated 6 October 2026
