Skip to content

Commit 24e9727

Browse files
committed
Support single-shard e2e verification
1 parent 2fe5ec1 commit 24e9727

4 files changed

Lines changed: 94 additions & 16 deletions

File tree

ceno_recursion/src/bin/e2e_aggregate.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ struct Args {
106106
// max cycle per shard
107107
#[arg(long, default_value = "536870912")] // 536870912 = 2^29
108108
max_cycle_per_shard: u64,
109+
110+
/// Restrict proving to a single shard id for debugging.
111+
#[arg(long)]
112+
shard_id: Option<u64>,
109113
}
110114

111115
fn main() {
@@ -221,6 +225,7 @@ fn main() {
221225

222226
let max_steps = args.max_steps.unwrap_or(usize::MAX);
223227
let public_io_digest = public_io_words_to_digest_words(&public_io);
228+
let target_shard_id = args.shard_id.map(|v| v as usize);
224229
let multi_prover = MultiProver::new(
225230
args.prover_id as usize,
226231
args.num_provers as usize,
@@ -240,7 +245,7 @@ fn main() {
240245
public_io_digest,
241246
max_steps,
242247
Checkpoint::Complete,
243-
None,
248+
target_shard_id,
244249
);
245250

246251
let zkvm_proofs = result

ceno_zkvm/src/bin/e2e.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,8 @@ fn run_inner<
350350
let vk_bytes = bincode::serialize(&vk).unwrap();
351351
fs::write(&vk_file, vk_bytes).unwrap();
352352

353-
if checkpoint > Checkpoint::PrepVerify && target_shard_id.is_none() {
353+
if checkpoint > Checkpoint::PrepVerify && (target_shard_id.is_none() || zkvm_proofs.len() == 1)
354+
{
354355
let verifier = ZKVMVerifier::new(vk);
355356
verify(zkvm_proofs.clone(), &verifier).expect("Verification failed");
356357
soundness_test(zkvm_proofs.first().cloned().unwrap(), &verifier);

ceno_zkvm/src/e2e.rs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,8 +1759,10 @@ pub fn run_e2e_with_checkpoint<
17591759
&init_full_mem,
17601760
);
17611761

1762-
if target_shard_id.is_some() {
1763-
// skip verify as the proof are in-completed
1762+
let can_verify_target_shard = target_shard_id.is_none() || zkvm_proofs.len() == 1;
1763+
if !can_verify_target_shard {
1764+
// Partial multi-shard subsets still skip verification because the
1765+
// continuation chain between omitted shards is unavailable.
17641766
return E2ECheckpointResult {
17651767
proofs: Some(zkvm_proofs),
17661768
vk: Some(vk),
@@ -1775,13 +1777,25 @@ pub fn run_e2e_with_checkpoint<
17751777
proofs: Some(zkvm_proofs.clone()),
17761778
vk: Some(vk),
17771779
next_step: Some(Box::new(move || {
1778-
run_e2e_verify(&verifier, zkvm_proofs, exit_code, max_steps)
1780+
run_e2e_verify(
1781+
&verifier,
1782+
zkvm_proofs,
1783+
exit_code,
1784+
max_steps,
1785+
target_shard_id,
1786+
)
17791787
})),
17801788
};
17811789
}
17821790

17831791
let start = std::time::Instant::now();
1784-
run_e2e_verify(&verifier, zkvm_proofs.clone(), exit_code, max_steps);
1792+
run_e2e_verify(
1793+
&verifier,
1794+
zkvm_proofs.clone(),
1795+
exit_code,
1796+
max_steps,
1797+
target_shard_id,
1798+
);
17851799
tracing::debug!("verified in {:?}", start.elapsed());
17861800

17871801
E2ECheckpointResult {
@@ -2023,15 +2037,29 @@ pub fn run_e2e_verify<E: ExtensionField, PCS: PolynomialCommitmentScheme<E>>(
20232037
zkvm_proofs: Vec<ZKVMProof<E, PCS>>,
20242038
exit_code: Option<u32>,
20252039
max_steps: usize,
2040+
target_shard_id: Option<usize>,
20262041
) {
20272042
let transcripts = (0..zkvm_proofs.len())
20282043
.map(|_| Transcript::new(b"riscv"))
20292044
.collect_vec();
2030-
assert!(
2045+
let expect_halt = zkvm_proofs
2046+
.last()
2047+
.map(|proof| proof.has_halt(&verifier.vk))
2048+
.unwrap_or(exit_code.is_some());
2049+
let verified = if target_shard_id.is_some() && zkvm_proofs.len() == 1 {
20312050
verifier
2032-
.verify_proofs_halt(zkvm_proofs, transcripts, exit_code.is_some())
2033-
.expect("verify proof return with error"),
2034-
);
2051+
.verify_shard_proof_halt(
2052+
zkvm_proofs.into_iter().next().unwrap(),
2053+
transcripts.into_iter().next().unwrap(),
2054+
expect_halt,
2055+
)
2056+
.expect("verify proof return with error")
2057+
} else {
2058+
verifier
2059+
.verify_proofs_halt(zkvm_proofs, transcripts, expect_halt)
2060+
.expect("verify proof return with error")
2061+
};
2062+
assert!(verified);
20352063
match exit_code {
20362064
Some(0) => tracing::info!("exit code 0. Success."),
20372065
Some(code) => tracing::error!("exit code {}. Failure.", code),
@@ -2099,11 +2127,19 @@ pub fn verify<E: ExtensionField, PCS: PolynomialCommitmentScheme<E> + serde::Ser
20992127
{
21002128
Instrumented::<<<E as ExtensionField>::BaseField as PoseidonField>::P>::clear_metrics();
21012129
}
2102-
let transcripts = (0..zkvm_proofs.len())
2103-
.map(|_| Transcript::new(b"riscv"))
2104-
.collect_vec();
21052130
let has_halt = zkvm_proofs.last().unwrap().has_halt(&verifier.vk);
2106-
verifier.verify_proofs_halt(zkvm_proofs, transcripts, has_halt)?;
2131+
if zkvm_proofs.len() == 1 {
2132+
verifier.verify_shard_proof_halt(
2133+
zkvm_proofs.into_iter().next().unwrap(),
2134+
Transcript::new(b"riscv"),
2135+
has_halt,
2136+
)?;
2137+
} else {
2138+
let transcripts = (0..zkvm_proofs.len())
2139+
.map(|_| Transcript::new(b"riscv"))
2140+
.collect_vec();
2141+
verifier.verify_proofs_halt(zkvm_proofs, transcripts, has_halt)?;
2142+
}
21072143
// print verification statistics such as hash count
21082144
#[cfg(debug_assertions)]
21092145
{

ceno_zkvm/src/scheme/verifier.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,35 @@ impl<E: ExtensionField, PCS: PolynomialCommitmentScheme<E>> ZKVMVerifier<E, PCS>
121121
self.verify_proofs_halt(vec![vm_proof], vec![transcript], expect_halt)
122122
}
123123

124+
/// Verify a single shard proof as a standalone segment.
125+
///
126+
/// Unlike `verify_proof_halt`, this checks proof validity and halt/segment
127+
/// invariants for one shard only and intentionally skips cross-shard
128+
/// continuation checks such as init_pc/heap chaining.
129+
pub fn verify_shard_proof_halt(
130+
&self,
131+
vm_proof: ZKVMProof<E, PCS>,
132+
transcript: impl ForkableTranscript<E>,
133+
expect_halt: bool,
134+
) -> Result<bool, ZKVMError> {
135+
let has_halt = vm_proof.has_halt(&self.vk);
136+
if has_halt != expect_halt {
137+
return Err(ZKVMError::VerifyError(
138+
format!("shard proof ecall/halt mismatch: expected {expect_halt} != {has_halt}",)
139+
.into(),
140+
));
141+
}
142+
143+
assert_eq!(
144+
vm_proof.public_values.query_by_index::<E>(INIT_CYCLE_IDX),
145+
E::BaseField::from_canonical_u64(Tracer::SUBCYCLES_PER_INSN)
146+
);
147+
148+
let shard_id = vm_proof.public_values.shard_id as usize;
149+
self.verify_proof_validity(shard_id, vm_proof, transcript)?;
150+
Ok(true)
151+
}
152+
124153
/// Verify a trace from start to optional halt.
125154
pub fn verify_proofs_halt(
126155
&self,
@@ -237,8 +266,15 @@ impl<E: ExtensionField, PCS: PolynomialCommitmentScheme<E>> ZKVMVerifier<E, PCS>
237266
}
238267
}
239268

240-
// check shard id
241-
assert_eq!(vm_proof.public_values.shard_id, shard_id as u32);
269+
if vm_proof.public_values.shard_id != shard_id as u32 {
270+
return Err(ZKVMError::VerifyError(
271+
format!(
272+
"proof shard_id mismatch: expected {} != {}",
273+
shard_id, vm_proof.public_values.shard_id
274+
)
275+
.into(),
276+
));
277+
}
242278

243279
// write fixed commitment to transcript
244280
// TODO check soundness if there is no fixed_commit but got fixed proof?

0 commit comments

Comments
 (0)