# 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.

- URL: https://oumuamua.dev/hekate/docs/basics/verifier-logic
- Published: 2026-05-10T12:45:14+07:00
- Updated: 2026-10-05T20:51:05+07:00

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](https://oumuamua.dev/hekate/docs/basics/air-constraints) builds its circuit in `src/circuit.rs`, and
[Execution Trace](https://oumuamua.dev/hekate/docs/basics/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](https://oumuamua.dev/hekate/docs/basics/system-architecture#open-crates-closed-prover) 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](https://oumuamua.dev/hekate/docs/basics/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

| Result      | Means                                                                                                    |
|:------------|:---------------------------------------------------------------------------------------------------------|
| `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.](https://oumuamua.dev/assets/hekate/docs/basics/verifier-logic/prepared.ae469ab7ce53f835d919323e3eac0634714b7f6bc8bfa22e7e98e3ec4a5bb356.svg "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 src/verify.rs:1
use crate::circuit::F;
use hekate_core::config::Config;
use hekate_core::errors;
use hekate_core::proofs::InnerProof;
use hekate_crypto::DefaultHasher;
use hekate_crypto::transcript::Transcript;
use hekate_program::{Program, ProgramInstance};
use hekate_sdk::deserialize_proof;
use hekate_verifier::HekateVerifier;
use hekate_verifier::prepared::{PreparedProgram, VerifierScratch};

pub const LABEL: &[u8] = b"xor-checksum";

pub struct Checker {
    audited_id: [u8; 32],
    prepared: PreparedProgram<F>,
    scratch: VerifierScratch<F>,
}

impl Checker {
    pub fn new<P: Program<F>>(
        audited_id: [u8; 32],
        program: &P,
        config: &Config,
    ) -> errors::Result<Self> {
        Ok(Self {
            audited_id,
            prepared: PreparedProgram::new(program, config)?,
            scratch: VerifierScratch::new(),
        })
    }

    pub fn check(
        &mut self,
        instance: &ProgramInstance<F>,
        proof_bytes: &[u8],
    ) -> errors::Result<bool> {
        let proof = deserialize_proof::<F>(proof_bytes)?;

        HekateVerifier::<F, DefaultHasher>::verify_prepared(
            &self.audited_id,
            &self.prepared,
            instance,
            &proof,
            &mut Transcript::new(LABEL),
            &mut self.scratch,
        )
    }

    pub fn check_batch(
        &self,
        proofs: &[(ProgramInstance<F>, InnerProof<F>)],
    ) -> Vec<errors::Result<bool>> {
        let items: Vec<_> = proofs
            .iter()
            .map(|(instance, proof)| (instance, proof))
            .collect();

        HekateVerifier::<F, DefaultHasher>::verify_batch(
            &self.audited_id,
            &self.prepared,
            LABEL,
            &items,
        )
    }
}
```

```rust src/main.rs:1
mod circuit;
mod trace;
mod verify;

use circuit::{F, build_program, table_rows};
use hekate_core::config::Config;
use hekate_core::proofs::InnerProof;
use hekate_program::digest::{program_id, program_id_hex};
use hekate_program::{Program, ProgramInstance, ProgramWitness};
use hekate_prover_sys::prove;
use hekate_sdk::{preflight, serialize_proof_bytes};
use rand::{TryRng, rngs::SysRng};
use std::error::Error;
use trace::generate_trace;
use verify::{Checker, LABEL};

fn main() -> Result<(), Box<dyn Error>> {
    let words = [3, 5, 2, 6];
    let checksum = words.iter().fold(0, |acc, word| acc ^ word);

    let config = Config::default();
    let rows = table_rows(&config, words.len());
    let program = build_program(rows, words.len())?;

    let statement = || ProgramInstance::new(rows, vec![F::from(u128::from(checksum))]);
    let witness = ProgramWitness::new(generate_trace(&words, rows)?);

    let report = preflight(&program, &statement(), &witness)?;

    if !report.is_clean() {
        eprintln!("{report}");

        return Err("the trace fails preflight".into());
    }

    println!("program id: {}", program_id_hex(&program)?);

    let mut checker = Checker::new(program_id(&program)?, &program, &config)?;

    let first = prove_once(&program, &statement(), &witness, &config)?;
    let bytes = serialize_proof_bytes(&first);

    println!("one proof: {}", checker.check(&statement(), &bytes)?);

    let second = prove_once(&program, &statement(), &witness, &config)?;
    let batch = [(statement(), first), (statement(), second)];

    for (at, verdict) in checker.check_batch(&batch).into_iter().enumerate() {
        println!("batch proof {at}: {}", verdict?);
    }

    Ok(())
}

fn prove_once<P: Program<F>>(
    program: &P,
    instance: &ProgramInstance<F>,
    witness: &ProgramWitness<F>,
    config: &Config,
) -> Result<InnerProof<F>, Box<dyn Error>> {
    let mut seed = [0; 32];
    SysRng.try_fill_bytes(&mut seed)?;

    Ok(prove(
        LABEL, program, instance, witness, config, seed, None,
    )?)
}
```

`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](https://oumuamua.dev/hekate/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](https://oumuamua.dev/hekate/docs/basics/air-constraints#fixed-columns-boundaries-public-inputs) 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](https://oumuamua.dev/primitives) lists what each one leaves outside the proof.

## Next steps

- [System Architecture](https://oumuamua.dev/hekate/docs/basics/system-architecture): where the verifier sits, and what it trusts.
- [Prover Engine](https://oumuamua.dev/hekate/docs/basics/prover-engine): the protocol the verifier replays.
- [AIR Constraints](https://oumuamua.dev/hekate/docs/basics/air-constraints): the circuit behind the checksum.