Prover and Verifier Flow
1. Why this chapter exists
Chapters 03-10 cover the arguments and commitments individually.
This chapter is the integration test: it walks through what
keygen_vk, keygen_pk, create_proof, and verify_proof
actually do, in order, and where each previous chapter's
machinery slots in. If you debug a "verifier returned OK but the
proof is wrong" issue, this is the chapter you re-read.
2. Definitions
Definition (Verifying key, VK). A bundle containing the
constraint system shape, the evaluation domain, the commitments
to the fixed columns, and the commitments to the permutation
polynomials. The VK is what circumvents the need to recommunicate
the circuit shape per proof. Defined in
halo2_proofs/src/plonk.rs.
Definition (Proving key, PK). A VerifyingKey plus the fixed
columns themselves and the permutation polynomials in coefficient
form. The PK contains everything the prover needs to recompute
the FFTs; the VK contains everything the verifier needs to check
openings.
Definition (Proof). A byte string consumed by
TranscriptRead. Structurally it is the concatenation of: advice
column commitments (one per circuit, per advice column),
permutation grand-product commitments, lookup commitments,
vanishing polynomial commitments, multiopen commitments, and a
final IPA opening.
3. The code
3.1 Key generation
pub fn keygen_vk<C, ConcreteCircuit>(
params: &Params<C>,
circuit: &ConcreteCircuit,
) -> Result<VerifyingKey<C>, Error>
where
C: CurveAffine,
C::Scalar: FromUniformBytes<64>,
ConcreteCircuit: Circuit<C::Scalar>,
{
let (domain, cs, config) = create_domain::<C, ConcreteCircuit>(params);
if (params.n as usize) < cs.minimum_rows() {
return Err(Error::not_enough_rows_available(params.k));
}
let mut assembly: Assembly<C::Scalar> = Assembly {
k: params.k,
fixed: vec![domain.empty_lagrange_assigned(); cs.num_fixed_columns],
permutation: permutation::keygen::Assembly::new(params.n as usize, &cs.permutation),
selectors: vec![vec![false; params.n as usize]; cs.num_selectors],
usable_rows: 0..params.n as usize - (cs.blinding_factors() + 1),
_marker: std::marker::PhantomData,
};
// Synthesize the circuit to obtain URS
ConcreteCircuit::FloorPlanner::synthesize(
&mut assembly,
circuit,
config,
cs.constants.clone(),
)?;
let mut fixed = batch_invert_assigned(assembly.fixed);
let (cs, selector_polys) = cs.compress_selectors(assembly.selectors);
fixed.extend(
selector_polys
.into_iter()
.map(|poly| domain.lagrange_from_vec(poly)),
);
let permutation_vk = assembly
.permutation
.build_vk(params, &domain, &cs.permutation);
let fixed_commitments = fixed
.iter()
.map(|poly| params.commit_lagrange(poly, Blind::default()).to_affine())
.collect();
Ok(VerifyingKey::from_parts(
domain,
fixed_commitments,
permutation_vk,
cs,
))
}
keygen_vk walks the circuit, builds the ConstraintSystem,
synthesizes the fixed columns and selectors using a witness-free
copy of the circuit, runs selector combining, computes the
permutation polynomials, and commits to everything.
/// Generate a `ProvingKey` from a `VerifyingKey` and an instance of `Circuit`.
pub fn keygen_pk<C, ConcreteCircuit>(
params: &Params<C>,
vk: VerifyingKey<C>,
circuit: &ConcreteCircuit,
) -> Result<ProvingKey<C>, Error>
where
C: CurveAffine,
ConcreteCircuit: Circuit<C::Scalar>,
{
let mut cs = ConstraintSystem::default();
let config = ConcreteCircuit::configure(&mut cs);
let cs = cs;
if (params.n as usize) < cs.minimum_rows() {
return Err(Error::not_enough_rows_available(params.k));
}
let mut assembly: Assembly<C::Scalar> = Assembly {
k: params.k,
fixed: vec![vk.domain.empty_lagrange_assigned(); cs.num_fixed_columns],
permutation: permutation::keygen::Assembly::new(params.n as usize, &cs.permutation),
selectors: vec![vec![false; params.n as usize]; cs.num_selectors],
usable_rows: 0..params.n as usize - (cs.blinding_factors() + 1),
_marker: std::marker::PhantomData,
};
// Synthesize the circuit to obtain URS
ConcreteCircuit::FloorPlanner::synthesize(
&mut assembly,
circuit,
config,
cs.constants.clone(),
)?;
let mut fixed = batch_invert_assigned(assembly.fixed);
let (cs, selector_polys) = cs.compress_selectors(assembly.selectors);
fixed.extend(
selector_polys
.into_iter()
.map(|poly| vk.domain.lagrange_from_vec(poly)),
);
let fixed_polys: Vec<_> = fixed
.iter()
.map(|poly| vk.domain.lagrange_to_coeff(poly.clone()))
.collect();
let fixed_cosets = fixed_polys
.iter()
.map(|poly| vk.domain.coeff_to_extended(poly.clone()))
.collect();
let permutation_pk = assembly
.permutation
.build_pk(params, &vk.domain, &cs.permutation);
// Compute l_0(X)
// TODO: this can be done more efficiently
let mut l0 = vk.domain.empty_lagrange();
l0[0] = C::Scalar::ONE;
let l0 = vk.domain.lagrange_to_coeff(l0);
let l0 = vk.domain.coeff_to_extended(l0);
// Compute l_blind(X) which evaluates to 1 for each blinding factor row
// and 0 otherwise over the domain.
let mut l_blind = vk.domain.empty_lagrange();
for evaluation in l_blind[..].iter_mut().rev().take(cs.blinding_factors()) {
*evaluation = C::Scalar::ONE;
}
let l_blind = vk.domain.lagrange_to_coeff(l_blind);
let l_blind = vk.domain.coeff_to_extended(l_blind);
// Compute l_last(X) which evaluates to 1 on the first inactive row (just
// before the blinding factors) and 0 otherwise over the domain
let mut l_last = vk.domain.empty_lagrange();
l_last[params.n as usize - cs.blinding_factors() - 1] = C::Scalar::ONE;
let l_last = vk.domain.lagrange_to_coeff(l_last);
let l_last = vk.domain.coeff_to_extended(l_last);
Ok(ProvingKey {
vk,
l0,
l_blind,
l_last,
fixed_values: fixed,
fixed_polys,
fixed_cosets,
permutation: permutation_pk,
})
}
keygen_pk takes the VerifyingKey and re-runs the same
synthesis to fill in the L1 / extended-domain caches used by the
prover.
3.2 The prover
/// generated previously for the same circuit. The provided `instances`
/// are zero-padded internally.
pub fn create_proof<
C: CurveAffine,
E: EncodedChallenge<C>,
R: RngCore,
T: TranscriptWrite<C, E>,
ConcreteCircuit: Circuit<C::Scalar>,
>(
params: &Params<C>,
pk: &ProvingKey<C>,
circuits: &[ConcreteCircuit],
instances: &[&[&[C::Scalar]]],
mut rng: R,
transcript: &mut T,
) -> Result<(), Error> {
if circuits.len() != instances.len() {
return Err(Error::InvalidInstances);
}
for instance in instances.iter() {
if instance.len() != pk.vk.cs.num_instance_columns {
return Err(Error::InvalidInstances);
}
}
// Hash verification key into transcript
pk.vk.hash_into(transcript)?;
let domain = &pk.vk.domain;
let mut meta = ConstraintSystem::default();
let config = ConcreteCircuit::configure(&mut meta);
// Selector optimizations cannot be applied here; use the ConstraintSystem
// from the verification key.
let meta = &pk.vk.cs;
struct InstanceSingle<C: CurveAffine> {
pub instance_values: Vec<Polynomial<C::Scalar, LagrangeCoeff>>,
pub instance_polys: Vec<Polynomial<C::Scalar, Coeff>>,
pub instance_cosets: Vec<Polynomial<C::Scalar, ExtendedLagrangeCoeff>>,
}
let instance: Vec<InstanceSingle<C>> = instances
.iter()
.map(|instance| -> Result<InstanceSingle<C>, Error> {
let instance_values = instance
.iter()
.map(|values| {
let mut poly = domain.empty_lagrange();
assert_eq!(poly.len(), params.n as usize);
if values.len() > (poly.len() - (meta.blinding_factors() + 1)) {
return Err(Error::InstanceTooLarge);
}
for (poly, value) in poly.iter_mut().zip(values.iter()) {
*poly = *value;
}
Ok(poly)
})
.collect::<Result<Vec<_>, _>>()?;
let instance_commitments_projective: Vec<_> = instance_values
.iter()
.map(|poly| params.commit_lagrange(poly, Blind::default()))
.collect();
let mut instance_commitments =
vec![C::identity(); instance_commitments_projective.len()];
C::Curve::batch_normalize(&instance_commitments_projective, &mut instance_commitments);
let instance_commitments = instance_commitments;
The full prover flow (file is 786 lines; reading it once is worth a study session by itself):
- Hash the verifying key into the transcript (binds the proof to a particular circuit shape).
- For each circuit (when batching multiple circuits), synthesize the witness, FFT each advice column into coefficient form, blind, commit, write the commitment to the transcript.
- Squeeze the lookup challenge ; run the lookup prover for each lookup argument, commit and .
- Squeeze ; run the permutation prover and the second half of the lookup prover (the grand product ).
- Commit to vanishing polynomial commitments.
- Squeeze the gate-combining challenge .
- Construct the quotient polynomial on the extended coset domain; divide by ; split into pieces of length ; commit to each piece.
- Squeeze the opening challenge .
- Evaluate every polynomial at (and at for rotated queries); write evaluations to the transcript.
- Run the multiopen prover to produce a single IPA opening.
3.3 The verifier
/// Returns a boolean indicating whether or not the proof is valid
pub fn verify_proof<
'params,
C: CurveAffine,
E: EncodedChallenge<C>,
T: TranscriptRead<C, E>,
V: VerificationStrategy<'params, C>,
>(
params: &'params Params<C>,
vk: &VerifyingKey<C>,
strategy: V,
instances: &[&[&[C::Scalar]]],
transcript: &mut T,
) -> Result<V::Output, Error> {
// Check that instances matches the expected number of instance columns
for instances in instances.iter() {
if instances.len() != vk.cs.num_instance_columns {
return Err(Error::InvalidInstances);
}
}
let instance_commitments = instances
.iter()
.map(|instance| {
instance
.iter()
.map(|instance| {
if instance.len() > params.n as usize - (vk.cs.blinding_factors() + 1) {
return Err(Error::InstanceTooLarge);
}
let mut poly = instance.to_vec();
poly.resize(params.n as usize, C::Scalar::ZERO);
let poly = vk.domain.lagrange_from_vec(poly);
Ok(params.commit_lagrange(&poly, Blind::default()).to_affine())
})
.collect::<Result<Vec<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?;
let num_proofs = instance_commitments.len();
// Hash verification key into transcript
vk.hash_into(transcript)?;
for instance_commitments in instance_commitments.iter() {
// Hash the instance (external) commitments into the transcript
for commitment in instance_commitments {
transcript.common_point(*commitment)?
}
}
let advice_commitments = (0..num_proofs)
.map(|_| -> Result<Vec<_>, _> {
// Hash the prover's advice commitments into the transcript
The verifier is structurally a mirror of the prover:
- Hash the verifying key.
- Read the advice / lookup / permutation commitments, re-squeeze each Fiat-Shamir challenge from the transcript.
- Read the vanishing-polynomial commitments and the quotient polynomial commitments.
- Read all polynomial evaluations.
- Re-compute the expected value of from the gate-combining identity and check it against the supplied evaluations.
- Run the multiopen verifier, producing a
Guardwhose final-MSM the strategy then resolves.
The VerificationStrategy parameter (see
halo2_proofs/src/plonk/verifier.rs)
selects between single (default) and batched verification.
SingleVerifier forces the MSM immediately; BatchVerifier
accumulates the MSM across many proofs and forces them all once
at the end. Batch verification can drop per-proof verifier cost
from to amortized.
3.4 The integration test
#![allow(clippy::many_single_char_names)]
#![allow(clippy::op_ref)]
use assert_matches::assert_matches;
use group::ff::{Field, WithSmallOrderMulGroup};
use halo2_proofs::arithmetic::CurveAffine;
use halo2_proofs::circuit::{Cell, Layouter, SimpleFloorPlanner, Value};
use halo2_proofs::dev::MockProver;
use halo2_proofs::pasta::{Eq, EqAffine, Fp};
use halo2_proofs::plonk::{
create_proof, keygen_pk, keygen_vk, verify_proof, Advice, Assigned, BatchVerifier, Circuit,
Column, ConstraintSystem, Error, Fixed, SingleVerifier, TableColumn, VerificationStrategy,
};
use halo2_proofs::poly::commitment::{Guard, MSM};
use halo2_proofs::poly::{commitment::Params, Rotation};
use halo2_proofs::transcript::{Blake2bRead, Blake2bWrite, Challenge255, EncodedChallenge};
use rand_core::OsRng;
use std::marker::PhantomData;
#[test]
fn plonk_api() {
const K: u32 = 5;
/// This represents an advice column at a certain row in the ConstraintSystem
#[derive(Copy, Clone, Debug)]
pub struct Variable(Column<Advice>, usize);
// Initialize the polynomial commitment parameters
let params: Params<EqAffine> = Params::new(K);
#[derive(Clone)]
struct PlonkConfig {
a: Column<Advice>,
b: Column<Advice>,
c: Column<Advice>,
d: Column<Advice>,
e: Column<Advice>,
sa: Column<Fixed>,
sb: Column<Fixed>,
sc: Column<Fixed>,
sm: Column<Fixed>,
sp: Column<Fixed>,
sl: TableColumn,
}
#[allow(clippy::type_complexity)]
trait StandardCs<FF: Field> {
fn raw_multiply<F>(
&self,
layouter: &mut impl Layouter<FF>,
f: F,
) -> Result<(Cell, Cell, Cell), Error>
where
F: FnMut() -> Value<(Assigned<FF>, Assigned<FF>, Assigned<FF>)>;
fn raw_add<F>(
&self,
layouter: &mut impl Layouter<FF>,
f: F,
) -> Result<(Cell, Cell, Cell), Error>
The integration test in tests/plonk_api.rs runs the full
keygen-prove-verify loop on a hand-designed circuit and compares
the resulting proof bytes against the checked-in fixture
plonk_api_proof.bin. If you change anything that affects proof
bytes (a new commitment, a reordered challenge, even an extra
zero pad), this fixture must be regenerated.
4. Failure modes
- Forgetting to hash the VK first. The first thing both
prover and verifier do is
vk.hash_into(transcript). Skipping this would let a malicious prover replay a proof against a different circuit. Audit any new transcript path for this. - Mismatched
Params. Prover and verifier must use the sameParams(k). Halo 2'sParams::newis deterministic, so a fresh prover and verifier rederive identical params; do not introduce randomness here. - Proof byte drift. Changing the order of any operation that
writes to the transcript (commitments, evaluations) silently
changes the proof bytes. The
plonk_api_proof.binfixture catches this in CI. - Skipping the
Guard. If you write a custom verifier that drops theGuardreturned byverify_proofwithout callinguse_challenges, the multiopen IPA check never runs and every proof passes. This is the kind of bug a security review catches.
5. Spec pointers
- Halo paper, eprint 2019/1021 section 4: the full IOP-to-NIZK construction.
- PLONK paper, eprint 2019/953 section 5: the integration of the gate, permutation, and lookup arguments. The halo2 prover follows the same template.
- The Halo 2 Book Protocol Description chapter walks through the same flow at the spec level.
6. Exercises
- Open
halo2_proofs/src/plonk/prover.rsand find every call totranscript.common_point(...),transcript.write_point(...), andtranscript.write_scalar(...). In order, list each. This is the proof's structure. - Repeat the exercise for the verifier in
halo2_proofs/src/plonk/verifier.rsusingread_pointandread_scalar. Confirm the two lists are dual. - Run
cargo test --release -p halo2_proofs --test plonk_apiand observe the test name(s). Confirm the proof fixture matches. - Modify one byte of
halo2_proofs/tests/plonk_api_proof.bin(use a hex editor) and rerun the test. Confirm the verifier rejects. - Why does
keygen_vkneed a witness-free circuit, whilekeygen_pkaccepts a witness-bearing one? Answer in two sentences after reading both functions.
Answers in the code
- Exercise 1: the order is (roughly)
vk.hash_into, advice commitments, lookup permuted commitments, permutation product commitments, vanishing commitments, quotient-poly piece commitments, evaluations, multiopen openings. - Exercise 5:
keygen_vkonly needs the shape; the witness would be redundant.keygen_pkre-synthesizes to fill the extended caches but does not include witness data in the proving key.
7. Further reading
- The Halo 2 Book Implementation chapter for the engineering details that drove the prover layout.
- The Halo 2 Book Proofs chapter for an opcode-by-opcode description of the proof format.