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
with the wires (witness vector plus a constant) and
public coefficients. Sapling Spend uses
approximately such constraints in
sapling-crypto's
circuit. The toolkit is bellman.
Definition (PLONKish / Halo 2). A table of cells with custom gates. Each row may impose constraints of the form
where is the selector polynomial that activates
the gate. Permutation arguments encode copy constraints, and
lookups enforce table membership. The Orchard Action circuit uses
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, ). The Jubjub- based windowed-multiplication hash defined in chapter 16 and used across the Sapling circuit. Constraint cost is approximately constraints per input bit.
Definition (Sinsemilla). The Pallas-based chunk-and-add hash used in Orchard. Each -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 such that:
- .
- The Merkle path from at leads to the public anchor.
- with and .
- (public).
- (public).
- (public ).
- .
- Either (dummy spend, Merkle check skipped) or the Merkle path is checked.
Public inputs: , , , .
The verifying-key hash for this circuit is pinned in
zcash_proofs/src/lib.rs:
loading...
3.2 Sapling Spend witness allocation
Approximate sub-circuit costs (R1CS constraints):
| Witness | Bits | Constraints |
|---|---|---|
| 64 | 64 boolean + 1 packing | |
| 256 (encoded as bits) | ~750 | |
| 256 | ~750 | |
| 252 | 252 boolean | |
| 252 | 252 boolean | |
| 256 | ~750 | |
| 252 | 252 boolean | |
| auth-path | ~8000 bit constraints |
The witness alone is ~12,000 constraints before any checks run.
3.3 Clause:
Scalar mul of a fixed generator by a 252-bit secret. Using the
fixed-base windowed comb gadget in sapling-crypto:
- 252 bits of 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 unrelated to lets the spender choose post-hoc, allowing nullifier prediction for unspent notes or forged nullifier collisions.
3.4 Clause:
is BLAKE2s-256 of the concatenated encodings of and (as -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 to produce . The reduction is a controlled bit-truncation rather than a full modular reduction, valid because and the top bits are zeroed.
Constraint cost: ~16,000.
Attack on omission: an unmoored would let the prover claim ownership of arbitrary they do not control.
3.5 Clause:
Variable-base scalar mul: 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 they do not own.
3.6 Clause:
Fixed-base scalar mul (~750 constraints) plus one Edwards addition (~6 constraints), then equality with the public .
Constraint cost: ~760.
Attack on omission: the published would not be a re-randomisation of the actual . An attacker could sign with their own key under their own while spending a victim's note.
3.7 Clause: NoteCommitment
The Pedersen hash gadget (chapter 16) is the most heavily used sub-circuit. For an input of bits:
- ~6 constraints per bit, so ~3500 for the hash.
- Plus the randomness term: 252-bit fixed-base scalar mul on , ~750 constraints.
Constraint cost: ~4250.
Attack on omission: a prover that picks freely can spend a note that does not exist.
3.8 Clause: Merkle path
For each layer :
- Witness the auth-path sibling at this layer and a boolean indicating "is the current node the left or right child".
- Compute where left/right are conditionally swapped based on the bit.
- Use the result as the current node at layer .
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 they invented, without it being in the tree. Money out of thin air.
3.9 Clause: dummy spend handling
If , 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 . 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 , the 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:
is computed in-circuit:
For bounded by , the scalar mul is cheap (~100 constraints). Plus one Edwards add (~6 constraints).
Total: ~110 constraints.
3.11 Clause:
The PRF is BLAKE2s with key and input . Same BLAKE2s gadget as .
Constraint cost: ~16,000.
The public output is .
Attack on omission: the nullifier could be arbitrary, allowing double-spends.
3.12 Clause: ValueCommitment
.
Fixed-base scalar mul on with the 64-bit (~250 constraints) plus fixed-base on with 252-bit (~750).
Constraint cost: ~1000.
The result is checked against the public .
Attack on omission: the published could fail to commit to , breaking the binding-signature equation and allowing value forgery.
3.13 Clause: 64-bit range check on
The boolean decomposition of is into exactly 64 bits, enforced by 64 boolean constraints during witnessing.
Attack on omission: a value 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:
| Clause | Constraints |
|---|---|
| Witness encoding | ~12,000 |
| Subgroup-check gadgets | ~6,000 |
| derivation | ~750 |
| (BLAKE2s) | ~16,000 |
| check | ~3,000 |
| check | ~760 |
| NoteCommitment | ~4,250 |
| Merkle path (32 layers) | ~96,000 |
| 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 such that:
- , with as the public output.
- (public).
- (public).
- .
- is a valid prime-order subgroup element (non-zero).
Sub-circuit costs:
| Clause | Constraints |
|---|---|
| Witness encoding | ~6,000 |
| Subgroup check on | ~750 |
| NoteCommitment | ~4,250 |
| Extract -coordinate | ~5 |
| ValueCommitment | ~1,000 |
| scalar mul | ~3,000 |
| Range check | 64 |
| Misc | ~few thousand |
Total: ~20,000 constraints. The Output circuit is much cheaper than Spend (no Merkle path, no nullifier).
Clause: subgroup check on
The circuit asserts that the witnessed is in the prime- order subgroup. For Jubjub this requires checking , which is expensive but unavoidable.
In practice the implementation uses an implicit subgroup check: is computed by the sender via cofactor multiplication outside the circuit; inside the circuit the prover proves (one non-zero check) and the rest of the structure relies on the subgroup membership being witnessed honestly. The canonical encoding of in the encrypted note plaintext, combined with the recipient's re- derivation at decryption time, catches non-subgroup from the recipient side. This is the kind of subtlety chapter 13 warns about; reading the circuit code is essential.
Clause:
Variable-base scalar mul. ~3000 constraints.
Attack on omission: the published could be unrelated to , breaking note-encryption recovery for the sender (via ).
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: .
- New note: .
Such that:
- If spends enabled: the old commitment is in the tree at the public anchor.
- - the new note's chains from the spent nullifier.
- Nullifier formula yields public .
- (public).
- matches public .
- (public).
- If outputs enabled: .
- .
- and .
Public inputs per Action: , , , , , , plus the two flag bits.
3.17 Halo 2 column layout
The Orchard circuit uses ~10 advice columns over a -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:
- Bit-decompose the input (, , , , ) into 10-bit chunks.
- Each chunk is looked up in the Sinsemilla generator table: .
- Iteratively combine via the incomplete-addition gate.
- After all chunks are processed, add the 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 encoded as a Sinsemilla domain.
Cost per layer: ~150 rows. Times 32: ~4,800 rows.
3.20 The trick
The novel Orchard idea: after computing , the circuit feeds it as into the new note commitment. This eliminates the need for an extra Pedersen-hash- based position-mix as in Sapling. The new note's is fully determined by the Action's inputs; the prover cannot choose it freely.
Implementation: an explicit copy constraint from "the row that outputs " to "the row that takes as input".
3.21 Nullifier derivation
The Orchard nullifier:
with a Poseidon-based PRF keyed by .
Sub-circuit: ~200 rows for Poseidon, ~100 for the scalar mul, ~50 for the addition and extract.
3.22 Value commitment (net)
Net value: , range-checked to lie in . Then
In-circuit cost: ~150 rows.
3.23 CommitIvk in-circuit
The Orchard incoming viewing key is derived inside the circuit:
The Sinsemilla commit is one Sinsemilla hash (over the encoded ) plus a randomness term . 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 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 " because Orchard allows mixed-mode Actions (only spend, only output, or both).
3.25 Orchard Action total
Approximate row counts per Action (out of rows in the domain):
| Clause | Rows |
|---|---|
| Witnessing + decomposition | ~200 |
| Sinsemilla note commitment | ~300 |
| Merkle path (32 layers) | ~4,800 |
| Nullifier | ~300 |
| check | ~100 |
| check | ~100 |
| Net value commitment | ~150 |
| CommitIvk | ~300 |
| check | ~200 |
| Flag conditional logic | ~50 |
Total: ~6,500 rows per Action. With Actions, the circuit is sized to fit (typically for 2-action bundles, for larger).
3.26 Comparison
| Sapling Spend | Sapling Output | Orchard Action | |
|---|---|---|---|
| Constraint model | R1CS | R1CS | PLONKish |
| Approx size | 150k constraints | 20k constraints | 6.5k rows per action |
| Includes spend? | yes | no | yes (or dummy) |
| Includes output? | no | yes | yes (or dummy) |
| Includes Merkle path? | yes | no | yes |
| 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:
loading...
Orchard (in orchard)
orchard::circuit: top-levelCircuit::synthesize.orchard::circuit::note_commit: note commitment gadget.orchard::circuit::commit_ivk: .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:
- (2 field elements).
- (2 field elements).
- (1 packed field element).
- (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 but whose gate references row 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
- Zcash Protocol Specification, section 4.8 (Spend statement): the normative Spend statement that section 3.1 paraphrases.
- Zcash Protocol Specification, section 4.9 (Output statement): the normative Output statement.
- Zcash Protocol Specification, section 4.13 (Action statement): the normative Orchard Action statement.
- ZIP 224: the Orchard Action semantics and key derivation.
- Halo paper: the proof system underpinning the Orchard verifier.
- Sinsemilla note: the chunk-and-add hash used in Orchard, defined in the protocol specification appendix.
6. Exercises
- 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. - Count constraints for a smaller circuit. Run the
bellmanconstraint counter on the Sapling Spend circuit (seesapling-crypto's test harness) and compare the actual constraint count to the table in section 3.14. State any discrepancy. - 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 theCircuit::synthesizepath runs without panicking. - 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-cryptothat defines the ordering. Argue why swapping any two would constitute a consensus break.
Answers in the code
- Sprout circuit top-level types:
zcash_proofs/src/circuit/sprout/mod.rs#L25-L54. - Sapling verifying-key hashes:
zcash_proofs/src/lib.rs#L40-L60. - Prover interface (Sapling Spend and Output, Orchard):
zcash_proofs/src/prover.rs. - The external circuit code lives at
sapling-cryptoandorchard.
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.