Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,21 @@ code paths, also run `cargo make tests` end-to-end.

## Verifier semantic contract

A valid Ceno proof attests to exactly two program-level facts:
**execution starts at the verifying key's entry PC**, and
**execution exits with code zero**. Everything else in the verifier
(transcript, sumcheck, PCS, tower/GKR, shard EC sum) is plumbing that
makes those two statements meaningful. The caller-supplied "expect
halt" flag picks the terminal mode (full run vs. prefix run); the
exit-code-zero invariant holds in both, and the halt-ecall presence
must match what the caller declared. A change that relaxes either
statement is a blocker. See `docs/src/technical-overview.md` for the
long form.

Prefix proofs (expect-halt = false) are a dev/bench affordance, not
a production surface — don't wire them into anything external without
first bringing the non-halting-shard public values up to the halt-path
standard.
What a valid Ceno proof attests to depends on the **verifier mode**,
committed at verifier construction and not derived from the proof:

- **FullRun** — trace from `vk.entry_pc` to halt with `exit_code == 0`.
Production default.
- **PrefixRun** — trace from `vk.entry_pc` that stopped at a step
budget; last-shard halt and exit code not checked. Dev/bench only.
- **DebugSegment** — one shard at any position; skips entry-PC, chain,
halt, and exit-code checks. Dev only.

Only `PrefixRun` / `DebugSegment` callers opt in via `new_with_mode`;
non-`FullRun` proofs must not be exposed to external consumers.
Weakening any mode's statement (dropping the entry-PC check, letting
intermediate shards halt, making the mode prover-derived, etc.) is a
blocker by default.

## What to prioritize when editing

Expand Down
9 changes: 6 additions & 3 deletions ceno_zkvm/benches/fibonacci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use criterion::*;
use ff_ext::BabyBearExt4;
use gkr_iop::cpu::default_backend_config;

use ceno_zkvm::{e2e::MultiProver, scheme::verifier::ZKVMVerifier};
use ceno_zkvm::{
e2e::MultiProver,
scheme::verifier::{VerifierMode, ZKVMVerifier},
};
use mpcs::BasefoldDefault;
use transcript::BasicTranscript;

Expand Down Expand Up @@ -69,10 +72,10 @@ fn fibonacci_prove(c: &mut Criterion) {

println!("e2e proof {}", proof);
let transcript = BasicTranscript::new(b"riscv");
let verifier = ZKVMVerifier::<E, Pcs>::new(vk);
let verifier = ZKVMVerifier::<E, Pcs>::new_with_mode(vk, VerifierMode::PrefixRun);
assert!(
verifier
.verify_full_trace_proofs_halt(vec![proof], vec![transcript], false)
.verify_proofs(vec![proof], vec![transcript])
.expect("verify proof return with error"),
);
println!();
Expand Down
2 changes: 1 addition & 1 deletion ceno_zkvm/benches/keccak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ fn keccak_prove(c: &mut Criterion) {
let verifier = ZKVMVerifier::<E, Pcs>::new(vk);
assert!(
verifier
.verify_full_trace_proofs_halt(vec![proof], vec![transcript], true)
.verify_proofs(vec![proof], vec![transcript])
.expect("verify proof return with error"),
);
println!();
Expand Down
23 changes: 11 additions & 12 deletions ceno_zkvm/src/bin/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ use ceno_zkvm::print_allocated_bytes;
use ceno_zkvm::{
e2e::{
Checkpoint, FieldType, MultiProver, PcsKind, Preset, public_io_words_to_digest_words,
run_e2e_full_trace_verify, run_e2e_single_shard_debug_verify, run_e2e_with_checkpoint,
run_e2e_verify, run_e2e_with_checkpoint,
setup_platform, setup_platform_debug,
},
scheme::{
ZKVMProof, constants::MAX_NUM_VARIABLES, create_backend, create_prover, hal::ProverDevice,
mock_prover::LkMultiplicityKey, verifier::ZKVMVerifier,
mock_prover::LkMultiplicityKey,
verifier::{VerifierMode, ZKVMVerifier},
},
with_panic_hook,
};
Expand Down Expand Up @@ -352,17 +353,15 @@ fn run_inner<
fs::write(&vk_file, vk_bytes).unwrap();

if checkpoint > Checkpoint::PrepVerify {
let verifier = ZKVMVerifier::new(vk);
if target_shard_id.is_some() {
run_e2e_single_shard_debug_verify(
&verifier,
zkvm_proofs.first().cloned().expect("missing shard proof"),
None,
max_steps,
);
// `target_shard_id.is_some()` => debug single-shard path; otherwise
// this binary has no exit code to pin, so treat as prefix run.
let mode = if target_shard_id.is_some() {
VerifierMode::DebugSegment
} else {
run_e2e_full_trace_verify(&verifier, zkvm_proofs.clone(), None, max_steps);
}
VerifierMode::PrefixRun
};
let verifier = ZKVMVerifier::new_with_mode(vk, mode);
run_e2e_verify(&verifier, zkvm_proofs.clone(), None, max_steps);
soundness_test(zkvm_proofs.first().cloned().unwrap(), &verifier);
}
}
Expand Down
80 changes: 23 additions & 57 deletions ceno_zkvm/src/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
mock_prover::{LkMultiplicityKey, MockProver},
prover::ZKVMProver,
septic_curve::SepticPoint,
verifier::ZKVMVerifier,
verifier::{VerifierMode, ZKVMVerifier},
},
structs::{
ProgramParams, ZKVMConstraintSystem, ZKVMFixedTraces, ZKVMProvingKey, ZKVMVerifyingKey,
Expand Down Expand Up @@ -1767,38 +1767,31 @@ pub fn run_e2e_with_checkpoint<
);
}

let verifier = ZKVMVerifier::new(vk.clone());
// Commit to the verifier's statement at construction time using the
// caller's out-of-band knowledge (--shard-id / max-steps outcome), NOT
// anything the prover sent. This prevents the prover from influencing
// which statement the verifier enforces.
let mode = if target_shard_id.is_some() {
VerifierMode::DebugSegment
} else if exit_code.is_some() {
VerifierMode::FullRun
} else {
VerifierMode::PrefixRun
};
let verifier = ZKVMVerifier::new_with_mode(vk.clone(), mode);

if let Checkpoint::PrepVerify = checkpoint {
return E2ECheckpointResult {
proofs: Some(zkvm_proofs.clone()),
vk: Some(vk),
next_step: Some(Box::new(move || match target_shard_id {
Some(_) => run_e2e_single_shard_debug_verify(
&verifier,
zkvm_proofs.into_iter().next().expect("missing shard proof"),
exit_code,
max_steps,
),
None => run_e2e_full_trace_verify(&verifier, zkvm_proofs, exit_code, max_steps),
next_step: Some(Box::new(move || {
run_e2e_verify(&verifier, zkvm_proofs, exit_code, max_steps)
})),
};
}

let start = std::time::Instant::now();
match target_shard_id {
Some(_) => run_e2e_single_shard_debug_verify(
&verifier,
zkvm_proofs
.clone()
.into_iter()
.next()
.expect("missing shard proof"),
exit_code,
max_steps,
),
None => run_e2e_full_trace_verify(&verifier, zkvm_proofs.clone(), exit_code, max_steps),
}
run_e2e_verify(&verifier, zkvm_proofs.clone(), exit_code, max_steps);
tracing::debug!("verified in {:?}", start.elapsed());

E2ECheckpointResult {
Expand Down Expand Up @@ -2035,11 +2028,11 @@ fn create_proofs_streaming<
proofs
}

/// Verify the full produced trace in the normal e2e flow.
///
/// This is the production-style verification path used when e2e is not scoped
/// to a debug `--shard-id` run.
pub fn run_e2e_full_trace_verify<E: ExtensionField, PCS: PolynomialCommitmentScheme<E>>(
/// Verify the produced proofs. The statement being verified (full run,
/// prefix run, or single-shard debug segment) is encoded in the verifier's
/// mode, which was chosen by the caller from out-of-band knowledge at
/// verifier construction time.
pub fn run_e2e_verify<E: ExtensionField, PCS: PolynomialCommitmentScheme<E>>(
verifier: &ZKVMVerifier<E, PCS>,
zkvm_proofs: Vec<ZKVMProof<E, PCS>>,
exit_code: Option<u32>,
Expand All @@ -2048,34 +2041,8 @@ pub fn run_e2e_full_trace_verify<E: ExtensionField, PCS: PolynomialCommitmentSch
let transcripts = (0..zkvm_proofs.len())
.map(|_| Transcript::new(b"riscv"))
.collect_vec();
let expect_halt = zkvm_proofs
.last()
.map(|proof| proof.has_halt(&verifier.vk))
.unwrap_or(exit_code.is_some());
let verified = verifier
.verify_full_trace_proofs_halt(zkvm_proofs, transcripts, expect_halt)
.expect("verify proof return with error");
assert!(verified);
match exit_code {
Some(0) => tracing::info!("exit code 0. Success."),
Some(code) => tracing::error!("exit code {}. Failure.", code),
None => tracing::error!("Unfinished execution. max_steps={:?}.", max_steps),
}
}

/// Verify a single produced shard as a standalone debug segment.
///
/// This path is only for explicit e2e `--shard-id` runs where exactly one proof
/// is produced. It intentionally does not claim full-trace verification.
pub fn run_e2e_single_shard_debug_verify<E: ExtensionField, PCS: PolynomialCommitmentScheme<E>>(
verifier: &ZKVMVerifier<E, PCS>,
zkvm_proof: ZKVMProof<E, PCS>,
exit_code: Option<u32>,
max_steps: usize,
) {
let expect_halt = zkvm_proof.has_halt(&verifier.vk) || exit_code.is_some();
let verified = verifier
.verify_single_shard_segment_halt(zkvm_proof, Transcript::new(b"riscv"), expect_halt)
.verify_proofs(zkvm_proofs, transcripts)
.expect("verify proof return with error");
assert!(verified);
match exit_code {
Expand Down Expand Up @@ -2145,11 +2112,10 @@ pub fn verify<E: ExtensionField, PCS: PolynomialCommitmentScheme<E> + serde::Ser
{
Instrumented::<<<E as ExtensionField>::BaseField as PoseidonField>::P>::clear_metrics();
}
let has_halt = zkvm_proofs.last().unwrap().has_halt(&verifier.vk);
let transcripts = (0..zkvm_proofs.len())
.map(|_| Transcript::new(b"riscv"))
.collect_vec();
verifier.verify_full_trace_proofs_halt(zkvm_proofs, transcripts, has_halt)?;
verifier.verify_proofs(zkvm_proofs, transcripts)?;
// print verification statistics such as hash count
#[cfg(debug_assertions)]
{
Expand Down
Loading
Loading