Anatomy of a halo 2 Orchard proof

The 4992 bytes the Orchard simulator emits are 156 group-element or scalar slots of 32 bytes each. Halo 2 emits them in a fixed order determined by the PLONKish protocol structure. Fiat-Shamir challenges (theta, beta, gamma, y, x, x_1, x_2, x_3, x_4) are not written; the verifier re-derives them by hashing the transcript so far. The structure below is for a one-Action Orchard proof; multi-Action bundles multiply most regions by the number of actions.

  1. Advice commitments. One curve point per advice column. Orchard ships around ten advice columns; each is a Pedersen commitment to that column's polynomial. Source: halo2_proofs::plonk::prover::create_proof.
  2. Lookup permuted commitments. Two curve points per lookup argument (A' and S'). Orchard uses several lookups for table-driven range checks.
  3. Permutation Z + lookup Z. Permutation argument running products, chunked by max_degree (the Orchard PK uses three chunks), plus one Z per lookup.
  4. Vanishing h pieces. The quotient polynomial h(X), split into chunks of degree ≤ n − 1.
  5. Evaluations at x. One scalar per (column, rotation) pair: advice, fixed, permutation, lookup, h piece. This is where the gate equation balance is checked.
  6. Multipoint reduction. A single batched IPA at point x_3 combines openings at all rotation points using random-linear-combination challenge x_4.
  7. IPA tail. k = log2(n) rounds of (Li, Ri) curve points, one final scalar a, one final blinder. For Orchard's n = 211 polynomial, that's 11 IPA rounds plus two final scalars.

SNARK families compared

Halo 2's proof size is the largest in the well-known SNARK families. The tradeoff is no trusted setup: every other line below requires a multi-party ceremony whose output, if compromised, lets an adversary forge proofs forever. Halo 2's Pasta cycle plus the inner-product argument gets transparency at the cost of larger proofs and slower verification.

SystemCurveProof sizeVerifyTrusted setup
Groth16BN254 192 B~3 ms per-circuit
PLONK (KZG)BN254 ~480 B~10 ms universal
BulletproofsRistretto ~1.3 KBlinear in n none
Halo 2 (Orchard)Pasta (vesta) 4992 B~30 ms none
STARK (FRI)field-only ~40 to 250 KB~10 to 100 ms none

The 4992 bytes per Action is also why Orchard transactions are large on chain: a single shielded transaction with multiple actions can run to tens of kilobytes just for the proofs. That cost is what buys Zcash a shielded pool that needs no trusted setup ceremony. Sapling, the previous Zcash shielded pool, uses Groth16 and required a multi-party setup ceremony; if any one participant kept their secret randomness, that participant could forge unlimited Sapling proofs. Orchard's Halo 2 design eliminates that attack surface entirely at the cost of larger, slower-to-verify proofs.

Orchard terminology

Terms used throughout the Orchard page. The Zcash protocol specification defines them across §3 (Concepts) and §4 (Abstract Protocol); the Action ZK relation that ties them together is section 4.18.4.

anchor
The root of the Orchard note-commitment Merkle tree at the moment the prover constructed the spend. A consensus node accepts the proof only if this root appears in a recent enough on-chain block, which is what prevents proving spends against a non-existent tree state.
cv_net
The Pedersen value commitment to the net of (spend value) minus (output value), blinded by a uniformly sampled trapdoor rcv. The binding signature later proves the prover knew the total trapdoor across all Actions, which is how Orchard enforces value balance without revealing it.
nf_old
The nullifier: nf = ExtractP([PRFnfOrchardnk(ρ) + ψ]·KOrchard + cm). A deterministic function of the spent note plus the spender's viewing key: Poseidon-based PRF on ρ, blinded by ψ, fixed-base scalar mul of KOrchard, point-add with the note commitment, take the x-coordinate. Adding the result to the Orchard nullifier set prevents double-spending. Without the viewing key no outside observer can link the nullifier to the spent note; with it, the spender can prove the link.
rk
The spend-authorization verification key (ak) randomized by a per-spend trapdoor α. The randomization unlinks this Action's signature from the spending key's underlying public key while still letting the spender sign with the corresponding randomized signing key.
cmx
The output note's commitment, x-coordinate only. The Sinsemilla-derived note commitment is a Pasta curve point; Zcash transactions carry only the x-coordinate to save space and the verifier reconstructs y.
Sinsemilla
A SNARK-friendly hash function used in Orchard for note commitments and the Merkle tree. Designed for a small circuit footprint when proved inside Halo 2.
Poseidon
The other SNARK-friendly hash used in Orchard, for the nullifier derivation. Optimised for low arithmetic-constraint cost.
RedPallas
The signature scheme over the Pallas curve used for both the binding signature and the per-Action spend-auth signature. Rerandomisable (the rk mechanism), compatible with batch verification.
FullViewingKey
The capability to see all incoming and outgoing notes for a given spending key, without the ability to spend. Decomposes into ak (auth) + nk (nullifier-deriving) + ovk (outgoing-viewing).

Frequently asked questions

What does "without witness" actually mean?

The simulator never has access to a specific spending key, note, or Merkle path that someone wants to keep private. It samples its own arbitrary witness (which happens to be a valid one for some statement) and proves that. Because the Orchard Action relation admits an enormous family of valid witnesses for any given public Instance, an observer who sees only the public Instance cannot tell which witness the prover used. That property is what makes the verifier learn nothing about which specific note was spent.

Why is the proof 4992 bytes when Groth16 proofs are 192 bytes?

Halo 2 is transparent: no per-circuit or universal trusted setup. The construction trades proof size and verification time for that property. Groth16 needs a per-circuit ceremony whose toxic waste, if retained, lets an adversary forge proofs forever. Zcash chose Halo 2 for Orchard specifically to eliminate this attack surface.

Is this the same code Zcash uses on mainnet?

The proof-generation path calls orchard::circuit::Proof::create and Proof::verify directly. Those are the same production wrappers around halo2_proofs::plonk::create_proof and verify_proof that zcashd and zebrad link against. The simulator's contribution is sampling a witness uniformly and driving the production prover with it. A small downstream patch exposes six read-only accessors (ProvingKey::{inner,params}, VerifyingKey::{inner,params}, Instance::to_halo2_instance, SigningMetadata::{alpha,is_dummy}) so a programmable transcript can be plumbed in and external verifiers can build the per-column instance scalars; otherwise the only available path uses Blake2b.

Could I use this to fake a Zcash transaction?

No. The "what would happen on mainnet" section on the Orchard page lists the five non-cryptographic reasons a consensus node would reject it. The simulator breaks no cryptographic property; it relies on the protocol doing more than cryptography for soundness against double-spend.

Why does the simulator return random-looking byte sequences?

That's the whole point. Honest Halo 2 proofs are uniform over the proof byte space (conditional on the public Instance), and the simulator's output is statistically indistinguishable from honest output. If a reader could see a pattern in the bytes, the protocol would leak. The two-proofs-same-witness panel on the Orchard page makes this concrete: two independent proofs of the same statement share roughly half their bytes by coincidence, which is exactly what zero-knowledge requires.

Why does Foundations mention a "byte-level no-witness construction" that isn't here?

For relations with a unique witness, the WI-to-ZK collapse on multi-witness relations does not apply: the simulator can't sample a different witness, so it has no way to make its output transcript distribution match the honest prover's. The only escape is to construct the proof bytes directly from the public statement plus programmed challenges, never sampling a witness in the first place — a byte-level emit that walks the verifier's check equations and solves for each forced value.

For relations like Orchard's, with many valid witnesses per public Instance, the construction is not needed. The simulator samples a uniform witness internally, runs the honest prover with a programmed Fiat-Shamir transcript, and outputs the result. Three facts together close the ZK gap: (i) Pasta-Pedersen polynomial commitments are perfectly hiding under uniform blinders, so the simulator's commitments carry no information about which witness was sampled; (ii) the Fiat-Shamir challenges are programmed rather than hashed from witness-dependent data; (iii) the witness was sampled uniformly from the witness set, so its marginal distribution conditional on the public statement matches the honest prover's. Composed, the simulator's output transcript is computationally indistinguishable from honest in the random-oracle model — the textbook ZK property.

No separate byte-level construction is implemented in this crate, because neither the toy MulCircuit relation (every nonzero a is half of a valid witness) nor the Orchard Action relation (any valid spending-key / note / Merkle-path tuple) is unique-witness. Closing the byte-level path is research-status work whose payoff is for unique-witness relations not deployed here.

References

Foundational papers for the constructions exercised on these pages:

Running it yourself

Every part of these pages is reproducible from a clean checkout. The crate is pure Rust plus a small JS glue layer. The real-Orchard path uses the upstream orchard and halo2_proofs crates plus a small downstream patch that exposes six read-only accessors (ProvingKey::{inner,params}, VerifyingKey::{inner,params}, Instance::to_halo2_instance, SigningMetadata::{alpha,is_dummy}).

Fast tests (about 3 seconds)

cargo test --features orchard

9 tests exercising the IPA, MulCircuit halo2 paths, and the proof byte-structure measurement. None of these run the real Orchard prover (gated under #[ignore] so the default run stays fast).

Real Orchard tests (about 2.5 minutes)

cargo test --features orchard -- --ignored

9 tests driving the production Orchard prover end-to-end on sampled witnesses: single Action verifies, multi-Action bundle verifies, signed bundle verifies, ROM-programmable variant verifies, tampered proof rejected, wrong Instance rejected, arbitrary-value spend verifies, byte distributions uniform.

The single-threaded web demo

PROFILE=release FEATURES=wasm-orchard bash build-wasm.sh
(cd web && python3 serve.py 8000)
xdg-open http://localhost:8000

Builds a ~4 MB single-threaded WASM module and serves the static HTML/JS at localhost:8000. No backend; everything runs in the browser tab.

The parallel web demo (4 to 8 times faster prove)

rustup component add rust-src --toolchain nightly
bash build-wasm-parallel.sh
(cd web && python3 serve.py 8000)
xdg-open http://localhost:8000

Builds a ~9 MB parallel WASM module (~7 MB once shrunk by wasm-opt -O3) via wasm-bindgen-rayon so halo2's FFT and MSM steps inside Proof::create dispatch across a SharedArrayBuffer-backed pool of inner Web Workers. Requires a nightly Rust toolchain and the bundled serve.py, which sets the Cross-Origin-{Opener,Embedder}-Policy headers the browser needs to enable SharedArrayBuffer. The page auto-detects the parallel build at load time and falls back to the single-threaded one if pkg-parallel/ isn't present. Compute remains 100% client-side; the headers are pure browser-security metadata.

The CLI

cargo run --features orchard --release --bin orchard-cli -- --seed 42 --pretty

Runs the same simulator from the command line, no browser. The JSON output matches the schema the Orchard page's "Download proof as JSON" button produces, so a CLI-generated proof and a browser-generated proof can be diffed structurally or pasted into either environment for verification.