Skip to main content

24 - Circuits, constraint by constraint

1. Why this chapter exists

The Sapling Spend circuit, the Sapling Output circuit, and the Orchard Action circuit are the cryptographic heart of Zcash's shielded protocols. Earlier chapters described them at the statement level. A contributor who modifies a circuit, adds a public input, or changes a generator without understanding each constraint will either break soundness or invalidate the trusted setup. This chapter walks every clause in the order the prover witnesses and the verifier checks, with an explicit "attack on omission" note showing what fails if the clause is removed. The implementations live in the external sapling-crypto and orchard crates, consumed by this workspace via zcash_proofs/src/circuit (for Sprout) and the high-level builders in zcash_primitives/src/transaction/builder.rs.

Constraint counts are approximate; exact numbers depend on gadget implementation and shift over time as optimisations land.

2. Definitions

Definition (R1CS). A Rank-1 Constraint System. Each constraint has the form

(iaiwi)(ibiwi)  =  (iciwi),\Bigl(\sum_i a_i\, w_i\Bigr) \cdot \Bigl(\sum_i b_i\, w_i\Bigr) \;=\; \Bigl(\sum_i c_i\, w_i\Bigr),

with wiw_i the wires (witness vector plus a constant) and ai,bi,cia_i, b_i, c_i public coefficients. Sapling Spend uses approximately 1.5×1051.5 \times 10^5 such constraints in sapling-crypto's circuit. The toolkit is bellman.

Definition (PLONKish / Halo 2). A table of cells with custom gates. Each row ii may impose constraints of the form

G(w1(ωi),w2(ωi),)qsel(ωi)  =  0,G\bigl(w_1(\omega^i), w_2(\omega^i), \ldots\bigr) \cdot q_{\text{sel}}(\omega^i) \;=\; 0,

where qselq_{\text{sel}} is the selector polynomial that activates the gate. Permutation arguments encode copy constraints, and lookups enforce table membership. The Orchard Action circuit uses 2112^{11} rows by default and is implemented in halo2_proofs.

Definition (Underconstrained advice cell). An advice cell in a Halo 2 circuit that no selector-gated gate constrains. A malicious prover can assign such a cell arbitrarily, breaking soundness. The Trail of Bits review of Orchard codified this as a recurring finding class.

Definition (Incomplete addition). A point-addition formula that fails when the operands coincide. The unified twisted- Edwards formula (used for Jubjub) does not have this issue; incomplete short-Weierstrass addition (used for Pallas in some gates) does, and requires a distinctness witness.

Definition (Pedersen hash gadget, PH\mathsf{PH}). The Jubjub- based windowed-multiplication hash defined in chapter 16 and used across the Sapling circuit. Constraint cost is approximately 66 constraints per input bit.

Definition (Sinsemilla). The Pallas-based chunk-and-add hash used in Orchard. Each 1010-bit chunk is looked up in a generator table, and successive points are combined via incomplete addition. Constraint cost is roughly proportional to the chunk count.

Definition (Statement). The relation the circuit enforces between public inputs and witness. Each chapter section names the statement before walking its clauses.

3. The code

3.1 Sapling Spend statement

The full statement (consolidated from chapter 04): the prover knows secret (v,gd,pkd,rcm,α,ak,nsk,auth-path,pos)(v, g_d, \mathsf{pk}_d, \mathsf{rcm}, \alpha, \mathsf{ak}, \mathsf{nsk}, \text{auth-path}, \text{pos}) such that:

  1. cm=NoteCommitrcm(gd,pkd,v)\mathsf{cm} = \mathsf{NoteCommit}^{\mathsf{rcm}}(g_d, \mathsf{pk}_d, v).
  2. The Merkle path from cm\mathsf{cm} at pos\text{pos} leads to the public anchor.
  3. pkd=[ivk]gd\mathsf{pk}_d = [\mathsf{ivk}]\,g_d with ivk=CRHivk(ak,nk)\mathsf{ivk} = \mathsf{CRH}^{\mathsf{ivk}}(\mathsf{ak}, \mathsf{nk}) and nk=[nsk]Gnk\mathsf{nk} = [\mathsf{nsk}]\, G^{\mathsf{nk}}.
  4. rk=ak+[α]Gak\mathsf{rk} = \mathsf{ak} + [\alpha]\,G^{\mathsf{ak}} (public).
  5. nf=PRFnknfSapling(MixingPedersenHash(cm,pos))\mathsf{nf} = \mathsf{PRF}^{\mathsf{nfSapling}}_{\mathsf{nk}}( \mathsf{MixingPedersenHash}(\mathsf{cm}, \text{pos})) (public).
  6. cv=[v]V+[rcv]R\mathsf{cv} = [v]\,V + [\mathsf{rcv}]\,R (public cv\mathsf{cv}).
  7. v[0,264)v \in [0, 2^{64}).
  8. Either v=0v = 0 (dummy spend, Merkle check skipped) or the Merkle path is checked.

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

The verifying-key hash for this circuit is pinned in zcash_proofs/src/lib.rs:

zcash_proofs/src/lib.rs
loading...

3.2 Sapling Spend witness allocation

Approximate sub-circuit costs (R1CS constraints):

WitnessBitsConstraints
vv6464 boolean + 1 packing
gdg_d256 (encoded as bits)~750
pkd\mathsf{pk}_d256~750
rcm\mathsf{rcm}252252 boolean
α\alpha252252 boolean
ak\mathsf{ak}256~750
nsk\mathsf{nsk}252252 boolean
auth-path32×25632 \times 256~8000 bit constraints

The witness alone is ~12,000 constraints before any checks run.

3.3 Clause: nk=[nsk]Gnk\mathsf{nk} = [\mathsf{nsk}]\,G^{\mathsf{nk}}

Scalar mul of a fixed generator by a 252-bit secret. Using the fixed-base windowed comb gadget in sapling-crypto:

  • 252 bits of nsk\mathsf{nsk} are decomposed.
  • For each 3-bit window, a constant-time table select picks one of 8 precomputed multiples.
  • The selected multiples are summed.

Constraint cost: ~750.

Attack on omission: a witnessed nk\mathsf{nk} unrelated to nsk\mathsf{nsk} lets the spender choose nk\mathsf{nk} post-hoc, allowing nullifier prediction for unspent notes or forged nullifier collisions.

3.4 Clause: ivk=CRHivk(ak,nk)\mathsf{ivk} = \mathsf{CRH}^{\mathsf{ivk}}(\mathsf{ak}, \mathsf{nk})

CRHivk\mathsf{CRH}^{\mathsf{ivk}} is BLAKE2s-256 of the concatenated encodings of ak\mathsf{ak} and nk\mathsf{nk} (as uu-coordinates) plus parity bits. Implementing BLAKE2s in R1CS is expensive: one of the largest sub-circuits in Sapling, ~16,000 constraints.

The output is reduced modulo J\ell_J to produce ivk\mathsf{ivk}. The reduction is a controlled bit-truncation rather than a full modular reduction, valid because J<2252\ell_J < 2^{252} and the top bits are zeroed.

Constraint cost: ~16,000.

Attack on omission: an unmoored ivk\mathsf{ivk} would let the prover claim ownership of arbitrary pkd\mathsf{pk}_d they do not control.

3.5 Clause: pkd=[ivk]gd\mathsf{pk}_d = [\mathsf{ivk}]\,g_d

Variable-base scalar mul: gdg_d is itself witnessed, so cannot use a fixed-base table. Implementation: 252-bit Edwards scalar-mul gadget using strongly-unified addition.

Constraint cost: ~3000.

Attack on omission: a prover could spend a note addressed to an arbitrary (d,pkd)(d, \mathsf{pk}_d) they do not own.

3.6 Clause: rk=ak+[α]Gak\mathsf{rk} = \mathsf{ak} + [\alpha]\,G^{\mathsf{ak}}

Fixed-base scalar mul [α]Gak[\alpha]\,G^{\mathsf{ak}} (~750 constraints) plus one Edwards addition (~6 constraints), then equality with the public rk\mathsf{rk}.

Constraint cost: ~760.

Attack on omission: the published rk\mathsf{rk} would not be a re-randomisation of the actual ak\mathsf{ak}. An attacker could sign with their own key under their own rk\mathsf{rk} while spending a victim's note.

3.7 Clause: NoteCommitment

cm=PedersenHashDnc(repr(v)repr(gd)repr(pkd))+[rcm]Rnc.\mathsf{cm} = \mathsf{PedersenHash}_{D_{\text{nc}}}\bigl( \text{repr}(v) \,\|\, \text{repr}(g_d) \,\|\, \text{repr}(\mathsf{pk}_d)\bigr) + [\mathsf{rcm}]\,R_{\text{nc}}.

The Pedersen hash gadget (chapter 16) is the most heavily used sub-circuit. For an input of 64+256+256=57664 + 256 + 256 = 576 bits:

  • ~6 constraints per bit, so ~3500 for the hash.
  • Plus the randomness term: 252-bit fixed-base scalar mul on RncR_{\text{nc}}, ~750 constraints.

Constraint cost: ~4250.

Attack on omission: a prover that picks cm\mathsf{cm} freely can spend a note that does not exist.

3.8 Clause: Merkle path

For each layer {0,1,,31}\ell \in \{0, 1, \ldots, 31\}:

  1. Witness the auth-path sibling at this layer and a boolean indicating "is the current node the left or right child".
  2. Compute MerkleHash(left,right)\mathsf{MerkleHash}_\ell(\text{left}, \text{right}) where left/right are conditionally swapped based on the bit.
  3. Use the result as the current node at layer +1\ell + 1.

The conditional swap costs ~2 constraints; the Pedersen hash for a 512-bit input is ~3000 constraints; the layer personalisation adds a small constant.

Per layer: ~3000 constraints. Times 32 layers: ~96,000 constraints. This is the dominant cost of the Spend circuit.

Attack on omission: a prover could spend an arbitrary cm\mathsf{cm} they invented, without it being in the tree. Money out of thin air.

3.9 Clause: dummy spend handling

If v=0v = 0, the Merkle path check is skipped: a dummy spend does not correspond to a real note in the tree. The circuit implements this by computing the Merkle output and a "dummy override" output, then conditionally selecting between them based on v=0v = 0. The override does not set the anchor to a free choice; the dummy clause makes the Merkle-path computation a no-op while all other clauses still hold. The effect: when v=0v = 0, the cm\mathsf{cm} may be any well-formed commitment but no anchor membership is claimed.

Implemented as an if-then-else over boolean multiplexing in bellman gadgets.

Attack on omission: without the dummy mechanism, every spend revealed the bundle's true input count, leaking metadata.

3.10 Clause: ρ=MixingPedersenHash(cm,pos)\rho = \mathsf{MixingPedersenHash}(\mathsf{cm}, \text{pos})

ρ\rho is computed in-circuit:

ρ  =  cm  +  [pos]Gρ.\rho \;=\; \mathsf{cm} \;+\; [\text{pos}]\,G_\rho.

For pos\text{pos} bounded by 2322^{32}, the scalar mul is cheap (~100 constraints). Plus one Edwards add (~6 constraints).

Total: ~110 constraints.

3.11 Clause: nf=PRFnknfSapling(ρ)\mathsf{nf} = \mathsf{PRF}^{\mathsf{nfSapling}}_{\mathsf{nk}}(\rho)

The PRF is BLAKE2s with key nk\mathsf{nk} and input ρ\rho. Same BLAKE2s gadget as CRHivk\mathsf{CRH}^{\mathsf{ivk}}.

Constraint cost: ~16,000.

The public output is nf\mathsf{nf}.

Attack on omission: the nullifier could be arbitrary, allowing double-spends.

3.12 Clause: ValueCommitment

cv=[v]V+[rcv]R\mathsf{cv} = [v]\,V + [\mathsf{rcv}]\,R.

Fixed-base scalar mul on VV with the 64-bit vv (~250 constraints) plus fixed-base on RR with 252-bit rcv\mathsf{rcv} (~750).

Constraint cost: ~1000.

The result is checked against the public cv\mathsf{cv}.

Attack on omission: the published cv\mathsf{cv} could fail to commit to vv, breaking the binding-signature equation and allowing value forgery.

3.13 Clause: 64-bit range check on vv

The boolean decomposition of vv is into exactly 64 bits, enforced by 64 boolean constraints during witnessing.

Attack on omission: a value >264> 2^{64} would overflow the binding-signature value-balance accumulator, allowing implicit value forgery.

3.14 Sapling Spend total

The Sapling Spend circuit has approximately 150,000 constraints in current implementations:

ClauseConstraints
Witness encoding~12,000
Subgroup-check gadgets~6,000
nk\mathsf{nk} derivation~750
CRHivk\mathsf{CRH}^{\mathsf{ivk}} (BLAKE2s)~16,000
pkd\mathsf{pk}_d check~3,000
rk\mathsf{rk} check~760
NoteCommitment~4,250
Merkle path (32 layers)~96,000
ρ\rho mixing~110
Nullifier PRF~16,000
ValueCommitment~1,000
Range check~64
Misc / linking~few thousand

Numbers from the public sapling-crypto code, subject to change with optimisations.

3.15 Sapling Output statement

From chapter 04: 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=NoteCommitrcm(gd,pkd,v)\mathsf{cm} = \mathsf{NoteCommit}^{\mathsf{rcm}}(g_d, \mathsf{pk}_d, v), with cmu\mathsf{cm}^u as the public output.
  2. cv=[v]V+[rcv]R\mathsf{cv} = [v]\,V + [\mathsf{rcv}]\,R (public).
  3. epk=[esk]gd\mathsf{epk} = [\mathsf{esk}]\,g_d (public).
  4. v[0,264)v \in [0, 2^{64}).
  5. gdg_d is a valid prime-order subgroup element (non-zero).

Sub-circuit costs:

ClauseConstraints
Witness encoding~6,000
Subgroup check on gdg_d~750
NoteCommitment~4,250
Extract uu-coordinate~5
ValueCommitment~1,000
epk\mathsf{epk} scalar mul~3,000
Range check vv64
Misc~few thousand

Total: ~20,000 constraints. The Output circuit is much cheaper than Spend (no Merkle path, no nullifier).

Clause: subgroup check on gdg_d

The circuit asserts that the witnessed gdg_d is in the prime- order subgroup. For Jubjub this requires checking [J]gd=O[\ell_J]\,g_d = \mathcal{O}, which is expensive but unavoidable.

In practice the implementation uses an implicit subgroup check: gd=DiversifyHash(d)g_d = \mathsf{DiversifyHash}(d) is computed by the sender via cofactor multiplication outside the circuit; inside the circuit the prover proves gdOg_d \neq \mathcal{O} (one non-zero check) and the rest of the structure relies on the subgroup membership being witnessed honestly. The canonical encoding of gdg_d in the encrypted note plaintext, combined with the recipient's re- derivation gd=DiversifyHash(d)g_d = \mathsf{DiversifyHash}(d) at decryption time, catches non-subgroup gdg_d from the recipient side. This is the kind of subtlety chapter 13 warns about; reading the circuit code is essential.

Clause: epk=[esk]gd\mathsf{epk} = [\mathsf{esk}]\,g_d

Variable-base scalar mul. ~3000 constraints.

Attack on omission: the published epk\mathsf{epk} could be unrelated to esk\mathsf{esk}, breaking note-encryption recovery for the sender (via ovk\mathsf{ovk}).

Why no anchor or nullifier

An Output creates value; it does not prove the new note is in the tree (it adds itself) and does not have a nullifier (it has not been spent).

3.16 Orchard Action statement

The Action circuit unifies Spend and Output. From chapter 05: for each Action, the prover knows:

  • Old note: (vold,gdold,pkdold,ρold,ψold,rcmold,auth-path,ak,nk,rivk,α)(v_{\text{old}}, g_d^{\text{old}}, \mathsf{pk}_d^{\text{old}}, \rho^{\text{old}}, \psi^{\text{old}}, \mathsf{rcm}^{\text{old}}, \text{auth-path}, \mathsf{ak}, \mathsf{nk}, \mathsf{rivk}, \alpha).
  • New note: (vnew,gdnew,pkdnew,ψnew,rcmnew)(v_{\text{new}}, g_d^{\text{new}}, \mathsf{pk}_d^{\text{new}}, \psi^{\text{new}}, \mathsf{rcm}^{\text{new}}).

Such that:

  1. If spends enabled: the old commitment is in the tree at the public anchor.
  2. ρnew=nfold\rho^{\text{new}} = \mathsf{nf}^{\text{old}} - the new note's ρ\rho chains from the spent nullifier.
  3. Nullifier formula yields public nf\mathsf{nf}.
  4. rk=ak+[α]Gak\mathsf{rk} = \mathsf{ak} + [\alpha]\,G^{\mathsf{ak}} (public).
  5. cmnew=NoteCommitrcmnew()\mathsf{cm}^{\text{new}} = \mathsf{NoteCommit}^{\mathsf{rcm}^{\text{new}}}(\ldots) matches public cmx\mathsf{cmx}.
  6. cvnet=[voldvnew]V+[rcv]R\mathsf{cv}^{\text{net}} = [v_{\text{old}} - v_{\text{new}}]\,V + [\mathsf{rcv}]\,R (public).
  7. If outputs enabled: epk=[esk]gdnew\mathsf{epk} = [\mathsf{esk}]\,g_d^{\text{new}}.
  8. vold,vnew[0,264)v_{\text{old}}, v_{\text{new}} \in [0, 2^{64}).
  9. ivk=Extract(SinsemillaCommitrivk(ak,nk))\mathsf{ivk} = \mathsf{Extract}( \mathsf{SinsemillaCommit}^{\mathsf{rivk}}( \mathsf{ak}, \mathsf{nk})) and pkdold=[ivk]gdold\mathsf{pk}_d^{\text{old}} = [\mathsf{ivk}]\,g_d^{\text{old}}.

Public inputs per Action: anchor\mathsf{anchor}, cvnet\mathsf{cv}^{\text{net}}, nf\mathsf{nf}, rk\mathsf{rk}, cmx\mathsf{cmx}, epk\mathsf{epk}, plus the two flag bits.

3.17 Halo 2 column layout

The Orchard circuit uses ~10 advice columns over a 2112^{11}-row domain. Each row is a small piece of computation; together the rows realise the full statement.

Custom gate groups (approximate):

  • q_ecc_add, q_ecc_double: Pallas point arithmetic.
  • q_sinsemilla: Sinsemilla chain steps.
  • q_poseidon: Poseidon hash steps.
  • q_lookup_range: range checks via lookup.
  • q_lookup_sinsemilla_S: Sinsemilla 10-bit chunk to point.
  • q_decomposition: bit-decomposition gates.
  • q_constraints: high-level equality gates.

Each gate is a polynomial identity over advice columns, gated by a selector.

3.18 Sinsemilla in-circuit

For the Note Commitment, the prover uses Sinsemilla:

  1. Bit-decompose the input (vv, gdg_d, pkd\mathsf{pk}_d, ρ\rho, ψ\psi) into 10-bit chunks.
  2. Each chunk is looked up in the Sinsemilla generator table: (chunk,S(chunk))(\text{chunk}, S(\text{chunk})).
  3. Iteratively combine via the incomplete-addition gate.
  4. After all chunks are processed, add the rcm\mathsf{rcm} blinding.

The Sinsemilla path costs ~300 rows for typical inputs.

Pitfall: incomplete addition fails when its operands coincide (chapter 13). The circuit must prove the operands are distinct, typically by witnessing intermediate accumulator values and asserting non-equality in a gate.

3.19 Merkle path with Sinsemilla

Each of the 32 Merkle layers uses one Sinsemilla hash of (layer index, left, right). The layer index is part of the personalisation DMH,D_{\text{MH}, \ell} encoded as a Sinsemilla domain.

Cost per layer: ~150 rows. Times 32: ~4,800 rows.

3.20 The ρnew=nfold\rho^{\text{new}} = \mathsf{nf}^{\text{old}} trick

The novel Orchard idea: after computing nfold\mathsf{nf}^{\text{old}}, the circuit feeds it as ρnew\rho^{\text{new}} into the new note commitment. This eliminates the need for an extra Pedersen-hash- based position-mix as in Sapling. The new note's ρ\rho is fully determined by the Action's inputs; the prover cannot choose it freely.

Implementation: an explicit copy constraint from "the row that outputs nfold\mathsf{nf}^{\text{old}}" to "the row that takes ρnew\rho^{\text{new}} as input".

3.21 Nullifier derivation

The Orchard nullifier:

nf  =  Extract([Hash(nk,ρold)+ψold]Knf  +  cmold),\mathsf{nf} \;=\; \mathsf{Extract}\bigl( [\mathsf{Hash}(\mathsf{nk}, \rho^{\text{old}}) + \psi^{\text{old}}]\,K_{\text{nf}} \;+\; \mathsf{cm}^{\text{old}}\bigr),

with Hash\mathsf{Hash} a Poseidon-based PRF keyed by nk\mathsf{nk}.

Sub-circuit: ~200 rows for Poseidon, ~100 for the scalar mul, ~50 for the addition and extract.

3.22 Value commitment (net)

Net value: vnet=voldvnewv^{\text{net}} = v_{\text{old}} - v_{\text{new}}, range-checked to lie in [(2641),2641][-(2^{64} - 1), 2^{64} - 1]. Then

cvnet=[vnet]VOrch+[rcv]ROrch.\mathsf{cv}^{\text{net}} = [v^{\text{net}}]\,V_{\text{Orch}} + [\mathsf{rcv}]\,R_{\text{Orch}}.

In-circuit cost: ~150 rows.

3.23 CommitIvk in-circuit

The Orchard incoming viewing key is derived inside the circuit:

ivk  =  Extract(SinsemillaCommitrivk(ak,nk)).\mathsf{ivk} \;=\; \mathsf{Extract}(\mathsf{SinsemillaCommit}^{\mathsf{rivk}}( \mathsf{ak}, \mathsf{nk})).

The Sinsemilla commit is one Sinsemilla hash (over the encoded ak,nk\mathsf{ak}, \mathsf{nk}) plus a randomness term [rivk]Rivk[\mathsf{rivk}]\,R_{\mathsf{ivk}}. Cost: ~300 rows.

3.24 Flag-conditional logic

Bundle flags determine whether spends and outputs are enabled per Action. If spends are disabled, the Merkle path check and nullifier publication are no-op'd (specific dummy values substituted). If outputs are disabled, the new note commitment and epk\mathsf{epk} are dummy.

In-circuit: each clause is multiplied by a flag bit, and a dummy-substitution gadget produces the public-input value when the flag is off. This is more complex than Sapling's "dummy when v=0v = 0" because Orchard allows mixed-mode Actions (only spend, only output, or both).

3.25 Orchard Action total

Approximate row counts per Action (out of 211=20482^{11} = 2048 rows in the domain):

ClauseRows
Witnessing + decomposition~200
Sinsemilla note commitment~300
Merkle path (32 layers)~4,800
Nullifier~300
rk\mathsf{rk} check~100
epk\mathsf{epk} check~100
Net value commitment~150
CommitIvk~300
pkd\mathsf{pk}_d check~200
Flag conditional logic~50

Total: ~6,500 rows per Action. With nn Actions, the circuit is sized to fit (typically k=11k = 11 for 2-action bundles, k=12k = 12 for larger).

3.26 Comparison

Sapling SpendSapling OutputOrchard Action
Constraint modelR1CSR1CSPLONKish
Approx size150k constraints20k constraints6.5k rows per action
Includes spend?yesnoyes (or dummy)
Includes output?noyesyes (or dummy)
Includes Merkle path?yesnoyes
Prover time (single)~2 s~0.2 s~1 s for bundle

3.27 Why each clause is necessary

The "attack on omission" notes throughout this chapter all reduce to one of:

  • Money forgery: prove a non-existent commitment as spent; prove a value larger than the input.
  • Double-spend: produce different nullifiers for the same note.
  • Identity theft: spend a note whose recipient was not the prover.
  • Metadata leak: distinguish dummy from real spends.

Every clause in the circuits maps to one of these. If you cannot articulate which attack a clause prevents, the clause is suspect.

3.28 Reading the actual circuit code

Sapling (in sapling-crypto)

  • sapling-crypto::circuit: top-level circuit definition.
  • sapling-crypto::circuit::spend: the Spend circuit synthesis.
  • sapling-crypto::circuit::output: the Output circuit.
  • sapling-crypto::circuit::pedersen_hash: the Pedersen hash gadget.
  • sapling-crypto::circuit::merkle: the Merkle path gadget.

This workspace's Sapling integration includes the Sprout circuit in zcash_proofs/src/circuit/sprout:

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

Orchard (in orchard)

  • orchard::circuit: top-level Circuit::synthesize.
  • orchard::circuit::note_commit: note commitment gadget.
  • orchard::circuit::commit_ivk: CommitIvk\mathsf{CommitIvk}.
  • orchard::circuit::value_commit_orchard: value commitment.
  • orchard::circuit::derive_nullifier: nullifier.
  • orchard::circuit::gadget::sinsemilla: Sinsemilla gadgets.
  • orchard::circuit::gadget::ecc: Pallas EC gadgets.

The proof artifacts are loaded by zcash_proofs/src/prover.rs.

3.29 The verifier's view

The Sapling Spend verifier checks one Groth16 pairing equation (chapter 04, section 3.7). Public inputs:

  • rk\mathsf{rk} (2 field elements).
  • cv\mathsf{cv} (2 field elements).
  • nf\mathsf{nf} (1 packed field element).
  • anchor\mathsf{anchor} (1 field element).

The encoding is fixed and must match the circuit's expected order. If you change the public-input order, you must rerun the trusted setup.

For Orchard, the verifier runs Halo 2's verifier, ~10 ms vs ~7 ms for Groth16, with no trusted setup.

4. Failure modes

See "Attack on omission" notes in each clause above and the consolidated list in section 3.27. Additional circuit-author mistakes from audit findings and informal lore:

  • Underconstrained advice cells: an advice cell with no gate forcing its value can be set arbitrarily by the prover. Verify every advice cell is used in at least one constraint with a selector on.
  • Off-by-one selector: a selector that is on at row ii but whose gate references row i1i - 1 can subtly misalign.
  • Incomplete-addition coincidence: as noted, both inputs must be provably distinct.
  • Lookup-table collision: two distinct chunks mapping to the same point break the lookup soundness.
  • Public-input ordering: prover and verifier must agree on which public input maps to which constraint. A swap is invisible in tests until a real attack exploits it.

5. Spec pointers

6. Exercises

  1. Match a clause to a function. For each clause in section 3.3 through 3.13 (Sapling Spend), open the corresponding file in sapling-crypto/src/circuit/ and find the function that implements it. Record the file and the function name.
  2. Count constraints for a smaller circuit. Run the bellman constraint counter on the Sapling Spend circuit (see sapling-crypto's test harness) and compare the actual constraint count to the table in section 3.14. State any discrepancy.
  3. Add an Orchard test vector. In a checkout, add a test vector under orchard/src/test_vectors/ (external repo) that exercises an Action with the spend flag off. Verify the Circuit::synthesize path runs without panicking.
  4. Trace public inputs. For a Sapling Spend proof, list the public inputs in the order the verifier expects them. Cite the file and function in sapling-crypto that defines the ordering. Argue why swapping any two would constitute a consensus break.

Answers in the code

7. Further reading

  • chapter 17: the Halo 2 proof system mechanics that underpin sections 3.16 through 3.25.
  • chapter 16: the Pedersen hash gadget central to the Sapling circuit.
  • chapter 23: the keying material that each clause consumes as witness or produces as public output.
  • Trail of Bits, Orchard / Halo 2 audit reports: source of several "attack on omission" framings.