Skip to main content

04 - Sprout and Sapling

1. Why this chapter exists

Sprout is the original Zerocash protocol embedded in Zcash at launch; it is historically important but largely frozen. Sapling is the production shielded pool and the locus of almost everything in zcash_primitives, zcash_proofs, and zcash_keys. A reader who cannot state the Spend statement from memory will not be able to follow the builder code in zcash_primitives/src/transaction/builder.rs or audit any change to it. By the end of this chapter you will be able to map every field of a SpendDescription and OutputDescription to its mathematical role and locate it in zcash_primitives/src/transaction/components/sapling.rs.

This chapter is the narrative introduction. For the authoritative symbol-by-symbol reference of every key (ask,nsk,ak,nk,ivk,ovk,dk,esk,epk,\mathsf{ask}, \mathsf{nsk}, \mathsf{ak}, \mathsf{nk}, \mathsf{ivk}, \mathsf{ovk}, \mathsf{dk}, \mathsf{esk}, \mathsf{epk}, \ldots), see chapter 23 - The complete key catalog. For the clause-by-clause walk of the Spend and Output circuits with constraint counts, see chapter 24 - Circuits, constraint by constraint.

2. Definitions

Sprout

Definition (Sprout note). The tuple

note  =  (apk,v,ρ,r),\mathsf{note} \;=\; (a_{\mathsf{pk}}, v, \rho, r),

where apka_{\mathsf{pk}} is the recipient paying key, vv is the value in zatoshis, ρ\rho is a uniqueness nonce, and rr is commitment randomness. The commitment is

cm  =  SHA256 ⁣(0xb0apkvρr).\mathsf{cm} \;=\; \mathsf{SHA256}\!\bigl( 0\text{xb0} \,\|\, a_{\mathsf{pk}} \,\|\, v \,\|\, \rho \,\|\, r \bigr).

Definition (JoinSplit). A constant-shape gadget with 2 inputs, 2 outputs, a public scalar vpuboldv_{\text{pub}}^{\text{old}} moving from transparent into the shielded side, a public scalar vpubnewv_{\text{pub}}^{\text{new}} moving the other way, a public Merkle root rt\mathsf{rt} (anchor), and a public per-JoinSplit signature digest hsigh_{\mathsf{sig}}.

Sapling

Definition (Pedersen hash, PH\mathsf{PH}). Algebraic hash on Jubjub: pad input bits to a multiple of three, group bits in chunks of 3 then segments of c=63c = 63 chunks (189 bits per segment), encode each segment as a signed integer via the windowed encoding enc3(b0,b1,b2)=(1+b0+2b1)(12b2){4,,1,1,,4}\text{enc}_3(b_0, b_1, b_2) = (1 + b_0 + 2 b_1)(1 - 2 b_2) \in \{-4, \ldots, -1, 1, \ldots, 4\}, multiply each segment by an independent generator GjG_j derived deterministically from a domain-separation string, and sum.

Definition (Sapling note commitment).

NoteCommit(rcm,v,gd,pkd)  =  PH(Dnc,repr(v)repr(gd)repr(pkd))  +  [rcm]Rnc.\mathsf{NoteCommit}(\mathsf{rcm}, v, g_d, \mathsf{pk}_d) \;=\; \mathsf{PH}\bigl(D_{\text{nc}},\, \text{repr}(v) \mathbin{\|} \text{repr}(g_d) \mathbin{\|} \text{repr}(\mathsf{pk}_d)\bigr) \;+\; [\mathsf{rcm}]\, R_{\text{nc}}.

The randomness term [rcm]Rnc[\mathsf{rcm}] R_{\text{nc}} makes the commitment perfectly hiding.

Definition (Sapling Merkle hash).

MerkleHash(,xleft,xright)  =  ExtractJubjub ⁣(PH(DMH,,xleftxright)),\mathsf{MerkleHash}(\ell, x_{\text{left}}, x_{\text{right}}) \;=\; \mathsf{ExtractJubjub}\!\bigl( \mathsf{PH}(D_{\text{MH}, \ell}, x_{\text{left}} \mathbin{\|} x_{\text{right}}) \bigr),

where \ell is the layer index and ExtractJubjub\mathsf{ExtractJubjub} takes the uu-coordinate of the resulting Jubjub point modulo the field. The layer-indexed domain separation prevents tree-rotation attacks.

Definition (Sapling key tree).

sk    PRFexpand    (ask,nsk,ovk)          (ak,nk,ovk)          ivk          (pkd,gd).\mathsf{sk} \;\xrightarrow{\;\mathsf{PRF}^{\text{expand}}\;}\; (\mathsf{ask}, \mathsf{nsk}, \mathsf{ovk}) \;\xrightarrow{\;\;\;}\; (\mathsf{ak}, \mathsf{nk}, \mathsf{ovk}) \;\xrightarrow{\;\;\;}\; \mathsf{ivk} \;\xrightarrow{\;\;\;}\; (\mathsf{pk}_d, g_d).

The full viewing key fvk=(ak,nk,ovk)\mathsf{fvk} = (\mathsf{ak}, \mathsf{nk}, \mathsf{ovk}) is sufficient to decrypt any incoming or outgoing-tagged note.

Definition (Sapling nullifier). For a note with commitment cm\mathsf{cm}, position pos\mathsf{pos} in the tree, and ρ=MixingPedersenHash(cm,pos)\rho = \mathsf{MixingPedersenHash}(\mathsf{cm}, \mathsf{pos}),

nf  =  PRFnknfSapling(ρ).\mathsf{nf} \;=\; \mathsf{PRF}^{\mathsf{nfSapling}}_{\mathsf{nk}}(\rho).

Invariant (Sapling binding equation). For any valid Sapling bundle,

icviin    jcvjout  =  [vbal]V  +  [rbal]R,\sum_i \mathsf{cv}_i^{\text{in}} \;-\; \sum_j \mathsf{cv}_j^{\text{out}} \;=\; [v_{\text{bal}}]V \;+\; [r_{\text{bal}}]R,

with rbal=rcviinrcvjoutr_{\text{bal}} = \sum \mathsf{rcv}_i^{\text{in}} - \sum \mathsf{rcv}_j^{\text{out}}. The binding signature certifies that the spender knows rbalr_{\text{bal}}, which is feasible only when the equation holds.

3. The code

3.1 Sprout: a one-page tour

The Sprout circuit lives in zcash_proofs/src/circuit/sprout/mod.rs (the "hybrid Sprout" implementation re-encoded for Groth16 instead of the original BCTV14 system). Its top-level types pin the shape:

zcash_proofs/src/circuit/sprout/mod.rs
loading...

The synthesize method enforces, for each input ii:

  1. Recompute cmi\mathsf{cm}_i from the witnessed note.
  2. Verify a Merkle path of depth 29 from cmi\mathsf{cm}_i to the public anchor rt\mathsf{rt} (with the witnessed authentication path).
  3. Derive the paying key apk=PRFaskaddr(0)a_{\mathsf{pk}} = \mathsf{PRF}^{addr}_{a_{\mathsf{sk}}}(0).
  4. Compute the nullifier nfi=PRFasknf(ρi)\mathsf{nf}_i = \mathsf{PRF}^{\mathsf{nf}}_{a_{\mathsf{sk}}}(\rho_i).
  5. Compute the "hih_i tag" hi=PRFaskpk(i,hsig)h_i = \mathsf{PRF}^{pk}_{a_{\mathsf{sk}}}(i, h_{\mathsf{sig}}) that binds the JoinSplit to a specific hsigh_{\mathsf{sig}}.

For each output jj it derives a fresh ρj\rho_j from ϕ\phi and indices and recomputes the commitment.

Finally it enforces the balance equation

vpubold+v1+v2  =  vpubnew+v1+v2,v_{\text{pub}}^{\text{old}} + v_1 + v_2 \;=\; v_{\text{pub}}^{\text{new}} + v'_1 + v'_2,

with each vi,vj[0,264)v_i, v'_j \in [0, 2^{64}) enforced via boolean range constraints.

The PRFs are all "SHA-256 with a tag prefix", for example

PRFasknf(ρ)=SHA256 ⁣(1110askρ),\mathsf{PRF}^{\mathsf{nf}}_{a_{\mathsf{sk}}}(\rho) = \mathsf{SHA256}\!\bigl( 1110 \,\|\, a_{\mathsf{sk}} \,\|\, \rho \bigr),

where 1110 is a 4-bit tag.

The Sprout-Groth16 proving key is 64 MB (sprout-groth16.params, SHA-256 in zcash_proofs/src/lib.rs#L52). Verification is a single Groth16 pairing equation.

Sprout is closed to new outputs since NU5. The code remains for historical sweeps from old Sprout balances; treat it as legacy.

3.2 Sapling: curves and parameters

  • Fr\mathbb{F}_r: the scalar field of BLS12-381, r2255r \approx 2^{255}, a 255-bit prime.

  • Fq\mathbb{F}_q: the base field of Jubjub. Equal to Fr\mathbb{F}_r.

  • Jubjub is a twisted Edwards curve over Fr\mathbb{F}_r,

    x2+y2  =  1  +  dx2y2,d=1024010241, -x^2 + y^2 \;=\; 1 \;+\; d\, x^2 y^2, \qquad d = -\frac{10240}{10241},

    with a prime-order subgroup of order

    =6554484396890773809930967563523245729705921265872317281365359162392183254199. \ell = 6\,554\,484\,396\,890\,773\,809\,930\,967\,563\,523\,245\,729\,705\,921\,265\,872\,317\,281\,365\,359\,162\,392\,183\,254\,199.
  • GJubjubG_{\text{Jubjub}} is the fixed generator of that subgroup.

Why this curve? Twisted Edwards arithmetic is strongly unified (the same formula for addition and doubling), which keeps the in-circuit constraint count small. Because Fr\mathbb{F}_r is the SNARK scalar field, a Jubjub scalar mul costs only roughly 750 R1CS constraints per operation.

3.3 The Sapling key tree

From a 32-byte spending key sk\mathsf{sk}:

  1. Derive

    ask=ToScalar ⁣(PRFskexpand(0)),\mathsf{ask} = \mathsf{ToScalar}\!\bigl( \mathsf{PRF}^{\text{expand}}_{\mathsf{sk}}(0)\bigr), nsk=ToScalar ⁣(PRFskexpand(1)),\mathsf{nsk} = \mathsf{ToScalar}\!\bigl( \mathsf{PRF}^{\text{expand}}_{\mathsf{sk}}(1)\bigr), ovk=PRFskexpand(2)[0..32],\mathsf{ovk} = \mathsf{PRF}^{\text{expand}}_{\mathsf{sk}}(2)[\,0..32\,],

    where ToScalar\mathsf{ToScalar} reduces a 64-byte string modulo \ell.

  2. Compute the public points

    ak=[ask]GSaplingak,nk=[nsk]GSaplingnk,\mathsf{ak} = [\mathsf{ask}] G_{\text{Sapling}}^{\text{ak}}, \qquad \mathsf{nk} = [\mathsf{nsk}] G_{\text{Sapling}}^{\text{nk}},

    with GSaplingakG_{\text{Sapling}}^{\text{ak}} and GSaplingnkG_{\text{Sapling}}^{\text{nk}} distinct fixed generators on Jubjub.

  3. Compute the incoming viewing key

    ivk=CRHivk(ak,nk)  mod  ,\mathsf{ivk} = \mathsf{CRH}^{\mathsf{ivk}}(\mathsf{ak}, \mathsf{nk}) \;\bmod\; \ell,

    where CRHivk\mathsf{CRH}^{\mathsf{ivk}} is BLAKE2s with personalisation "Zcashivk".

  4. For each 11-byte diversifier d{0,1}88d \in \{0,1\}^{88}, compute gd=DiversifyHash(d)g_d = \mathsf{DiversifyHash}(d), a hash-to-curve into Jubjub. Not every dd yields a valid prime-order point; if it does not, the diversifier is invalid and skipped. The diversified transmission key is

    pkd=[ivk]gd.\mathsf{pk}_d = [\mathsf{ivk}] g_d.
  5. A Sapling payment address is the pair (d,pkd)(d, \mathsf{pk}_d) encoded as 43 plaintext bytes then bech32 with HRP zs.

Diversified addresses follow: each ivk\mathsf{ivk} generates infinitely many payment addresses sharing the same viewing key. A wallet can hand out a fresh dd to every counterparty without revealing the common ivk\mathsf{ivk}.

3.4 Spend description (math)

A Sapling SpendDescription is the tuple

SD  =  (cv,anchor,nf,rk,πSpend,σspendAuth).\mathsf{SD} \;=\; (\mathsf{cv}, \mathsf{anchor}, \mathsf{nf}, \mathsf{rk}, \pi_{\text{Spend}}, \sigma_{\text{spendAuth}}).
  • cv=[v]V+[rcv]R\mathsf{cv} = [v]V + [\mathsf{rcv}]R is the value commitment to the spent note's value with fresh randomness rcv\mathsf{rcv}.
  • anchor\mathsf{anchor} is the Merkle root used for membership.
  • nf\mathsf{nf} is the nullifier.
  • rk=ak+[α]Gak\mathsf{rk} = \mathsf{ak} + [\alpha] G^{\mathsf{ak}} is the re-randomised spend-authority public key.
  • πSpend\pi_{\text{Spend}} is the Groth16 proof.
  • σspendAuth\sigma_{\text{spendAuth}} is a RedJubjub signature under rsk=ask+α\mathsf{rsk} = \mathsf{ask} + \alpha over the sighash of the transaction.

The wire format is implemented by read_spend_v4 (Sapling v4 transactions, full per-spend signature and proof) and read_spend_v5 (Sapling v5 transactions, sigs and proofs are factored to the end of the bundle):

zcash_primitives/src/transaction/components/sapling.rs
loading...

The Spend statement (what the circuit enforces): the prover knows (v,gd,pkd,rcm,α,ak,nsk,auth-path)(v, g_d, \mathsf{pk}_d, \mathsf{rcm}, \alpha, \mathsf{ak}, \mathsf{nsk}, \text{auth-path}) such that

  1. cm=NoteCommit(rcm,v,gd,pkd)\mathsf{cm} = \mathsf{NoteCommit}(\mathsf{rcm}, v, g_d, \mathsf{pk}_d).
  2. The Merkle path proves cm\mathsf{cm} is in the tree with root anchor\mathsf{anchor}. Special case: if v=0v = 0 the path check is skipped, allowing "dummy" spends used to mask the input count.
  3. ivk=CRHivk(ak,nk)\mathsf{ivk} = \mathsf{CRH}^{\mathsf{ivk}}(\mathsf{ak}, \mathsf{nk}) where nk=[nsk]Gnk\mathsf{nk} = [\mathsf{nsk}] G^{\mathsf{nk}}.
  4. pkd=[ivk]gd\mathsf{pk}_d = [\mathsf{ivk}] g_d (the spender owns this address).
  5. rk=ak+[α]Gak\mathsf{rk} = \mathsf{ak} + [\alpha] G^{\mathsf{ak}}.
  6. nf=PRFnknfSapling(MixingPedersenHash(cm,pos))\mathsf{nf} = \mathsf{PRF}^{\mathsf{nfSapling}}_{\mathsf{nk}}\bigl( \mathsf{MixingPedersenHash}(\mathsf{cm}, \mathsf{pos})\bigr).
  7. cv=[v]V+[rcv]R\mathsf{cv} = [v]V + [\mathsf{rcv}]R for known rcv\mathsf{rcv}.
  8. v[0,264)v \in [0, 2^{64}).

Public inputs: cv,anchor,nf,rk\mathsf{cv}, \mathsf{anchor}, \mathsf{nf}, \mathsf{rk}.

3.5 Output description (math)

An OutputDescription is

OD  =  (cv,cmu,epk,Cenc,Cout,πOutput).\mathsf{OD} \;=\; (\mathsf{cv}, \mathsf{cm}^u, \mathsf{epk}, C^{\text{enc}}, C^{\text{out}}, \pi_{\text{Output}}).
  • cv\mathsf{cv}: value commitment of the new note.
  • cmu\mathsf{cm}^u: the uu-coordinate of the new note's commitment. (The full commitment is recoverable; only the uu-coordinate is published to save space.)
  • epk=[esk]gd\mathsf{epk} = [\mathsf{esk}] g_d: ephemeral public key for ECDH note encryption.
  • CencC^{\text{enc}}: the encrypted note plaintext (recipient, value, rcm\mathsf{rcm}, memo).
  • CoutC^{\text{out}}: the outgoing ciphertext that lets the sender recover the plaintext using ovk\mathsf{ovk}.
  • πOutput\pi_{\text{Output}}: the Groth16 proof.

The Output statement: the prover knows (v,gd,pkd,rcm,rcv,esk)(v, g_d, \mathsf{pk}_d, \mathsf{rcm}, \mathsf{rcv}, \mathsf{esk}) such that

  1. cm=NoteCommit(rcm,v,gd,pkd)\mathsf{cm} = \mathsf{NoteCommit}(\mathsf{rcm}, v, g_d, \mathsf{pk}_d) and cmu\mathsf{cm}^u is its uu-coordinate.
  2. cv=[v]V+[rcv]R\mathsf{cv} = [v]V + [\mathsf{rcv}]R.
  3. epk=[esk]gd\mathsf{epk} = [\mathsf{esk}] g_d.
  4. v[0,264)v \in [0, 2^{64}).
  5. gdg_d is a valid prime-order subgroup element (non-zero).

Public inputs: cv,cmu,epk\mathsf{cv}, \mathsf{cm}^u, \mathsf{epk}.

3.6 The bundle and binding signature

A Sapling bundle is

Bundle  =  ({SDi},{ODj},vbal,σbind).\mathsf{Bundle} \;=\; \bigl( \{\mathsf{SD}_i\},\, \{\mathsf{OD}_j\},\, v_{\text{bal}},\, \sigma_{\text{bind}} \bigr).

The binding equation from Section 2 holds because each cv\mathsf{cv} is Pedersen and the proofs internally certify well-formedness. The binding signature σbind\sigma_{\text{bind}} is a RedJubjub signature over the sighash whose verification key is

bvk=icviinjcvjout[vbal]V.\mathsf{bvk} = \sum_i \mathsf{cv}_i^{\text{in}} - \sum_j \mathsf{cv}_j^{\text{out}} - [v_{\text{bal}}] V.

If the equation holds, bvk=[rbal]R\mathsf{bvk} = [r_{\text{bal}}]R, so the spender holds the secret key to that point. If anything is off by even a single zatoshi or one randomness off, bvk\mathsf{bvk} is a random-looking point and the signature cannot be forged. Balance is enforced by a signature whose key is a function of the commitments.

3.7 Groth16 specifics

A Groth16 proof is

π=(A,B,C),A,CG1,  BG2.\pi = (A, B, C), \qquad A, C \in \mathbb{G}_1,\; B \in \mathbb{G}_2.

Verification given public inputs (x1,,x)(x_1, \ldots, x_\ell) and verifying key vk=(α,β,γ,δ,{τi}i=0)\mathsf{vk} = (\alpha, \beta, \gamma, \delta, \{\tau_i\}_{i=0}^{\ell}):

e(A,B)  =?  e(α,β)e ⁣(i=0xiτi,γ)e(C,δ).e(A, B) \;\stackrel{?}{=}\; e(\alpha, \beta) \cdot e\!\Bigl(\textstyle\sum_{i=0}^{\ell} x_i \tau_i, \gamma\Bigr) \cdot e(C, \delta).

The vector {τi}\{\tau_i\} is the input key: one G1\mathbb{G}_1 point per public input, plus a constant. For Sapling Spend =7\ell = 7 (witness encoding of cv,anchor,nf,rk\mathsf{cv}, \mathsf{anchor}, \mathsf{nf}, \mathsf{rk}); for Sapling Output =5\ell = 5.

The Sapling trusted setup was performed in two MPC ceremonies in 2017-2018 ("Powers of Tau" then per-circuit). The verifying-key hashes are hardcoded in zcash_proofs/src/lib.rs (SAPLING_SPEND_HASH, SAPLING_OUTPUT_HASH). The wallet downloads the proving keys with download-params and verifies them by SHA-256.

3.8 End-to-end

To spend vinv_{\text{in}} from a note and create a new output of value voutv_{\text{out}} (plus a change output) with fee ff, a Sapling transaction:

  1. Picks anchor rt\mathsf{rt} from a recent block.
  2. Constructs SpendDescriptions for each input, sampling αi\alpha_i and rcviin\mathsf{rcv}_i^{\text{in}}, generating the Groth16 proof and the spend-auth signature.
  3. Constructs OutputDescriptions, sampling fresh rcvjout\mathsf{rcv}_j^{\text{out}}, rcmj\mathsf{rcm}_j, and eskj\mathsf{esk}_j, encrypting the note plaintext.
  4. Sets $v_{\text{bal}} = v_{\text{in}} - v_{\text{out}}
    • v_{\text{change}}$ (sign convention from the spec).
  5. Computes σbind\sigma_{\text{bind}} under the implicit key $\mathsf{bvk} = \sum \mathsf{cv}_i^{\text{in}}
    • \sum \mathsf{cv}j^{\text{out}} - [v{\text{bal}}] V$.

A node verifies each proof against its vk\mathsf{vk}, verifies the spend-auth signatures, verifies the binding signature, and checks nullifier non-membership.

The Sapling protocol implementation moved out of this workspace into the sapling-crypto crate. What remains here:

4. Failure modes

  • Sprout counterfeiting CVE-2019-7167. The original Sprout proving system was BCTV14. A soundness flaw in BCTV14 allowed a prover to forge a JoinSplit proof under a weaker assumption. Mitigation: migration to Groth16 with a fresh MPC and the "hybrid Sprout" wrapper that this repo still ships. See chapter 12 for the full timeline and the ECC remediation post. Any change to the Sprout circuit must preserve the boolean range constraints; removing them silently restores the BCTV14 failure mode at the application layer.
  • Sapling InternalH issue and cm-vs-cm^u confusion. Early Sapling implementations conflated the full commitment with its extracted uu-coordinate. Any code change that publishes a full cm\mathsf{cm} where the spec asks for cmu\mathsf{cm}^u (or the reverse) leaks information and breaks downstream wallets.
  • Dummy-spend value drift. A Sapling bundle may include dummy spends with v=0v = 0 to mask the input count. Builders must enforce v=0v = 0 for dummies; non-zero dummies silently corrupt vbalv_{\text{bal}} and break the binding signature.
  • Wrong ToScalar\mathsf{ToScalar} reduction. ToScalar\mathsf{ToScalar} reduces a 64-byte string modulo \ell. Substituting a 32-byte truncation biases the key distribution and silently breaks unlinkability proofs.
  • Re-randomisation reuse. Each spend must sample a fresh α\alpha. Reusing α\alpha across two spends links their rk\mathsf{rk} to the same underlying ak\mathsf{ak}, defeating the entire point of RedJubjub re-randomisation.

Tests under zcash_primitives/src/transaction/tests.rs exercise the full v4 / v5 serialization round-trip; the sapling-crypto crate carries the circuit-level tests for the Spend and Output statements.

5. Spec pointers

6. Exercises

  1. Map a field. Open SpendDescription and identify the source line of each component of the tuple (cv,anchor,nf,rk,πSpend,σspendAuth)(\mathsf{cv}, \mathsf{anchor}, \mathsf{nf}, \mathsf{rk}, \pi_{\text{Spend}}, \sigma_{\text{spendAuth}}).
  2. Predict the dispatch. For a v4 transaction, which of read_spend_v4 or read_spend_v5 does the parser call? What about a v5 transaction? Cite the call site in zcash_primitives/src/transaction/components/sapling.rs.
  3. Modify and test. In a checkout, add a unit test under zcash_primitives that constructs a SpendDescription with a deliberately wrong rk\mathsf{rk} (e.g. negate it) and confirms that the spend-auth signature verification fails. The test should pass (i.e. the assertion that verification returns an error must hold). Cite the public verification entry point from the redjubjub crate as your reference.

Answers in the code

7. Further reading

  • chapter 05: Orchard and Halo 2, which replace the trusted-setup Groth16 stack with a transparent IPA-based system.
  • chapter 16: the windowed encoding for Pedersen hashes, in-circuit cost, generator derivation.
  • chapter 24: clause-by-clause walk of the Spend and Output circuits with constraint counts.
  • Hopwood, Bowe, Hornby, Wilcox. Sapling design notes.