AIR Constraints
Write AIR constraints for Hekate zero-knowledge circuits in Rust: polynomials over two rows, selectors, boundaries, gadgets, degree and preflight reports.
On this page
An AIR constraint is a polynomial over two neighbouring rows of the trace that must equal zero on every row. A Hekate program states what a valid trace looks like with constraints, boundaries, fixed columns and buses, and one proof covers all of them.
The example on this page is the circuit of a XOR checksum over private 32-bit words. Execution Trace fills its table, and Verifier Logic proves and verifies it.
§ A constraint reads two rows
1use Config;
2use errors;
3use ;
4use ;
5use ;
6
7pub type F = Block128;
8
9define_columns!
16
17
20
21
define_columns! names the columns and their types, cx.schema adds them to the table, and cols.at returns a
handle for each. cs.col and cs.next read a column by index on the current row and on the next one; the closures
col and next do the same from a handle. constrain_named declares an expression zero on every row and labels it.
The checksum has one constraint, xor_step:
On a row where ACTIVE is 1, the next accumulator is this one XOR this word: addition in a binary field is XOR, and
exactly when . On a row where ACTIVE is 0, the constraint holds whatever the row contains.
The row after the last is row 0. Switch transition constraints off on the last row, as ACTIVE does, or they chain
the last row into the first.
§ Fixed columns, boundaries, public inputs
The rest of build_program pins the table to the statement.
cx.fixmakesACTIVEa fixed column: 1 on the firstwordsrows, 0 after. The verifier computes a fixed column from its shape, and the prover has no say in it; a selector the prover filled in could switch constraints off, the failure a postmortem describes.FixedShapealso offersFirstRow,LastRow,Periodic,Segments,Sparse,DenseandCustom.cx.boundarypinsACCto 0 on row 0.cx.publishpinsACCon the row after the last word to a public input: the checksum the verifier holds.
A published cell is only as good as the constraints that determine it, and the engine does not check that they do:
the documentation of publish calls it "the audited circuit's burden". Here the boundary fixes ACC on row 0 and
xor_step every ACC up to row 4, which makes the published cell the XOR of the words in the trace. The words
themselves are free: this circuit proves only that some four words have the checksum, and a real program binds its
inputs to something public, as Your First ZK Program binds notes
to a Merkle root.
§ Gadgets
ConstraintSystem builds three common constraints for you. The DSL has no numeric literals: cs.one() and
cs.constant(v) make constants, cs.scale(k, x) multiplies x by the constant k, and cs.sum(&[...]) adds many terms in one node.
| Gadget | Registers | Holds when |
|---|---|---|
assert_boolean(s) | s is 0 or 1 | |
assert_zero_when(sel, x) | x is 0 wherever sel is 1 | |
assert_one_hot(s) | the selectors sum to 1 |
The one-hot gadget needs care in a binary field: , and three boolean selectors at 1 pass it as surely as one. For exactly one, add for every pair, or derive the selectors from fixed columns with disjoint shapes.
§ Degree
The degree of a constraint is the largest number of cells multiplied together in one of its terms: xor_step
multiplies ACTIVE by a sum of cells, degree 2. A table's round polynomial has the degree of its heaviest
constraint plus one, and Hekate caps it at 16 points, which limits a constraint to degree 14
(Prover Engine explains the rounds). To
lower a degree, move a product into a column of its own and constrain that column to the product.
§ The expression graph and the program id
Each operator and constant adds a node to an arena, and each cell adds one the first time it is read; a constraint is
a root that points into it. The program id hashes every node in arena order and every root, and labels are not part of
it. An unused handle such as let _x = cs.one(); still adds a node and changes the id, and the same constraints built
in a different order give a different id: pin the id of the circuit you audited and rebuild it from the same source.
§ Breaking a constraint
Preflight names a failing constraint by its label. In generate_trace on
Execution Trace, set ACC on row 2 to 7 instead of 6 with
tb.set_b32(Checksum::ACC, 2, Block32::from(7u32))?; before tb.build(), and run again. Preflight reports
PREFLIGHT: 2 constraint, 0 boundary, 0 fixed column, 0 bus, names constraint #0, "xor_step", on rows: [1, 2],
and main on Verifier Logic stops with
"the trace fails preflight".
One wrong cell breaks the constraint on both rows that read it: on row 1 it is the next accumulator, on row 2 the
current one. hekate-scribble automates this kind of tampering:
Execution Trace covers it.
§ Next steps
- Execution Trace: fill the table this circuit describes.
- Verifier Logic: prove the checksum and verify it.
- Your First ZK Program: three groups of constraints behind a private payment.