Skip to main content

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 ovk\mathsf{ovk} 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 (esk,epk,ock,Kenc\mathsf{esk}, \mathsf{epk}, \mathsf{ock}, K_{\text{enc}}) 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 vv.
  • The diversifier dd (so the recipient can recover gd,pkdg_d, \mathsf{pk}_d).
  • The randomness rcm\mathsf{rcm} for the commitment.
  • For Orchard, the additional fields ρ,ψ\rho, \psi.
  • 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 ovk\mathsf{ovk}, 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:

  • CencC^{\text{enc}}: encryption for the recipient (580 bytes for Sapling).
  • CoutC^{\text{out}}: 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,publishes, publishes \mathsf{epk} = [\mathsf{esk}] g_d$, and computes the shared secret

shared  =  [esk]pkd  =  [eskivk]gd.\mathsf{shared} \;=\; [\mathsf{esk}]\, \mathsf{pk}_d \;=\; [\mathsf{esk} \cdot \mathsf{ivk}]\, g_d.

The recipient computes the same value as [ivk]epk[\mathsf{ivk}]\,\mathsf{epk}. The symmetric key is

Kenc  =  KDFSapling(shared,epk)  =  BLAKE2b-256 ⁣(pers="Zcash_SaplingKDF",  repr(shared)repr(epk)).K_{\text{enc}} \;=\; \mathsf{KDF}_{\text{Sapling}}(\mathsf{shared}, \mathsf{epk}) \;=\; \mathsf{BLAKE2b\text{-}256}\!\bigl( \text{pers}=\text{"Zcash\_SaplingKDF"},\; \text{repr}(\mathsf{shared}) \mathbin{\|} \text{repr}(\mathsf{epk}) \bigr).

The inclusion of epk\mathsf{epk} 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).

npt  =  0x02dvLErcmmemo.\mathsf{npt} \;=\; 0\text{x}02 \mathbin{\|} d \mathbin{\|} v_{\text{LE}} \mathbin{\|} \mathsf{rcm} \mathbin{\|} \mathsf{memo}.

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 ρ,ψ\rho, \psi.

Definition (recipient ciphertext).

Cenc  =  ChaCha20-Poly1305Kenc ⁣(nonce=0,AD=,npt).C^{\text{enc}} \;=\; \mathsf{ChaCha20\text{-}Poly1305}_{K_{\text{enc}}}\!\bigl( \text{nonce}=0,\, \text{AD}=\emptyset,\, \mathsf{npt}\bigr).

Output is the ciphertext (564 bytes for Sapling) followed by the 16-byte authentication tag. Total 1+11+8+32+512+16=5801 + 11 + 8 + 32 + 512 + 16 = 580 bytes.

2.5 The outgoing key

Definition (outgoing cipher key).

Kout  =  PRFovkock ⁣(cv,cmu,epk)  =  BLAKE2b-256 ⁣(pers="Zcash_Derive_ock",ovkcvcmuepk).K_{\text{out}} \;=\; \mathsf{PRF}^{\mathsf{ock}}_{\mathsf{ovk}}\!\bigl(\mathsf{cv}, \mathsf{cm}_u, \mathsf{epk}\bigr) \;=\; \mathsf{BLAKE2b\text{-}256}\!\bigl( \text{pers}=\text{"Zcash\_Derive\_ock"}, \mathsf{ovk} \mathbin{\|} \mathsf{cv} \mathbin{\|} \mathsf{cm}_u \mathbin{\|} \mathsf{epk} \bigr).

The outgoing ciphertext is

Cout  =  ChaCha20-Poly1305Kout ⁣(nonce=0,pkdesk),C^{\text{out}} \;=\; \mathsf{ChaCha20\text{-}Poly1305}_{K_{\text{out}}}\!\bigl( \text{nonce}=0,\, \mathsf{pk}_d \mathbin{\|} \mathsf{esk}\bigr),

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 KencK_{\text{enc}}.

Trial decryption for each output ×\times each tracked ivk\mathsf{ivk}:

  1. Compute shared=[ivk]epk\mathsf{shared} = [\mathsf{ivk}]\,\mathsf{epk}.
  2. Derive KencK_{\text{enc}}.
  3. AEAD-decrypt CencC^{\text{enc}}; on tag failure, move on.
  4. On success, parse (d,v,rcm,memo)(d, v, \mathsf{rcm}, \mathsf{memo}), recover gd=DiversifyHash(d)g_d = \mathsf{DiversifyHash}(d), then pkd=[ivk]gd\mathsf{pk}_d = [\mathsf{ivk}]\,g_d.
  5. Re-derive cm=NoteCommit(rcm,v,gd,pkd)\mathsf{cm}' = \mathsf{NoteCommit}(\mathsf{rcm}, v, g_d, \mathsf{pk}_d) and check cm=cm\mathsf{cm}' = \mathsf{cm} 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 gdGJubjubg_d \in \mathbb{G}_{\text{Jubjub}} and eskFJub\mathsf{esk} \in \mathbb{F}_{\ell_{\text{Jub}}}; for Orchard gdGPallasg_d \in \mathbb{G}_{\text{Pallas}} and eskFqPallas\mathsf{esk} \in \mathbb{F}_{q_{\text{Pallas}}}. 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:

zcash_client_backend/src/decrypt.rs
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:

zcash_client_backend/src/scanning.rs
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 52\approx 52 bytes of the plaintext (0x02, diversifier, value, rcm\mathsf{rcm}) 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

AspectSaplingOrchard
CurveJubjubPallas
KDF personalisationZcash_SaplingKDFZcash_OrchardKDF
OCK personalisationZcash_Derive_ockOrchard_ock_pre, then BLAKE2b
Plaintext leading byte0x020x02 (with Orchard fields appended)
Plaintext length564 bytes580 bytes
AEADChaCha20-Poly1305ChaCha20-Poly1305
Trial-decrypt stepsidentical structureidentical 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 ovk\mathsf{ovk} (a privacy-conscious user paying a public counterparty does not want a leaked ovk\mathsf{ovk} to compromise that payment). The protocol allows the sender to substitute a random ovk\mathsf{ovk} 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 CencC^{\text{enc}}) needs roughly 100\sim 100 B per output instead of 700\sim 700 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 KencK_{\text{enc}} 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 ivk\mathsf{ivk} leaks, all historical received outputs are recoverable. This is intentional: the viewing key is supposed to see history.
  • Sender deniability. Without ovk\mathsf{ovk}, no third party can tie the sender to the output, even given (cv,cmu,epk)(\mathsf{cv}, \mathsf{cm}_u, \mathsf{epk}).

4. Failure modes

  • Reusing esk\mathsf{esk}. Re-using the same esk\mathsf{esk} across two outputs is catastrophic: the attacker can derive the recipient's ivk\mathsf{ivk} from the two shared secrets. The builder must sample fresh esk\mathsf{esk} for every output. This invariant is enforced by the builder API in zcash_primitives/src/transaction/builder.rs; any change to the OutputBuilder that allowed esk injection from the caller without an explicit unsafe-tagged API would defeat this.
  • ZIP 212 transition. Sapling enc-ciphertext used to allow the raw plaintext bytes for rcm\mathsf{rcm} as a 32-byte little-endian scalar. ZIP 212 (Canopy) changed this to derive rcm\mathsf{rcm} from a 32-byte seed via a hash. The Rseed enum in sapling-crypto carries 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

6. Exercises

  1. Identify the rejected output. Given an OutputDescription whose AEAD tag verifies but whose re-derived commitment differs from the published cmu\mathsf{cm}_u, trace the call path in zcash_client_backend/src/decrypt.rs that rejects it. Cite the line.
  2. Compute the OCK key by hand. Given a synthetic ovk\mathsf{ovk} and the public (cv,cmu,epk)(\mathsf{cv}, \mathsf{cm}_u, \mathsf{epk}) bytes, compute KoutK_{\text{out}} using a BLAKE2b CLI. Verify by decrypting the CoutC^{\text{out}} bytes with ChaCha20-Poly1305.
  3. Modify and test (code change). Add a unit test under zcash_client_backend that constructs a DecryptedOutput with a deliberately-wrong rcm\mathsf{rcm} and confirms that decrypt_transaction does not return it. The test must pass (i.e., the assertion that the output is not returned must hold).
  4. 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

7. Further reading