verifier
the standalone verifier algorithm for zheng. accepts a proof and a public statement, returns accept or reject. the verifier is a nox program — it runs inside the same VM that produced the original trace, enabling recursive proof composition.
the Lens opening today is Brakedown with TensorMerkle authentication: every queried codeword column carries a Merkle path against the commitment root. proof size scales with trace size — see performance for measured numbers. a Merkle-free recursive opening (constant ~2 KiB) was designed but is blocked on a soundness gap; see recursive-brakedown in roadmap/ and lens issue #6.
algorithm
VERIFY(commitment C, statement S, proof pi) -> accept/reject:
1. INIT TRANSCRIPT
T = transcript_init(DOMAIN_SEP)
T.absorb(S)
T.absorb(C)
2. SUMCHECK VERIFICATION (k rounds, k = n + 4)
claim_0 = 0 (constraints must sum to zero)
for i in 1..k:
g_i = pi.sumcheck_polynomials[i]
assert g_i(0) + g_i(1) = claim_{i-1}
T.absorb(g_i)
r_i = T.squeeze()
claim_i = g_i(r_i)
3. EVALUATION CHECK
r = (r_1, ..., r_k)
v = pi.evaluation_value
assert claim_k = constraint_eval(v, r, S)
4. LENS VERIFICATION
assert Brakedown_verify(C, r, v, pi.pcs_opening)
(TensorMerkle: per-column Merkle path check against the commitment root,
plus the row-combination consistency check against the encoding)
return accept
step 2 is pure field arithmetic. step 4 authenticates each queried column against the commitment root via Merkle path, then checks encoding consistency — this is where hashing dominates the verifier's cost (see performance).
constraint evaluation
step 3 checks that the sumcheck's final claim equals constraint_eval(v, r, S). this function reconstructs the constraint polynomial's evaluation at the random point r chosen during sumcheck, using the prover-supplied register evaluation v.
register layout
the trace table has 16 registers per row:
| register | meaning |
|---|---|
| r0 | pattern tag (which of the 17 nox patterns this row executes) |
| r1, r2 | operand hashes (left, right) |
| r3, r4 | inputs (immediate values or dereferenced operands) |
| r5 | result |
| r6 | focus_before (focus counter entering this step) |
| r7 | focus_after (focus counter leaving this step) |
| r8-r13 | auxiliary (pattern-specific intermediate values) |
| r14 | status (0 = running, 1 = halted, 2 = error) |
| r15 | step (row index, starting from 0) |
the prover commits to the multilinear extension of each register column. v is the vector of all 16 register evaluations at the random point r: v[j] = f_j(r) where f_j is the multilinear extension of register column j over the boolean hypercube.
selector polynomials
each pattern p in {0, 1, ..., 16} has a Lagrange selector polynomial:
selector_p(x) = product_{q != p} (x - q) / (p - q)
evaluated at v[0] (the pattern tag register at point r), selector_p(v[0]) is 1 when the pattern tag equals p and 0 otherwise. this demultiplexes the single constraint evaluation into the correct pattern.
pattern constraints
each pattern p defines a constraint polynomial C_p over the register values. for example:
- pattern 5 (add):
C_5(v) = v[3] + v[4] - v[5](inputs sum to result) - pattern 7 (mul):
C_7(v) = v[3] * v[4] - v[5](inputs multiply to result) - pattern 8 (inv):
C_8(v) = v[3] * v[5] - 1(input times result is one) - pattern 15 (hash):
C_15(v) = hemera(v[3], v[4]) - v[5](hash of inputs equals result)
all 17 patterns contribute, but the selector zeroes out every pattern except the active one.
the formula
constraint_eval(v, r, S) = eq(r_row, r) * sum_{p=0}^{16} selector_p(v[0]) * C_p(v)
where:
r_rowis the row-dimension component of r (the first n variables, where 2^n is the number of trace rows)eq(r_row, r)is the multilinear equality polynomial:eq(a, b) = product_i (a_i * b_i + (1 - a_i)(1 - b_i)), which ties the sumcheck reduction to the specific evaluation point- the sum over p combines all 17 pattern constraints, weighted by selectors
C_p(v)evaluates pattern p's constraint using the register values at r
the sumcheck protocol reduces the claim "the constraint polynomial sums to zero over the boolean hypercube" to a single evaluation at a random point. constraint_eval computes that single evaluation. if the prover is honest, every row satisfies its pattern constraint, the sum is zero, and this evaluation matches claim_k.
boundary constraints
boundary constraints enforce the public statement S at specific trace positions. these are point evaluations at binary coordinates (vertices of the hypercube):
- at row 0:
f_14(0, 0, ..., 0) = 0(status is running),f_15(0, 0, ..., 0) = 0(step counter starts at zero) - at the final row:
f_14(1, 1, ..., 1) = 1(status is halted) - program hash: the commitment to the opcode column must match
S.program_hash - input/output:
f_3(0, ..., 0) = S.input_hashandf_5(last_row) = S.output_hash - focus bound:
f_6(0, ..., 0) - f_7(last_row) <= S.focus_bound
boundary constraints are folded into the same sumcheck by adding them as additional terms with their own random combiners (drawn from the transcript).
relationship to sumcheck
the sumcheck protocol (step 2) starts with claim_0 = 0 because a valid trace satisfies all constraints on every row, so the constraint polynomial sums to zero over the hypercube. after k rounds of interaction, the verifier holds a random point r and a final claim claim_k. step 3 asserts claim_k = constraint_eval(v, r, S): the reduced claim equals the actual constraint evaluation at r. step 4 then verifies (via Brakedown) that v is the correct evaluation of the committed polynomials at r. together, steps 3 and 4 complete the sumcheck verification — step 3 checks algebraic consistency, step 4 checks that the prover did not lie about v.
cost breakdown
the verifier uses TensorMerkle Brakedown: every queried column is authenticated by a Merkle path against the commitment root, so hashing — not field arithmetic — dominates. see performance for the full component breakdown; summary:
| configuration | constraints | description |
|---|---|---|
| Layer 1 only (no jets) | ~600,000 | Merkle verification is ~83% of this |
| with jets | ~70,000 | merkle_verify jet gives Merkle checks a 10× reduction |
a Merkle-free design (recursive Brakedown, targeting ~825 constraints generic / ~89 with algebraic Fiat-Shamir) was analyzed but is blocked on a soundness gap in the recursive opening — see recursive-brakedown in roadmap/ and lens issue #6. those numbers are the target if that gap closes, not the current cost.
nox pattern decomposition
every verifier operation maps to native nox patterns:
| verifier operation | nox patterns | why native |
|---|---|---|
| field arithmetic | 5 (add), 6 (sub), 7 (mul), 8 (inv) | Goldilocks field is the native field |
| hash computation | 15 (hash) / hash jet | hemera — Fiat-Shamir only, ~3 calls |
| sumcheck verification | 5, 7, 9 | pure field arithmetic |
| Brakedown Lens opening verification | 5, 7, 15 (hash) / hash jet | Merkle path checks per queried column, plus a field-arithmetic encoding-consistency check |
no external primitive enters the verification loop. the verifier is closed under the nox instruction set. consequence: verify(proof) can itself be proven, and verify(verify(proof)) too, to arbitrary depth.
Brakedown opening verification with TensorMerkle is hash-dominated: each queried column needs a Merkle path checked against the commitment root. hemera is used both for that and for the Fiat-Shamir transcript.
input/output format
INPUTS (public):
commitment: [u8; 32] // hemera digest
statement: Statement {
program_hash: [u8; 32], // hash of the nox program
input_hash: [u8; 32], // hash of public inputs
output_hash: [u8; 32], // hash of public outputs
focus_bound: u64, // maximum focus consumed
}
PROOF:
sumcheck_polynomials: Vec<Vec<GoldilocksElement>>,
evaluation_value: GoldilocksElement,
pcs_opening: BrakedownProof,
OUTPUT:
accept / reject
verification time
| security level | verification time |
|---|---|
| 100-bit | ~290 μs |
| 128-bit | ~1.0 ms |
see performance for the derivation. verification time is dominated by Merkle-path checks over the queried columns, not by the trace size directly — but proof size (and so the number of columns to check) does grow with trace size, unlike the constant-size claim a Merkle-free lens would give.
recursive verification
when the verifier runs as a nox program, its execution trace can be proven by zheng. the recursive proof attests that a previous proof was valid.
proof_A = zheng.prove(computation) // ~|C| constraints
proof_B = zheng.prove(zheng.verify(proof_A)) // ~70,000 constraints (with jets)
proof_C = zheng.prove(zheng.verify(proof_B)) // ~70,000 constraints (with jets)
each recursion level costs ~70,000 constraints with jets (~600,000 without) — see performance. commit currently rejects axis/look openings for recursive composition (CommitError::UnsupportedRecursiveOpening); the native decider path works, recursive composition beyond that is release-blocked.
with HyperNova folding, recursive composition drops further: ~30 field ops + 1 hemera hash per fold step, with one decider proof at the end. see recursion for folding-first composition.
see transcript for Fiat-Shamir construction, sumcheck for the core protocol, Brakedown for the Lens opening verification, constraints for the AIR format, nox for the VM