Skip to content

Verify STARK

A guest library for recursive verification of OpenVM STARK proofs. The library uses the deferral framework to offload proof verification to the aggregation phase, making recursive verification efficient within guest programs. Readers should first read that documentation, which explains deferral circuits and how the deferral extension is configured.

You can find the library source code here.

Guest

The guest library (openvm-verify-stark-guest) provides functions to request and consume deferred STARK proof verification.

verify_stark_unchecked

pub fn verify_stark_unchecked<const DEF_IDX: u16>(input_commit: &Commit) -> ProofOutput

Invokes a deferred call at deferral index DEF_IDX with the given input_commit (a 32-byte commitment identifying the proof). Returns a ProofOutput containing:

  • app_exe_commit: Commit — commitment to the app executable whose execution is being verified
  • app_vm_commit: Commit — commitment to the app VM configuration
  • user_public_values: Vec<u8> — user public values revealed by the verified app

When correctly configured with the DeferredVerifyCircuit, verification is performed outside the VM circuit as part of the deferral flow. From the guest perspective, this establishes that there exists a proof matching input_commit that verifies and yields the returned ProofOutput; the proof itself is never accessible from the guest program.

The deferral circuit output stores each user public value byte as a field element, but the guest helper returns the dense byte representation. ProofOutput.user_public_values should therefore match the bytes revealed by the verified app.

DEF_IDX must match the index of the corresponding entry in DeferralConfig.circuits, as configured when building the VM extension. Each STARK verifying key will correspond to a different deferral circuit, and thus a different DEF_IDX.

verify_stark

pub fn verify_stark<const DEF_IDX: u16>(input_commit: &Commit, expected: &ProofOutput)

Calls verify_stark_unchecked and panics if the result does not match expected.

Host

The circuit crate (openvm-verify-stark-circuit) provides the DeferredVerifyCircuit, which is the deferral circuit that actually verifies the STARK proof during aggregation. It:

  1. Verifies the child STARK proof using the recursion verifier sub-circuit
  2. Constrains that the user public values are present in the final memory state
  3. Produces the output commitment containing app_exe_commit, app_vm_commit, and user_public_values

There should be a unique DeferredVerifyCircuit for each STARK verifying key you want to support.

Using DeferralConfig

Your VM config declares which deferral circuits it supports through a DeferralConfig. To make a DeferredVerifyCircuit usable from a guest program, add a DeferralCircuitConfig for it (with def_type: SupportedDeferral::VerifyStark) to the DeferralConfig.circuits list. A VM config can support up to MAX_DEF_CIRCUITS = 512 different deferral circuits at one time. DeferralConfig has a single field:

  • circuits: Vec<DeferralCircuitConfig> - one entry per deferral circuit

Each DeferralCircuitConfig entry has:

  • def_type: SupportedDeferral - the supported deferral function, such as SupportedDeferral::VerifyStark
  • commit: CommitBytes - the corresponding def_circuit_commit, serialized in TOML as a 32-byte hex string

The SDK converts DeferralConfig into the lower-level DeferralExtension when building execution, transpiler, and prover components.

For verify-stark recursion, the simplest setup is DeferralAggProver::verify_stark, which derives the deferral-enabled internal-recursive verifying key and self-referential def_hook_commit needed to verify proofs from the same SDK, including proofs that themselves carry deferrals:

let deferral_agg_prover = DeferralAggProver::verify_stark(
    &agg_params,
    hook_params,
    memory_dimensions,
    num_user_pvs,
);
let deferral_config = deferral_agg_prover
    .multi_deferral_circuit_prover
    .make_config(vec![SupportedDeferral::VerifyStark]);
 
let vm_config = SdkVmConfig::builder()
    .system(Default::default())
    .rv32i(Default::default())
    .rv32m(Default::default())
    .io(Default::default())
    .deferral(deferral_config)
    .build()
    .optimize();
 
let sdk = CpuSdk::builder()
    .app_config(AppConfig::new(vm_config, app_params))
    .agg_params(agg_params)
    .deferral_agg_prover(deferral_agg_prover)
    .build()?;

The lower-level manual flow is:

  1. Build a MultiDeferralCircuitProver whose single-circuit prover is a DeferredVerifyCircuitProver.
  2. Create the deferral config with make_config, passing the supported deferral type(s).
  3. Build SdkVmConfig with .deferral(deferral_config) before calling .optimize().
  4. Build your SDK with Sdk::builder().multi_deferral_circuit_prover(multi_deferral_circuit_prover).

For example, to initialize the SDK with the verify-stark deferral circuit:

const VERIFY_STARK_DEF_IDX: usize = 0;
let deferred_verify_prover = DeferredVerifyCpuProver::new::<E>(
    ir_vk,
    ir_pcs_data.commitment.into(),
    def_circuit_params,
    memory_dimensions,
    num_user_pvs,
    None,
    VERIFY_STARK_DEF_IDX,
);
let verify_stark_prover = DeferredVerifyCpuCircuitProver::new(deferred_verify_prover);
 
let multi_deferral_circuit_prover = MultiDeferralCircuitProver::new(verify_stark_prover, agg_config, hook_params);
let deferral_config =
    multi_deferral_circuit_prover.make_config(vec![SupportedDeferral::VerifyStark]);
 
let vm_config = SdkVmConfig::builder()
    .system(Default::default())
    .rv32i(Default::default())
    .rv32m(Default::default())
    .io(Default::default())
    .deferral(deferral_config)
    .build()
    .optimize();
 
let sdk = CpuSdk::builder()
    .app_config(AppConfig::new(vm_config, app_params))
    .agg_params(agg_params)
    .multi_deferral_circuit_prover(multi_deferral_circuit_prover)
    .build()?;

Passing None to DeferredVerify*Prover::new(..., None, def_idx) means that child proofs are asserted to be non-deferral proofs, i.e. they must not expose deferral public values or deferral Merkle proofs. To enable nested verification of deferral-enabled child proofs, pass Some(expected_def_hook_commit) instead. The DeferralAggProver::verify_stark constructor computes this value for the self-recursive SDK case.

Each guest call using DEF_IDX corresponds to the deferral circuit inserted at DEF_IDX. If there is exactly one deferral function registered (as above), DEF_IDX = 0.

Generating Inputs and Proving

The guest only passes an input commitment to verify_stark_unchecked; the child STARK proof being verified lives on the host and must be supplied to both phases described in the deferral framework's Inputs and Proving section. For verify-stark, the deferral input is the child proof itself:

// For execution: let the VM resolve the deferred output for the input commitment.
stdin.deferrals = vec![get_deferral_state(&child_vk, from_ref(proof), verify_stark_cached_commit, VERIFY_STARK_DEF_IDX as u32)?];
 
// For proving: hand the child proof to the deferral circuit prover.
let def_input = DeferralInput::from_inputs(from_ref(proof));
let (proof, _) = sdk.prove(exe, stdin, &[def_input])?;

End-to-End Example

The repository includes an end-to-end verify-stark example under examples/verify-stark. It has:

  • guest/ - a guest program that calls verify_stark_unchecked, optionally across multiple deferral circuits and with multiple verify calls per circuit.
  • host/ - a standalone host crate that constructs the deferral-enabled SDK (with a configurable number of verify-stark deferral circuits), writes its generated openvm.toml, caches the SDK proving keys, builds the guest, and proves child STARK proofs through the verify-stark circuits.
  • verify-book-examples.sh - a CUDA-oriented CI script that proves each book example, wraps each proof through the verify-stark example, and verifies the resulting STARK proof with cargo openvm verify stark. It accepts optional [num_def_circuits] [num_proves_per_circuit] [example] arguments.

The host crate uses Sdk::cached_proving_key() after keygen and reconstructs the SDK later with Sdk::from_deferral_cached_proving_key(...), so the expensive verify-stark proving keys are built once and reused by the build and prove commands.