# Commitments & nullifiers

> Commitments and nullifiers are the two cryptographic objects that let the chain track who-spent-what without revealing identities or values. A commitment announces that a [note](../concepts/notes) exists; a nullifier announces that a note was spent. This page explains how each is computed and why the pair is unlinkable.

Commitments and nullifiers are the two cryptographic objects that let the chain track who-spent-what without revealing identities or values. A commitment announces that a [note](../concepts/notes) exists; a nullifier announces that a note was spent. This page explains how each is computed and why the pair is unlinkable.

## Commitments: note identifiers

A commitment is the public identifier of a note, computed as a chain of Poseidon hashes:

```ts
noteAddressHash = Poseidon(ownerAddress, noteSecret)
precommitment   = Poseidon(noteAddressHash, tokenId, value, metadata)  // metadata = 0 in v2.0
commitment      = Poseidon(precommitment, label)
```

Each input to this chain is known only to specific parties, so the chain can store the commitment publicly without exposing the value, the owner, or the noteSecret behind it. Anyone who does hold the inputs can recompute the chain and prove that a given note matches. This is exactly the computation an auditor performs when verifying a [payment receipt](../operations/generate-receipt).

## Nullifiers: spend markers

A nullifier is the public marker of a spent note. It is computed once per note, from the spender's private [nullifying key](../concepts/keys) and the note's commitment:

```ts
nullifier = Poseidon(privateNullifyingKey, commitment)
```

The chain stores spent nullifiers in [`PoolVault`](/protocol/contracts/pool-vault)`.spentNullifiers`, and spending a note publishes its nullifier there. Because the same note always produces the same nullifier, an attempt to spend it a second time collides with the stored entry and the transaction reverts.

## Why both?

-   **Commitment** says "this note exists." It is public and lives permanently in the [state tree](../concepts/state-tree).
-   **Nullifier** says "this note was spent." It is also public and lives permanently in the spent-nullifier set.
-   The chain sees both, so it can verify that a spend consumed a valid input and published a fresh nullifier, but it cannot link the nullifier back to the commitment that was spent. Only the spender's `privateNullifyingKey` closes that link.

## The unlinkability property

All notes belonging to the same wallet share one `privateNullifyingKey`. But because each nullifier hashes its own note's commitment, spending two different notes produces two unrelated-looking nullifiers. An observer of the chain cannot tell that the two spends came from the same wallet.

## Verify a commitment is on-chain

```ts
const timestamp = await poolVault.read.commitments([BigInt(commitment)]);
if (timestamp === 0n) throw new Error("commitment not in state tree");
```

## Verify a nullifier is spent

```ts
const spent = await poolVault.read.spentNullifiers([BigInt(nullifier)]);
if (spent !== 0n) throw new Error("note already spent");
```

See [verify spent status](../operations/verify-spent) for the operational pattern.
