Protocol
Notes and state

Note commitments

How a note's thirteen fields become one commitment, why the chain wraps that commitment with a slot and a position before storing it, and what the chain checks about either.

A note commitment is one Poseidon hash of all thirteen of a note's fields. It is the only form in which a note's contents reach the chain, and the chain never stores even that directly: it wraps the commitment with the slot and position at which it appended it, and keeps the result as a leaf of the note tree.

  1. Note fields
    13 values, known to sender and recipient
  2. Note commitment
    H_off[4] of all 13, computed by the sender's proof
  3. Tree leaf
    H_on(36, commitment, slot, position), computed by the chain
  4. Note tree root
    published on chain
From a note to the tree. The first step happens on your device, the second on chain.

The commitment

The commitment hashes the fields in a fixed order: version, kind, zone, amount, valid after, valid before, blinding factor, nullifier blinding factor, owner commitment, mint identifier, funding user commitment, funding program commitment and memo hash. It uses the circuit form of Poseidon, H_off[4], with the domain 4 and the input count carried in the sponge's capacity. Thirteen inputs is more than the Solana Poseidon syscall accepts, and the chain never needs to compute it, because it never sees the fields. See hash functions.

The same template that hashes the fields also range-checks the six numeric ones: 16 bits for version and kind, 20 for zone, 64 for amount and 48 for each end of the validity window. Every spent note and every created note goes through it, so no note in the tree can carry an amount of 2⁶⁴ or more.

The commitment has the two properties a commitment needs. It is hiding, because the blinding factor is a random field element that nobody outside the transaction can guess, so seeing the commitment tells an observer nothing about the amount, token or owner. It is binding, because opening it to a different set of fields would mean finding a Poseidon collision. The general case is on the commitments page.

What the chain sees

A value transaction publishes three output commitments, one per output slot, dummies included. They are three of the 53 values folded into the transaction proof's public input, and the proof shows that each one is the commitment of a note satisfying every rule of the circuit.

The program itself checks only that each output commitment is a canonical field element, below the BN254 modulus; otherwise it refuses the transaction with error 65, listed in the reference section below. It cannot check more, because it never learns the fields.

The leaf

When the transaction succeeds, the program appends the three commitments to the note tree. For each one it computes the leaf

leaf = H_on(36, commitment, insertion slot, insertion position)

with the syscall form of Poseidon, where the insertion slot is the slot the append happens in and the insertion position is the leaf's global position: the pool index times 2³² plus its index in that tree. The note tree page explains positions and pool indexes.

Wrapping the commitment gives the chain, not the prover, the last word on where a note sits and when it arrived. A spend has to rebuild this exact leaf inside its proof, from the note's fields and the slot and position the chain chose, and then show the leaf is in the tree. It cannot use a different position, because a different position is a different leaf, and that leaf is not in the tree.

Both stamped values are used again later. The position is an input to the note's nullifier, so each leaf has exactly one nullifier, even if two leaves happened to wrap the same commitment. The slot is one of the per-field values the disclosure tree records for each spent note.

What the recipient must know

To spend a note later, its owner has to reproduce the commitment and then the leaf, so they need every field, the slot and the position. The encrypted note carries twelve of the fields; the owner commitment is left out, because the recipient knows it already. The slot and position come from the program's log of the append. Both blinding factors must be non-zero. The transaction circuit refuses an output, real or dummy, with either one at zero, and the escape-hatch circuit refuses to spend such a note, so the rule holds for every note the pool creates; the note-plaintext encoding enforces it off chain as well, since a zero blinding factor would publish a value every observer could predict.

Reference

Last updated

On this page