Skip to main content

Note Commitments

1. Why this chapter exists

A note commitment is what the Sapling protocol writes into the Merkle tree to mark "this note exists and has been received". The commitment hides the note's contents (value, recipient, randomness) from anyone who does not know them. The commitment scheme is built out of the Pedersen hash from the previous chapter and one extra component: the coordinate extractor that maps a Jubjub point to a single Fq\mathbb{F}_q field element.

By the end you should be able to walk through the path note plaintext \to NoteCommitment (a Jubjub point) \to ExtractedNoteCommitment (a single field element, called cmu) \to Merkle leaf, and identify which file holds each transformation.

2. Definitions

Definition 5.1 (note plaintext). A Sapling note is the tuple

n=(d,pkd,v,rseed),n = (\mathsf{d}, \mathsf{pk_d}, v, \mathsf{rseed}),

where d{0,1}88\mathsf{d} \in \{0,1\}^{88} is the recipient's diversifier, pkdJ(r)\mathsf{pk_d} \in \mathbb{J}^{(r)} is the recipient's diversified transmission key, v[0,264)v \in [0, 2^{64}) is the non-negative note value, and rseed\mathsf{rseed} is the note seed randomness (either a pre-ZIP-212 scalar or a ZIP-212 32-byte seed; see Notes, commitments, nullifiers).

Definition 5.2 (note commitment). Given a note nn, the note commitment is

NoteCommit(n)=WindowedPedersenCommitrcm(vreprJ(gd)reprJ(pkd)),\mathsf{NoteCommit}(n) = \mathsf{WindowedPedersenCommit}_{\mathsf{rcm}}( v \mathbin{\|} \mathsf{repr}_{\mathbb{J}}(\mathsf{g_d}) \mathbin{\|} \mathsf{repr}_{\mathbb{J}}(\mathsf{pk_d}) ),

where rcm\mathsf{rcm} is derived from rseed\mathsf{rseed} via either the identity map (pre-ZIP-212) or PRFExpand,rcm(rseed)\mathsf{PRF^{Expand,rcm}}(\mathsf{rseed}) (post-ZIP-212), gd=DiversifyHash(d)\mathsf{g_d} = \mathsf{DiversifyHash}(\mathsf{d}), and reprJ\mathsf{repr}_{\mathbb{J}} is the 32-byte encoding of a Jubjub point. The output lives in J(r)\mathbb{J}^{(r)}. The bit-length of the input is 64+256+256=57664 + 256 + 256 = 576 bits.

Definition 5.3 (extracted note commitment, cmu). Let c=NoteCommit(n)J(r)c = \mathsf{NoteCommit}(n) \in \mathbb{J}^{(r)}. Convert cc to affine coordinates (u,v)Fq×Fq(u, v) \in \mathbb{F}_q \times \mathbb{F}_q and take its u-coordinate:

cmu(n)=u(c)Fq.\mathsf{cmu}(n) = u(c) \in \mathbb{F}_q.

This is what the protocol writes into the Merkle tree.

Lemma 5.4 (cmu is injective on the prime-order subgroup). Because cJ(r)c \in \mathbb{J}^{(r)} and the prime-order subgroup is the preimage of an injective function under the u-coordinate map (Hopwood, Spec §5.4.9.4), cmu\mathsf{cmu} is injective on J(r)\mathbb{J}^{(r)}. Without this fact, two distinct notes could share the same Merkle leaf and the nullifier check would not guarantee uniqueness. Citation: Zcash Protocol Specification §5.4.9.4.

Invariant 5.5 (cmu serialization is canonical). When cmu is read from the wire (e.g. when verifying a block), the byte representation must be the canonical LEBS2OSP256(reprFq())\mathsf{LEBS2OSP}_{256}(\mathsf{repr}_{\mathbb{F}_q}(\cdot)). Non-canonical encodings (a field element written as a value above the field modulus) are rejected. Enforced by ExtractedNoteCommitment::from_bytes via the bls12_381::Scalar::from_repr check.

3. The code

3.1 The internals

The full implementation of NoteCommitment::derive and ExtractedNoteCommitment is one short file:

src/note/commitment.rs
loading...

Read the bit-ordering carefully:

  • Line 53: v.to_le_bits() is little-endian, 64 bits.
  • Line 54: BitArray::<_, Lsb0>::new(g_d).iter().by_vals(), also least-significant-bit-first, 256 bits.
  • Line 55: ditto for pk_d, 256 bits.

If you ever swap LSB-first for MSB-first by mistake, every test vector in the crate fails immediately.

3.2 The trapdoor: rcm

In NoteCommitTrapdoor the "trapdoor" is the commitment randomness rcm, a single Jubjub scalar (jubjub::Fr). It is not a trapdoor in the trapdoor-permutation sense (a secret that inverts a one-way function). The name comes from the commitment-scheme literature, where the random opening value is conventionally called the trapdoor.

To see the role it plays, expand Definition 5.2. The windowed Pedersen commitment is

NoteCommitrcm(gd,pkd,v)=PedersenHashToPoint(vgdpkd)+[rcm]R,\mathsf{NoteCommit}_{\mathsf{rcm}}(\mathsf{g_d}, \mathsf{pk_d}, v) = \mathsf{PedersenHashToPoint}(v \mathbin{\|} \mathsf{g_d} \mathbin{\|} \mathsf{pk_d}) + [\mathsf{rcm}] \cdot R,

where RR is a fixed, independent generator (NOTE_COMMITMENT_RANDOMNESS_GENERATOR). The rcm.0 scalar passed on line 56 is the multiplier on RR. That [rcm]R[\mathsf{rcm}] \cdot R term is what turns a bare Pedersen hash into a commitment:

  • Hiding comes from rcm. Because rcm is sampled uniformly and RR is independent of the Pedersen generators, [rcm]R[\mathsf{rcm}] \cdot R is a uniformly random group element that masks the hash of the note contents. Without rcm the commitment reveals nothing about vv, gd\mathsf{g_d}, or pkd\mathsf{pk_d}. Holding rcm is what lets you reopen the commitment, hence "trapdoor".
  • Binding comes from discrete-log hardness between the Pedersen generators and RR: finding a second (msg,rcm)(\mathsf{msg}', \mathsf{rcm}') that maps to the same point implies a non-trivial DL relation.

Pedersen commitments are perfectly (information-theoretically) hiding and computationally binding. The trapdoor rcm is what makes hiding unconditional, while binding rests on the DL assumption. The same construction is reused for the value commitment cv, where the blinding scalar ValueCommitTrapdoor (rcv) plays the identical role; see Value commitments.

The randomness rcm is wrapped in a small newtype because the protocol distinguishes between the user-visible "rseed" (a 32-byte buffer) and the derived scalar that goes into the commitment. The newtype prevents the wrong field from being passed:

src/note/commitment.rs (NoteCommitTrapdoor)
loading...

The derivation lives one module up, in note.rs:

src/note.rs (Rseed -> rcm)
loading...

Before ZIP 212, rcm is sampled directly as a Jubjub scalar. After ZIP 212, rcm is derived from a 32-byte rseed via PrfExpand::SAPLING_RCM. The change matters because pre-ZIP-212 notes have a "small" rcm value (only ~252 bits of entropy because the scalar was sampled in the field), while post-ZIP-212 notes get a full 256-bit rseed from which both rcm and the ephemeral key are deterministically derived.

3.3 From point to field element

The coordinate extractor is in spec.rs:

src/spec.rs::extract_p
loading...

The From<NoteCommitment> for ExtractedNoteCommitment impl wires this in:

src/note/commitment.rs (extraction)
loading...

extract_p is exactly one operation on the affine form: take the u-coordinate. The comment on line 161 documents the injectivity fact from Lemma 5.4.

3.4 The Note type

The user-facing type ties it together:

src/note.rs (Note::cm_full_point, Note::cmu)
loading...

Three observations:

  1. The note exposes cmu() for "give me the Merkle leaf" and cm_full_point() (private) for "give me the Jubjub point that feeds into the nullifier".
  2. Note::eq is defined as equality of cmu (source line 56-60). This is canonical: two notes with the same commitment are by definition the same note (even if their rseed differs as long as rcm matches).
  3. The Nullifier derivation also consumes cm_full_point(); see Notes, commitments, nullifiers.

4. Failure modes

  • Using cm_full_point where the protocol expects cmu. The Merkle tree stores cmu, not the full point. Confusing them produces a tree where the verifier cannot reconstruct the leaf from the witness. Caught by: the type system. cm_full_point is not public (it is private to note.rs); only ExtractedNoteCommitment escapes.
  • A non-canonical cmu encoding. A byte buffer that decodes to a value above the field modulus is silently rejected by ExtractedNoteCommitment::from_bytes. If a wire-format parser forgets to call this and instead does bls12_381::Scalar::from_bytes, the canonicality consensus rule is bypassed. Caught by: the type system; ExtractedNoteCommitment::from_bytes is the only public constructor from bytes.
  • Forgetting the windowed-Pedersen randomization step. A bare Pedersen hash is not a hiding commitment; the randomization by rcm makes it one. If a refactor stripped the [r] * NOTE_COMMITMENT_RANDOMNESS_GENERATOR term, two notes with identical (v,d,pkd)(v, \mathsf{d}, \mathsf{pk_d}) would commit to the same value, and an observer who knows the bit-string could trivially correlate them on the chain. Caught by: nothing automatic in this workspace; the Spec §5.4.8.2 invariant is enforced by code review.

5. Spec pointers

6. Exercises

  1. Derive cmu for the zero note. Take a note with value = 0, g_d and pk_d both serialized as 32 zero bytes (not a valid Sapling note but a clean toy input), and rcm = jubjub::Fr::ZERO. By hand, compute WindowedPedersenCommit_0(64 zero bits || 256 zero bits || 256 zero bits). This equals pedersen_hash(NoteCommitment, 576 zero bits). Then apply extract_p. Compare against a Rust program that does the same.
  2. Add a check that a Note round-trips through its commitment. Construct a Note via Note::from_parts with random fields, compute note.cmu(), then check ExtractedNoteCommitment::from_bytes(&note.cmu().to_bytes()) == note.cmu(). Add this as a proptest under src/note.rs using the arb_note generator. Run cargo test --all-features.
  3. Read the canonicality check. Open src/note/commitment.rs line 65-79. In one sentence, state the consensus rule the doc comment references and the function that enforces it. Then read src/value.rs::ValueCommitment::from_bytes_not_small_order and explain the difference between "canonical encoding" (for cmu) and "not small order" (for cv).

Answers in the code. For exercise 1, the result is the u-coordinate of pedersen_hash(Personalization::NoteCommitment, [false; 576]). For exercise 2, the existing arb_cmu strategy is a near-fit but generates random field elements, not random commitments; you will want arb_note instead. For exercise 3: cmu is canonical (only the unique 32-byte encoding of each field element is accepted), while cv is not-small-order (any encoding decoding to a small-order point is rejected, regardless of canonicality).