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 field element.
By the end you should be able to walk through the path note plaintext
NoteCommitment (a Jubjub point)
ExtractedNoteCommitment (a single field element,
called cmu) Merkle leaf, and identify which file holds each
transformation.
2. Definitions
Definition 5.1 (note plaintext). A Sapling note is the tuple
where is the recipient's diversifier, is the recipient's diversified transmission key, is the non-negative note value, and 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 , the note commitment is
where is derived from via either the identity map (pre-ZIP-212) or (post-ZIP-212), , and is the 32-byte encoding of a Jubjub point. The output lives in . The bit-length of the input is bits.
Definition 5.3 (extracted note commitment, cmu). Let . Convert to affine coordinates and take its u-coordinate:
This is what the protocol writes into the Merkle tree.
Lemma 5.4 (cmu is injective on the prime-order subgroup). Because and the prime-order subgroup is the preimage of an injective function under the u-coordinate map (Hopwood, Spec §5.4.9.4), is injective on . 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 .
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:
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
where is a fixed, independent generator
(NOTE_COMMITMENT_RANDOMNESS_GENERATOR).
The rcm.0 scalar passed on
line 56
is the multiplier on . That term is what turns a
bare Pedersen hash into a commitment:
- Hiding comes from
rcm. Becausercmis sampled uniformly and is independent of the Pedersen generators, is a uniformly random group element that masks the hash of the note contents. Withoutrcmthe commitment reveals nothing about , , or . Holdingrcmis what lets you reopen the commitment, hence "trapdoor". - Binding comes from discrete-log hardness between the Pedersen generators and : finding a second 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:
loading...
The derivation lives one module up, in note.rs:
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:
loading...
The From<NoteCommitment> for ExtractedNoteCommitment impl wires this in:
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:
loading...
Three observations:
- The note exposes
cmu()for "give me the Merkle leaf" andcm_full_point()(private) for "give me the Jubjub point that feeds into the nullifier". Note::eqis defined as equality ofcmu(source line 56-60). This is canonical: two notes with the same commitment are by definition the same note (even if theirrseeddiffers as long asrcmmatches).- The
Nullifierderivation also consumescm_full_point(); see Notes, commitments, nullifiers.
4. Failure modes
- Using
cm_full_pointwhere the protocol expectscmu. The Merkle tree storescmu, 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_pointis not public (it is private tonote.rs); onlyExtractedNoteCommitmentescapes. - A non-canonical
cmuencoding. A byte buffer that decodes to a value above the field modulus is silently rejected byExtractedNoteCommitment::from_bytes. If a wire-format parser forgets to call this and instead doesbls12_381::Scalar::from_bytes, the canonicality consensus rule is bypassed. Caught by: the type system;ExtractedNoteCommitment::from_bytesis the only public constructor from bytes. - Forgetting the windowed-Pedersen randomization step. A bare Pedersen hash
is not a hiding commitment; the randomization by
rcmmakes it one. If a refactor stripped the[r] *NOTE_COMMITMENT_RANDOMNESS_GENERATORterm, two notes with identical 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
- Zcash Protocol Specification, §5.4.8.2 (Windowed Pedersen commitments)
defines
WindowedPedersenCommit, exactly the functionspec::windowed_pedersen_commitimplements. - Zcash Protocol Specification, §5.4.9.4 (Coordinate extractor for Jubjub)
defines
Extract_J, exactly the functionspec::extract_pimplements. The injectivity claim lives here. - ZIP 212 (Allow Recipient to Derive Ephemeral Secret Key from Note Plaintext) explains the Rseed::AfterZip212 path and why it exists (a fix for decryption-time DoS).
6. Exercises
- Derive cmu for the zero note. Take a note with
value = 0,g_dandpk_dboth serialized as 32 zero bytes (not a valid Sapling note but a clean toy input), andrcm = jubjub::Fr::ZERO. By hand, computeWindowedPedersenCommit_0(64 zero bits || 256 zero bits || 256 zero bits). This equalspedersen_hash(NoteCommitment, 576 zero bits). Then applyextract_p. Compare against a Rust program that does the same. - Add a check that a
Noteround-trips through its commitment. Construct aNoteviaNote::from_partswith random fields, computenote.cmu(), then checkExtractedNoteCommitment::from_bytes(¬e.cmu().to_bytes()) == note.cmu(). Add this as a proptest undersrc/note.rsusing thearb_notegenerator. Runcargo test --all-features. - Read the canonicality check. Open
src/note/commitment.rsline 65-79. In one sentence, state the consensus rule the doc comment references and the function that enforces it. Then readsrc/value.rs::ValueCommitment::from_bytes_not_small_orderand explain the difference between "canonical encoding" (forcmu) and "not small order" (forcv).
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).