Skip to main content

Halo 2 Primer

1. Why This Chapter Exists

The Orchard Action circuit is a Halo 2 circuit. A reader who has only seen R1CS-style systems will not be able to navigate src/circuit.rs without first learning what advice columns, selectors, custom gates, and lookups are. By the end the reader can read a Halo 2 Config struct and predict the column layout it will produce.

2. Definitions

Definition 2.1 (PLONKish Table)

A Halo 2 circuit is a rectangular table with 2K2^K rows and a fixed number of columns of four kinds:

  • advice columns A0,,Aa1A_0, \dots, A_{a-1}, populated by the prover;
  • fixed columns F0,,Ff1F_0, \dots, F_{f-1}, populated by the verifier during trusted preprocessing;
  • instance columns I0,,Ii1I_0, \dots, I_{i-1}, the public inputs;
  • selector columns S0,,Ss1S_0, \dots, S_{s-1}, fixed columns that toggle constraints on or off per row.

Definition 2.2 (Custom Gate)

A custom gate is a multivariate polynomial gFq[X0,,Xm1]g \in \mathbb{F}_q[X_0, \dots, X_{m-1}] together with a selector SgS_g. For every row rr,

Sg(r)g(c0(r),c1(r),,cm1(r))=0,S_g(r) \cdot g\big(c_0(r), c_1(r), \dots, c_{m-1}(r)\big) = 0,

where each cj(r)c_j(r) is some column at some row r+δjr + \delta_j. The offsets δj{1,0,+1}\delta_j \in \{-1, 0, +1\} are rotations and let gates couple adjacent rows.

Definition 2.3 (Lookup)

A lookup argument requires that for every row rr, the tuple (c0(r),,cm1(r))\big(c_0(r), \dots, c_{m-1}(r)\big) appears as a row of a fixed table TFqmT \subset \mathbb{F}_q^m. Lookups encode range checks (T={(0),(1),,(2k1)}T = \{(0), (1), \dots, (2^k - 1)\}) and the Sinsemilla windowed multiplication table.

Definition 2.4 (Inner-Product Argument Commitment)

A column cc of length 2K2^K is committed as Comm(c)=i=02K1ciGi\mathrm{Comm}(c) = \sum_{i = 0}^{2^K - 1} c_i \, G_i for a fixed basis {Gi}G\{G_i\} \subset \mathbb{G}. The IPA protocol opens Comm(c)\mathrm{Comm}(c) at a point ζ\zeta in O(log2K)O(\log 2^K) rounds with no trusted setup. In Orchard, G\mathbb{G} is the Vesta curve.

Definition 2.5 (Transcript)

The Fiat-Shamir transcript is a Blake2b instance personalised with "Halo2-Transcript". Every commitment and challenge is absorbed in protocol order. The outer transcript is not recursive in Orchard. The personalisation is set in the Blake2bRead::init and Blake2bWrite::init constructors of halo2_proofs, both pinned to the halo2_proofs-0.3.0 tag that Orchard 0.13.1 depends on:

halo2_proofs/src/transcript.rs (Blake2bRead::init)
loading...
halo2_proofs/src/transcript.rs (Blake2bWrite::init)
loading...

3. The Code

3.1 The Halo 2 Imports

src/circuit.rs
loading...

The plonk re-exports name the column kinds, the Constraints builder, the Expression type used in gates, and the verifier choices (SingleVerifier, BatchVerifier). The transcript re-exports give Blake2b read/write halves.

3.2 The Chip / Gadget Pattern

A chip is a Config (column layout, gate definitions) plus the synthesis code that populates a region of the table. A gadget is a higher-level construction composed of one or more chips. Orchard pulls the ECC and Sinsemilla chips from halo2_gadgets and adds its own Orchard-specific chips: CommitIvkChip and NoteCommitChip.

Example. Inside the Action circuit's Circuit::configure, each chip is constructed by calling its associated configure function with the columns and helpers it needs. The ECC chip is representative:

src/circuit.rs (ECC chip configuration)
loading...

The call returns a typed EccConfig that records which advice, fixed, and lookup columns the chip owns. That EccConfig is stored inside the Action circuit's Config struct and later handed to EccChip::construct(config) during synthesis, so every region that uses curve arithmetic shares the same column layout. The Sinsemilla, Poseidon, and Merkle chips just below the ECC call in the same function follow exactly the same pattern.

3.3 The K Constant

The Action circuit uses K=11K = 11, giving 2K=20482^K = 2048 rows in the PLONKish table. The constant lives at the top of src/circuit.rs:

src/circuit.rs (K constant)
loading...

2K2^K is the row count of the table; larger KK admits more constraints but doubles the prover time per increment. The same value appears at the top of the pinned circuit description, which serialises the verifier key produced from this K:

src/circuit_description (k: 11, extended_k: 14)
loading...

3.4 The Lookup Table

The Sinsemilla windowed multiplication uses a fixed lookup table populated from src/constants/sinsemilla.rs. The table has 2102^{10} rows, one per ten-bit window value, and maps the window to the precomputed generator multiple.

4. Failure Modes

  • Region overlap. Chips synthesise into regions. Two chips with overlapping selectors silently corrupt each other; the reviewer must trace the region offsets.
  • Constraint not gated. A custom gate that lacks a selector multiplier applies to every row, including padding rows whose advice columns are zero. The Halo 2 dev-mode prover catches this; production proving silently produces a wrong proof.
  • Wrong K. Setting K too small causes Error::NotEnoughRows; too large is silent and wastes prover time. The circuit_description snapshot pins the chosen K.
  • Transcript divergence between prover and verifier. If a field element is absorbed in a different order or under a different personalisation string, verification fails. This is the most common cause of a "passes locally, fails in CI" report after a Halo 2 dependency bump.

5. Spec Pointers

  • PLONK paper: the underlying argument.
  • Halo paper: the IPA-based recursion that motivated Halo 2.
  • Halo 2 Book: the canonical reference for the column model, gates, and lookups.
  • zcash/halo2: the source of halo2_proofs and halo2_gadgets.

6. Exercises

  1. Open src/circuit.rs and find the K constant. State its value and explain how 2^K relates to the constraint count.
  2. List every meta.advice_column() and meta.fixed_column() call in src/circuit.rs. Group them by the chip that consumes them.
  3. Code task. Add a new advice column to the Action circuit without consuming it (a dead column). Run cargo test --lib circuit::tests. Confirm the dev-mode prover rejects the change with an "unused column" lint, then revert.

7. Further Reading