# Commitments and nullifiers

The note encodes two random values (`nullifier` and `secret`) and from these the protocol derives two on-chain artifacts: a **commitment** and a **nullifier hash**. Understanding what each is for is the key to understanding the protocol.

## The two values in the note

When the CLI generates a note, it samples:

* `nullifier`: 31 random bytes (248 bits, comfortably less than the 254-bit BLS12-381 scalar field).
* `secret`: 31 random bytes.

These two values together are your deposit secret. They never leave your machine. The CLI serializes them into the hex tail of the note string.

Why 31 bytes? Field elements modulo the BLS12-381 scalar field need to be less than the field's prime, roughly 254 bits. Using 248 bits guarantees no value ever overflows. Field-range checks at the pool catch any non-canonical encoding.

## The commitment

```
commitment = Poseidon(nullifier, secret)
```

This is what goes on-chain at deposit time. It is:

* **One-way.** Computing the commitment from `(nullifier, secret)` is fast. Recovering `(nullifier, secret)` from the commitment requires brute-forcing the entire input space (effectively impossible: `2^496` candidates).
* **Binding.** Once you commit to a particular `(nullifier, secret)`, you can't later claim the commitment was for a different pair. Poseidon's collision resistance ensures this.
* **Hiding.** The commitment reveals nothing about either input. An observer sees a 256-bit hash and that's it.

When you deposit, the pool's Merkle tree gets a new leaf containing this commitment. Anyone can see the leaf; every leaf is public. But there are now thousands of leaves, and no observer can tell which one corresponds to which depositor.

## The nullifier hash

```
nullifierHash = Poseidon(nullifier)
```

The nullifier hash is what gets recorded on-chain at withdraw time. It serves one purpose: **preventing double-spends**.

The pool keeps a dictionary of nullifier hashes. On every withdrawal, the pool:

1. Confirms the proof's `nullifierHash` public input is **not** already in the dict.
2. Adds it to the dict.

A second withdrawal attempt with the same note would produce the same `nullifierHash`, and the pool would reject it with `ERR_ALREADY_SPENT`.

## Why two values and not one

You might ask: why not just hash the whole note and use that as both the commitment and the nullifier? Because then **anyone watching the chain could link your deposit to your withdrawal trivially**: the same hash would appear in both places.

By separating the two:

* The commitment is `Poseidon(nullifier, secret)`. Public at deposit time.
* The nullifier hash is `Poseidon(nullifier)`. Public at withdraw time.

These are two **completely different hashes** that share no observable relationship to outsiders. Only the holder of `(nullifier, secret)` can demonstrate, via a zero-knowledge proof, that they correspond to the same note, and the proof itself reveals nothing.

This is the same trick Zcash uses, and the same trick Tornado Cash used on Ethereum. It's the heart of the privacy guarantee.

## What the withdraw circuit enforces

The circuit takes both `nullifier` and `secret` as *private* inputs, and checks two equations:

```
Poseidon(nullifier, secret) == leaf at some position in the tree
Poseidon(nullifier)         == nullifierHash (the public input)
```

If you know `(nullifier, secret)`, both equations hold trivially. If you don't know them, and instead are trying to forge a withdrawal, you have to find values that satisfy *both* equations simultaneously, against a target `commitment` someone else owns and a target `nullifierHash` the chain has never seen. That requires breaking Poseidon's preimage resistance, which is the assumption we're trusting.

For the exact circuit signal layout, see [Circuit spec](/protocol-reference/circuit-spec.md).

## What can go wrong

A few subtle failure modes the contract guards against:

* **Field-element non-uniqueness.** If the pool stored the `nullifierHash` as an arbitrary 256-bit integer rather than a canonical field element, an attacker could submit `nh` and `nh + p` (where `p` is the field's prime), both reducing to the same Poseidon output but distinct as dict keys. The pool's nullifier dict would treat them as different entries, allowing double-spend. **Fixed** by enforcing `0 <= nh < p` on every public input. See [security review C-1](/security/security-review.md#c-1).
* **Collisions in Poseidon.** A collision would let an attacker construct a fake note with a commitment matching someone else's deposit. Poseidon over BLS12-381 has the same algebraic security analysis as Poseidon over BN254; see [Poseidon vs. MiMC](/how-it-works/poseidon-vs-mimc.md).
* **Bad randomness in note generation.** If `nullifier` or `secret` come from a weak RNG, the attacker can reconstruct them. The CLI uses Node's `crypto.randomBytes`, which is cryptographically secure.

## Why you can never withdraw the same note twice

A second `Poseidon(nullifier)` is a deterministic function of `nullifier`. Same input, same output. The pool's nullifier dict makes the rejection straightforward:

```
if (nullifierHash in nullifiers) throw ERR_ALREADY_SPENT
```

There is no way to compute a different `nullifierHash` for the same `nullifier`. Poseidon is a function, not a randomized commitment. The only ways to produce a different nullifier hash are:

1. Generate a fresh `(nullifier, secret)`, which is a new deposit, not the same one.
2. Break Poseidon, which is the assumption we're trusting.

This single dict is what makes the protocol airtight against replay.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.tonadocash.com/how-it-works/commitments-and-nullifiers.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
