08 - Note encryption
1. Why this chapter exists
Note encryption is what delivers a shielded note to its recipient
on-chain without revealing the recipient or the note content to
anyone else. The construction is called in-band secret distribution
because the encrypted note plaintext rides inside the transaction
itself. Every wallet that receives shielded value runs trial
decryption on every output of every block; a contributor who cannot
explain the ECDH-KDF-AEAD chain or the role of will
break scanning performance or, worse, accept malformed notes. By the
end of this chapter you will be able to follow an OutputDescription
from the wire bytes through zcash_note_encryption into the
decrypt_transaction entry point in
zcash_client_backend/src/decrypt.rs,
and identify which step rejects a tampered ciphertext.
The encryption-specific keys () and their relationships are summarised
here and recorded in
chapter 23 - The complete key catalog. The
actual code lives in the external crate zcash_note_encryption,
used by both sapling-crypto and orchard.
2. Definitions
2.1 What needs to be conveyed
For each shielded output, the sender must convey to the recipient:
- The note value .
- The diversifier (so the recipient can recover ).
- The randomness for the commitment.
- For Orchard, the additional fields .
- An optional memo of up to 512 bytes.
Plus the sender wants to recover the same data later from the chain, using a key derived from their own , without keeping per-output state.
Definition (security goals). The construction must provide:
- Confidentiality: only the intended recipient (and the sender) can decrypt.
- Authentication: the recipient is sure the plaintext matches the on-chain commitment.
- Compactness: the encrypted form is small.
- Fast trial decryption: a wallet must check every output of every block against every viewing key it tracks; the check must be cheap.
2.2 The two ciphertexts
Definition. Each shielded OutputDescription / Action carries two ciphertexts:
- : encryption for the recipient (580 bytes for Sapling).
- : encryption for the sender's recovery (80 bytes).
Both use ChaCha20-Poly1305 with a static nonce; each key is single-use.
2.3 The Sapling KDF
Definition (KDF). The sender samples \mathsf{esk} \stackrel{\}{\leftarrow} \mathbb{F}_\ell\mathsf{epk} = [\mathsf{esk}] g_d$, and computes the shared secret
The recipient computes the same value as . The symmetric key is
The inclusion of in the KDF input ties the shared key to the specific ephemeral; this is needed for contributory key-agreement security.
2.4 The recipient plaintext
Definition (Sapling note plaintext).
The leading byte is the plaintext version so different transmission formats can coexist. Orchard prepends a different leading byte and extends the structure to include .
Definition (recipient ciphertext).
Output is the ciphertext (564 bytes for Sapling) followed by the 16-byte authentication tag. Total bytes.
2.5 The outgoing key
Definition (outgoing cipher key).
The outgoing ciphertext is
with 32 + 32 plaintext bytes plus 16-byte tag = 80 bytes.
2.6 The trial-decryption procedure
Invariant (decrypt-then-recommit). A wallet does not trust an AEAD-decrypted plaintext until it re-derives the note commitment and confirms it matches the published commitment. Acceptance without this step would allow accepting forged notes whose ciphertexts happen to decrypt under .
Trial decryption for each output each tracked :
- Compute .
- Derive .
- AEAD-decrypt ; on tag failure, move on.
- On success, parse , recover , then .
- Re-derive and check from the wire.
3. The code
3.1 The Domain abstraction
The crate zcash_note_encryption (external to this workspace)
abstracts a Domain trait. For Sapling the domain has
and
; for Orchard
and
. The structural
flow is identical; the curves differ.
The implementations:
sapling-crypto::note_encryption::SaplingDomain.orchard::note_encryption::OrchardDomain.
Both provide derive_esk, epk, kdf, derive_ock, etc.
3.2 High-level decryption entry point
The wallet-facing decryption is decrypt_transaction in
zcash_client_backend:
loading...
It iterates over each ufvks (account -> UFVK) entry, builds
PreparedIncomingViewingKey values for both external and internal
scopes, and tries every output. The OVK is also extracted so the
sender's own outputs are recoverable.
3.3 Batched scanning
For block scanning the path is
zcash_client_backend::scanning::scan_block:
loading...
The function takes a CompactBlock (not a full block) plus the set
of ScanningKeys, and produces a ScannedBlock. Trial decryption
runs in batches; the ECDH scalar multiplication dominates.
3.4 Compact decryption
A common optimisation: only the first bytes of the
plaintext (0x02, diversifier, value, ) are needed
to re-derive the commitment. Light wallets pull only those bytes
from lightwalletd and skip the memo, which makes scanning much
cheaper. The mode lives in the compact API of
zcash_note_encryption.
3.5 Sapling vs Orchard differences
| Aspect | Sapling | Orchard |
|---|---|---|
| Curve | Jubjub | Pallas |
| KDF personalisation | Zcash_SaplingKDF | Zcash_OrchardKDF |
| OCK personalisation | Zcash_Derive_ock | Orchard_ock_pre, then BLAKE2b |
| Plaintext leading byte | 0x02 | 0x02 (with Orchard fields appended) |
| Plaintext length | 564 bytes | 580 bytes |
| AEAD | ChaCha20-Poly1305 | ChaCha20-Poly1305 |
| Trial-decrypt steps | identical structure | identical structure |
Both use the same trait, parameterised by the Domain.
3.6 OVK-disabled mode
A wallet may want some outputs to be unrecoverable from (a privacy-conscious user paying a public counterparty does not want a leaked to compromise that payment). The protocol allows the sender to substitute a random for a specific output, making the OutCiphertext effectively unrecoverable to the sender. This is a per-output decision.
3.7 Performance and bandwidth
For each shielded output, trial decryption costs:
- One scalar mul on Jubjub or Pallas (the ECDH step).
- One BLAKE2b for the KDF.
- One ChaCha20-Poly1305 with empty associated data.
A light wallet downloading the compact subset (commitment +
ephemeral key + first 52 bytes of ) needs roughly
B per output instead of B. This is the basis
of the lightwalletd protocol.
3.8 Security properties (informally)
- Confidentiality. The ECDH shared secret is indistinguishable from random under DDH on the relevant curve, and the KDF is modeled as a random oracle in the proof, so is pseudorandom from the attacker's view. AEAD security gives confidentiality of the plaintext.
- Authentication of plaintext-to-commitment. The wallet refuses to accept a note whose claimed plaintext does not produce the published commitment.
- No forward secrecy. If leaks, all historical received outputs are recoverable. This is intentional: the viewing key is supposed to see history.
- Sender deniability. Without , no third party can tie the sender to the output, even given .
4. Failure modes
- Reusing . Re-using the same
across two outputs is catastrophic: the attacker can derive the
recipient's from the two shared secrets. The
builder must sample fresh for every output. This
invariant is enforced by the builder API in
zcash_primitives/src/transaction/builder.rs; any change to the OutputBuilder that allowedeskinjection from the caller without an explicitunsafe-tagged API would defeat this. - ZIP 212 transition. Sapling enc-ciphertext used to allow the
raw plaintext bytes for as a 32-byte little-endian
scalar. ZIP 212 (Canopy) changed this to derive
from a 32-byte seed via a hash. The
Rseedenum insapling-cryptocarries both variants; pre-Canopy notes ("pre-canopy notes") must be handled with the old derivation. - Wrong plaintext-version byte. The 0x02 plaintext-version byte is a hard requirement; an output with anything else must be rejected by trial decryption.
- Forgetting the AEAD tag length. The AEAD tag is included in the 580-byte length; novice readers sometimes treat it as overhead and trim it.
- Skipping commitment re-derivation. Accepting an output based solely on a successful AEAD tag check is unsafe. The re-derivation in step 5 of Section 2.6 catches forged ciphertexts. Removing this check in scanning code is a recipe for accepting garbage notes.
- Light-wallet partial-byte attacks. When using the compact decryption mode, a light wallet that does not subsequently fetch the full ciphertext to verify the memo and AEAD tag is trusting the lightwalletd server not to forge a value-binding match. This is documented in ZIP 307 and is a known limitation.
5. Spec pointers
- Zcash Protocol Specification, section 4.20 (In-band secret distribution): the formal definition of and the trial-decryption procedure cited in Section 2.
- ZIP 212 - Allow recipient to derive ephemeral secret from note plaintext:
the Canopy-era change to how is derived. Implemented
via
Rseedinsapling-crypto. - ZIP 307 - Light Client Protocol for Payment Detection: the compact-decryption optimisation and its trust model.
- Bernstein, ChaCha20 and Poly1305: the AEAD used in step 3 of Section 2.4.
- Hopwood et al., Sapling protocol design notes: the security argument for the ECDH-KDF-AEAD chain.
6. Exercises
- Identify the rejected output. Given an OutputDescription
whose AEAD tag verifies but whose re-derived commitment differs
from the published , trace the call path in
zcash_client_backend/src/decrypt.rsthat rejects it. Cite the line. - Compute the OCK key by hand. Given a synthetic and the public bytes, compute using a BLAKE2b CLI. Verify by decrypting the bytes with ChaCha20-Poly1305.
- Modify and test (code change). Add a unit test under
zcash_client_backendthat constructs aDecryptedOutputwith a deliberately-wrong and confirms thatdecrypt_transactiondoes not return it. The test must pass (i.e., the assertion that the output is not returned must hold). - Measure scan cost. Time a batched scan of 1000 outputs against 4 IVKs using a release build. Identify whether the bottleneck is the ECDH scalar multiplication or the AEAD; compare against the prediction in Section 3.7.
Answers in the code
- High-level decryption:
zcash_client_backend/src/decrypt.rs#L123-L165. - Block scan:
zcash_client_backend/src/scanning.rs#L609-L630. - ZIP 212
Rseedvariant: searchsapling-cryptoforRseed.
7. Further reading
- chapter 09: consensus rules the scanner must respect.
- chapter 10: the scanner's place in the wallet pipeline.
- Hopwood et al., Sapling protocol specification: formal definitions and security argument.