Skip to content
Oumuamua Labs
Hekate ZK Engine menu

Verifier Logic

Verify Hekate zero-knowledge proofs in Rust: pin the program id, read the result, and verify one proof, many with a prepared program, or a parallel batch.

Updated verifier-logic.md
On this page

The verifier decides whether a proof is valid. It replays the prover's transcript and every check in open-source Rust, from the program you give it, the public instance and the proof bytes, and returns Ok(true) only when all of them pass.

The example on this page proves and verifies a XOR checksum over four private 32-bit words. AIR Constraints builds its circuit in src/circuit.rs, and Execution Trace fills its table in src/trace.rs.

§ What the verifier needs

Four inputs come from your build and one from the prover: the program built from audited source, its pinned program id, the Config and transcript label, the instance, and the proof. System Architecture explains why each is pinned. The verifier runs no prover code and needs no native library, and with default-features = false plus blake3 it builds as no_std with alloc.

§ The checks, in order

The verifier works through a proof in the order the prover built it; Prover Engine walks through the protocol.

  1. Shape. Table heights and counts, and the security floor of the configuration.
  2. Identity. The id computed from your program against the pinned one.
  3. Binding. The program id, configuration, sizes, public inputs and the commitment over every table enter the transcript.
  4. Buses. The bus challenges γ and β, one r_bus per lookup bus, then the root of the helper columns.
  5. Each table, chiplets first: the ZeroCheck rounds down to one point and the constraints at that point (recorded for the outer argument with zero knowledge on).
  6. Close. One evaluation sumcheck over every table and the opened columns against the commitments and the code; then the bus sums cancel or, with zero knowledge on, the outer argument holds over the recorded checks.

§ Reading the result

ResultMeans
Ok(true)every check passed
Ok(false)the main table's ZeroCheck, any table's evaluation check, or the outer argument, failed
Err(_)any other check failed, the proof or input is malformed, the program id differs, or the floor is not met

Only Ok(true) accepts; a forged proof can produce either of the other two. ProgramIdMismatch points at the verifying build itself: its program drifted from the one you audited.

§ One proof, many, or a batch

HekateVerifier::verify prepares the program on every call, rebuilding its table shapes and recomputing its id. A service that verifies many proofs of one program prepares it once.

A PreparedProgram, built once, holds the tables, shapes, ring-switch plans and program id, and is shared read-only. Three threads each hold their own scratch and turn a proof into a result.
One prepared program serves every thread; each thread keeps its own scratch.
  • PreparedProgram::new(&program, &config) computes the tables, shapes and program id once and is read-only after that, which lets one copy serve every thread. Its Config is fixed at preparation, and verify_prepared takes none.
  • VerifierScratch caches state between proofs and is mutable: one per thread.
  • verify_batch checks a slice of proofs, one per pool thread with the parallel feature, each with its own scratch and a transcript opened with the label, and returns the results in input order.
rust
1use crate::circuit::F;
2use hekate_core::config::Config;
3use hekate_core::errors;
4use hekate_core::proofs::InnerProof;
5use hekate_crypto::DefaultHasher;
6use hekate_crypto::transcript::Transcript;
7use hekate_program::{Program, ProgramInstance};
8use hekate_sdk::deserialize_proof;
9use hekate_verifier::HekateVerifier;
10use hekate_verifier::prepared::{PreparedProgram, VerifierScratch};
11
12pub const LABEL: &[u8] = b"xor-checksum";
13
14pub struct Checker {
15    audited_id: [u8; 32],
16    prepared: PreparedProgram<F>,
17    scratch: VerifierScratch<F>,
18}
19
20impl Checker {
21    pub fn new<P: Program<F>>(
22        audited_id: [u8; 32],
23        program: &P,
24        config: &Config,
25    ) -> errors::Result<Self> {
26        Ok(Self {
27            audited_id,
28            prepared: PreparedProgram::new(program, config)?,
29            scratch: VerifierScratch::new(),
30        })
31    }
32
33    pub fn check(
34        &mut self,
35        instance: &ProgramInstance<F>,
36        proof_bytes: &[u8],
37    ) -> errors::Result<bool> {
38        let proof = deserialize_proof::<F>(proof_bytes)?;
39
40        HekateVerifier::<F, DefaultHasher>::verify_prepared(
41            &self.audited_id,
42            &self.prepared,
43            instance,
44            &proof,
45            &mut Transcript::new(LABEL),
46            &mut self.scratch,
47        )
48    }
49
50    pub fn check_batch(
51        &self,
52        proofs: &[(ProgramInstance<F>, InnerProof<F>)],
53    ) -> Vec<errors::Result<bool>> {
54        let items: Vec<_> = proofs
55            .iter()
56            .map(|(instance, proof)| (instance, proof))
57            .collect();
58
59        HekateVerifier::<F, DefaultHasher>::verify_batch(
60            &self.audited_id,
61            &self.prepared,
62            LABEL,
63            &items,
64        )
65    }
66}
1mod circuit;
2mod trace;
3mod verify;
4
5use circuit::{F, build_program, table_rows};
6use hekate_core::config::Config;
7use hekate_core::proofs::InnerProof;
8use hekate_program::digest::{program_id, program_id_hex};
9use hekate_program::{Program, ProgramInstance, ProgramWitness};
10use hekate_prover_sys::prove;
11use hekate_sdk::{preflight, serialize_proof_bytes};
12use rand::{TryRng, rngs::SysRng};
13use std::error::Error;
14use trace::generate_trace;
15use verify::{Checker, LABEL};
16
17fn main() -> Result<(), Box<dyn Error>> {
18    let words = [3, 5, 2, 6];
19    let checksum = words.iter().fold(0, |acc, word| acc ^ word);
20
21    let config = Config::default();
22    let rows = table_rows(&config, words.len());
23    let program = build_program(rows, words.len())?;
24
25    let statement = || ProgramInstance::new(rows, vec![F::from(u128::from(checksum))]);
26    let witness = ProgramWitness::new(generate_trace(&words, rows)?);
27
28    let report = preflight(&program, &statement(), &witness)?;
29
30    if !report.is_clean() {
31        eprintln!("{report}");
32
33        return Err("the trace fails preflight".into());
34    }
35
36    println!("program id: {}", program_id_hex(&program)?);
37
38    let mut checker = Checker::new(program_id(&program)?, &program, &config)?;
39
40    let first = prove_once(&program, &statement(), &witness, &config)?;
41    let bytes = serialize_proof_bytes(&first);
42
43    println!("one proof: {}", checker.check(&statement(), &bytes)?);
44
45    let second = prove_once(&program, &statement(), &witness, &config)?;
46    let batch = [(statement(), first), (statement(), second)];
47
48    for (at, verdict) in checker.check_batch(&batch).into_iter().enumerate() {
49        println!("batch proof {at}: {}", verdict?);
50    }
51
52    Ok(())
53}
54
55fn prove_once<P: Program<F>>(
56    program: &P,
57    instance: &ProgramInstance<F>,
58    witness: &ProgramWitness<F>,
59    config: &Config,
60) -> Result<InnerProof<F>, Box<dyn Error>> {
61    let mut seed = [0; 32];
62    SysRng.try_fill_bytes(&mut seed)?;
63
64    Ok(prove(
65        LABEL, program, instance, witness, config, seed, None,
66    )?)
67}

Checker is the long-lived verifier: one prepared program, one scratch, and the pinned id. main checks the trace with preflight, proves the checksum twice with fresh seeds, and verifies the first proof alone, from its wire bytes, then both in a batch. It prints the program id, then one proof: true, batch proof 0: true and batch proof 1: true. program_id(&program) stands in for the pinned id here: a real verifier compiles in the hex that program_id_hex printed once, at audit time.

§ Speed

parallel, on by default, spreads leaf hashing and proximity checks above a size threshold, the outer argument's openings and tests, and verify_batch over a Rayon pool. table-math makes basis conversion faster and variable-time: safe in a verifier, which only touches public values, but Cargo turns it on for every crate in the build: leave it off in a binary that also builds witnesses. Benchmarks lists verification times for each workload.

§ What a valid proof does not show

A valid proof shows that a trace satisfying your program exists, and nothing beyond the program.

  • It is as strong as the circuit. A published cell that no constraint determines can take any value the prover likes; AIR Constraints explains.
  • It does not show that the witness stayed private. Zero knowledge depends on how the proof was made: a fresh seed and the prover binary that read the witness.
  • Each chiplet proves a stated scope, and Primitives lists what each one leaves outside the proof.

§ Next steps