Note Encryption
1. Why this chapter exists
Every Sapling Output carries two ciphertexts on chain: an enc_ciphertext (encrypted note plaintext, decryptable by the receiver's ivk) and an out_ciphertext (encrypted symmetric keying material, decryptable by the sender's ovk). Together they mean the receiver learns of incoming payments without any out-of-band communication, and the sender can re-scan their own history after a key rotation.
The cryptography is in this crate via an implementation of the
zcash_note_encryption::Domain trait.
This chapter pins down the KDF, the OCK derivation, the ZIP-212 enforcement
modes, and the trial-decryption entry points.
2. Definitions
Definition 10.1 (Sapling KA). A Diffie-Hellman key agreement on Jubjub. Given an ephemeral secret and a public point derived from the recipient's ivk and diversifier:
The cofactor 8 ensures the agreed point lives in . Code:
spec::ka_sapling_agree_prepared
(the .clear_cofactor() call).
Definition 10.2 (Sapling KDF). Given the agreed shared secret and the 32-byte ephemeral public key encoding,
The output is a 32-byte ChaCha20-Poly1305 key. Code:
SharedSecret::kdf_sapling.
Definition 10.3 (PRF^ock). A second BLAKE2b-32 instance, used to derive the AEAD key for the outgoing ciphertext that lets the sender decrypt their own outputs:
Code:
note_encryption::prf_ock.
Definition 10.4 (ZIP-212 enforcement). Three modes that govern which note-plaintext versions are accepted at decryption time:
loading...
Off: accept both v1 (pre-ZIP-212,rcmfield) and v2 (ZIP-212,rseedfield). Used for pre-Canopy chains.GracePeriod: accept both versions; used during the transition blocks at Canopy activation.On: accept only v2 plaintexts. Used on every mainnet block after the grace window.
Invariant 10.5 (esk binding for ZIP-212). Under ZIP-212, the ephemeral
secret is not sampled fresh; it is derived from the note's
via . This means the
in an Output description is a deterministic function of the
encrypted note. A decrypting receiver can verify this by recomputing
from the decrypted note; if it does not match the on-chain
, the output is rejected. Code:
Note::derive_esk.
3. The code
3.1 The Domain implementation
The crate's heavy lifting is to satisfy the zcash_note_encryption::Domain
trait, which abstracts over Sapling and Orchard's note encryption schemes. The
trait's associated types and methods get filled in here:
loading...
The interesting methods downstream of that header:
derive_esk: pulls the ZIP-212 esk from the note (Nonefor v1).ka_derive_public: computes .ka_agree_enc/ka_agree_dec: the two directions of the DH.kdf: the BLAKE2b-32 withKDF_SAPLING_PERSONALIZATION.
3.2 ZIP-212 grace period handling
The bulk of the rules live in the plaintext parser. Both versions of the
plaintext format share the first byte (0x01 or 0x02), the next 11 bytes
(diversifier), the next 8 bytes (value, little endian), and 32 bytes of
randomness. After ZIP-212, that last 32 bytes is rseed (used to
derive both rcm and esk); before, it is rcm directly:
loading...
The version-validity gating at line 82
(plaintext_version_is_valid) is what enforces
Zip212Enforcement::On: it returns false for v1
plaintexts when only v2 is accepted.
3.3 Trial decryption
The receiver scans every output with their ivk; the per-output trial-decrypt is
one call. The crate re-exports the zcash_note_encryption trial-decrypt
functions:
try_sapling_note_decryption(SaplingDomain, ivk, ShieldedOutput)try_sapling_compact_note_decryption(SaplingDomain, ivk, ShieldedOutput)(skips the memo, used in light client sync)try_sapling_output_recovery_with_ock/try_sapling_output_recovery(the sender's path, using ovk)
3.4 Memo bytes
Sapling memos are exactly 512 bytes ([u8; 512]). The crate does not impose
structure beyond that; encoding (UTF-8 vs binary "f6"-padding) is the caller's
responsibility, and changed signature in 0.5: see the
CHANGELOG entry for 0.5.0.
4. Why decryption gives a spendable note
Section 2 defined the key agreement and the KDF; section 3 showed where they live in the code. What is missing is the concrete answer to the recipient's question: what exactly did the sender publish on chain, and which of those bytes do I need to spend the note later? This section walks through the on-chain payload byte by byte, identifies the small slice the receiver actually consumes, and connects that slice to the Spend witness in chapter 11 (Definition 11.1).
4.1 What the sender publishes on chain
For every output the sender writes one OutputDescription to the block:
loading...
Six fields, with sizes fixed by the protocol (v4 transactions):
| Field | Size | What it is |
|---|---|---|
cv | 32 bytes | Pedersen value commitment to v with trapdoor rcv |
cmu | 32 bytes | u-coordinate of the note commitment cm; appended to the tree |
ephemeral_key | 32 bytes | epk = [esk] g_d, the Diffie-Hellman ephemeral public key |
enc_ciphertext | 580 bytes | AEAD-wrapped 564-byte note plaintext, encrypted under the DH key |
out_ciphertext | 80 bytes | AEAD-wrapped 64-byte sender-recovery plaintext, encrypted under ock (derived from the sender's ovk) |
zkproof | 192 bytes | Groth16 proof of the Output relation (Definition 11.2) |
The 580 bytes of enc_ciphertext are an AEAD wrapper (564-byte plaintext plus a
16-byte Poly1305 tag) around one note plaintext. The plaintext layout is the
same one the parser uses in section 3.2:
| Plaintext bytes | Field | Notes |
|---|---|---|
0 | version | 0x01 (pre-Canopy) or 0x02 (ZIP 212) |
1..12 | d | 11-byte diversifier |
12..20 | v | 8-byte little-endian value |
20..52 | rseed | 32 bytes (v2) or 32-byte rcm (v1) |
52..564 | memo | 512-byte free-form memo |
The 80 bytes of out_ciphertext wrap a 64-byte plaintext (esk || pk_d),
encrypted under a key derived from the sender's own ovk. This blob is for the
sender to re-decrypt their own outgoing history (see section 3.3); the
receiver never reads it.
4.2 What the receiver actually needs
Of the six fields above, the receiver needs exactly two to recover the note:
ephemeral_key(=epk), to compute the DH shared secret and unlock the AEAD;enc_ciphertext, the bytes to decrypt.
Trial decryption (try_sapling_note_decryption(ivk, output)) feeds those two
fields plus the receiver's ivk into the KDF + ChaCha20-Poly1305 chain defined
in section 2. On success it returns the 564-byte plaintext, of which the first
52 bytes are all the spend-time circuit will ever see:
(version, d, v, rseed). The 512-byte memo is human-facing and never touches
the circuit.
The receiver also reads two more on-chain fields, not to decrypt them, but to cross-check the decryption:
cmu: after derivingrcmfromrseedandpk_d = [ivk] g_d, the receiver recomputescm = NoteCommit(g_d, pk_d, v, rcm)and checks its u-coordinate against the on-chaincmu. The samecmudoubles as the note's address inside the note commitment tree, so finding it there gives the receiver the Merkle position and the authentication path.epk(again): the receiver re-deriveseskfromrseed, computesepk' = [esk] g_d, and rejects the output ifepk' != epk(Invariant 10.5). This is the binding check that prevents a malicious sender from attaching two valid plaintexts to oneepk.
cv, out_ciphertext, and zkproof are never used by the receiver. They serve
the validator (cv and zkproof) or the sender (out_ciphertext).
So at the byte level the sender's job for the receiver is to publish the 52
bytes of compact note plaintext (wrapped as the AEAD payload of
enc_ciphertext) and the 32-byte epk needed to unwrap them. Everything else
the receiver needs they either compute from those bytes or already hold
themselves.
4.3 Reconstructing the Spend witness
Chapter 11, Definition 11.1, lists the Spend circuit's private witness as
Each component comes from exactly one place at spend time:
| Witness component | Source |
|---|---|
g_d | GroupHash(d), with d from the decrypted compact note |
v | bytes 12..20 of the decrypted compact note |
rcm | , rseed from bytes 20..52 |
auth_path | receiver's local note commitment tree, indexed by the on-chain cmu |
ak | receiver's key tree (derived from ask) |
nsk | receiver's key tree |
rcv, ar | freshly sampled per spend |
The first four rows are exactly the data the sender delivered through
enc_ciphertext + the on-chain cmu. The next two come from the receiver's own
spending key. The last two are local randomness. Nothing more is needed, and in
particular the receiver never has to talk to the sender again.
The same logic explains why the receiver also recomputes pk_d from their own
ivk rather than reading it from the ciphertext: the Spend circuit re-derives
pk_d = [ivk] g_d as constraint 6 of Definition 11.1, and any inconsistency
between the decrypted note and the receiver's ivk shows up as a witness that
fails to satisfy constraint 8 (the note commitment).
4.4 Why the Output circuit can skip recipient authentication
Definition 11.2 has the Output circuit witnessing pk_d as an unchecked field
element: no on-curve check, no subgroup check, no proof that pk_d
corresponds to anyone's ivk. Chapter 11 notes the reason in passing; this is
the place to make it explicit.
At Output time the sender alone cannot authenticate the recipient even if the
protocol asked: the sender only has the public address (d, pk_d) and no
signature from the receiver. So the Output circuit constrains only what the
sender can prove unilaterally:
cvis a valid value commitment opening to the witnessedv;epk = [esk] g_dfor the witnessedeskandg_d;g_dis on-curve and not small order;cmuis theu-coordinate ofNoteCommit(v, g_d, pk_d, rcm).
If pk_d is garbage (not on the curve, in the small-order subgroup, or not
anyone's diversified key), the chain still accepts the OutputDescription. The
cost is borne entirely by the named recipient: they cannot spend the note
because no ivk they hold would let them derive a matching pk_d under
constraint 6 of Definition 11.1. The malformed output becomes unspendable
change, and the sender has burned their funds.
Authentication of the recipient is therefore deferred to spend time:
inside the Spend circuit binds the note to the holder of ivk, and only that
holder could have decrypted the ciphertext in the first place. The two checks
(off-chain decryption with ivk, in-circuit pk_d equality) lock the same
identity at two different moments, with no need for the sender to participate in
either.
This is also the structural reason the Output circuit is much smaller than the
Spend circuit (section 3 of chapter 11): no Merkle ascent, no nullifier
derivation, no ivk re-derivation. All of those move to spend time, where the
actor with the secrets is also the actor doing the proving.
5. Failure modes
- ZIP-212 enforcement misconfigured. A node that runs with
Zip212Enforcement::Offon a chain past the grace period will accept pre-Canopy notes that should have been rejected; a node that runs withOnbefore the grace period activation will reject legitimate v1 notes. The crate does not pick the mode itself; it is the caller's responsibility (typically based on block height). Caught by: nothing automatic in this crate; the configuration surface is in the caller (e.g.zebrad). epkdecoded as a small-order point. The deserializer for ephemeral keys rejects small-order points (source). A bypass would let an attacker bind a note to multiple ivk-decryptable plaintexts. Caught by: theSaplingVerificationContextInner::check_outputconsensus check.- Memo bytes left uninitialized. Memos are 512-byte buffers and the
encrypted form leaks the full 512 bytes (length is not hidden). A caller
passing uninitialised memory leaks process state. The type signature
(
[u8; 512], not&[u8]) makes this hard to do accidentally; constructing the array requires explicitly setting each byte. Caught by: the type system, mostly. - Reusing
eskacross two outputs. Pre-ZIP-212,eskwas sampled freely; nothing prevented reuse. Reuse breaks DH security (the shared secrets across the two outputs are related). ZIP-212 fixes this by derivingeskdeterministically fromrseed, so reuse requires reusingrseed, which itself is a per-note fresh sample. Caught by: see Notes and nullifiers.
6. Spec pointers
- Zcash Protocol Specification, §5.4.4 (In-band secret distribution) is the authority on the protocol-level encryption.
- Zcash Protocol Specification, §5.4.5.3 (Sapling key agreement) defines the KA primitive (Definition 10.1 above).
- ZIP 212 is the source of Definition 10.5 and the grace-period semantics.
zcash_note_encryptioncrate documentation is the trait this module implements. Read itsDomaintrait before extending the Sapling impl.
7. Exercises
- Decrypt a known test vector. Open
src/test_vectors/note_encryption.rsand pick one. Construct theSaplingDomainwith the right enforcement mode and calltry_sapling_note_decryption. Verify the returnedNotematches the vector. The existingnote_encryption.rstests exercise this; pick one and run it in isolation. - Show the OVK separation. Construct two outputs to the same address with
the same memo but different ovks. Confirm that the two
out_ciphertextblobs differ (becauseockderives fromovk), while theenc_ciphertextblobs also differ (because they have different fresh trapdoors). - Add a
Displayimpl forZip212Enforcement. Useful when debugging an ambiguous chain configuration. Lowercase"off","grace_period","on". Add a unit test that round-trips throughformat!.
Answers in the code. For exercise 1, the test vectors are in the
test_vectors::note_encryption::make_test_vectors helper.
For exercise 2, the
prf_ock
function takes ovk as the first byte input, so two distinct ovks give two
distinct ocks with overwhelming probability.