Skip to content

Commit bf688c3

Browse files
kunxian-xiaclaude
andauthored
feat(recursion-v2): add leaf verifier prover wiring (#1367)
Closes #1365. Stacked on #1363. ## Summary - Add `AggProver<LEAF_FANIN, INTERNAL_FANIN>` orchestrating the leaf → internal → root recursion pipeline with configurable fanin and system params per layer - Add `rebuild_circuit_index()` on `ZKVMVerifyingKey` for post-deserialization reconstruction of the serde-skipped index map - Fix `PvsAirConsistencyBus` LogUp imbalance (uncomment `add_key_with_lookups` in VerifierPvsAir) - Use `--pcs=basefold` in fixture generation to match the test's expected PCS type - Rewrite e2e tests to use `AggProver` API ## Test plan - `bash ceno_recursion_v2/scripts/run_e2e_test.sh` (generates fixtures + runs all prover_integration tests) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 241923a commit bf688c3

14 files changed

Lines changed: 577 additions & 91 deletions

File tree

.github/workflows/integration.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,11 @@ jobs:
189189
CENO_CROSS_SHARD_LIMIT: 32
190190
run: cargo run --release --package ceno_zkvm --features sanity-check --bin e2e -- --platform=ceno --max-cycle-per-shard=20000 --hints=10 --public-io=4191 examples/target/riscv32im-ceno-zkvm-elf/release/examples/fibonacci
191191

192-
- name: Run aggregation e2e
192+
- name: Run recursion v2 aggregation e2e
193193
env:
194194
RUSTFLAGS: "-C opt-level=3"
195-
run: |
196-
cargo run --release --package ceno_recursion --bin e2e_aggregate -- --platform=ceno --max-cycle-per-shard=1600 examples/target/riscv32im-ceno-zkvm-elf/release/examples/keccak_syscall
195+
RUST_MIN_STACK: "33554432"
196+
run: bash ceno_recursion_v2/scripts/run_e2e_test.sh
197197

198198
- name: Install cargo make
199199
run: |

.github/workflows/tests.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,11 @@ jobs:
5454
run: |
5555
cargo make --version || cargo install cargo-make
5656
57-
# - name: Run aggregation tests
57+
# - name: Run recursion v2 aggregation tests
5858
# env:
5959
# RUSTFLAGS: "" # cleanup RUSTFLAGS and avoid target-cpu=native
60-
# run: |
61-
# cargo run --release --package ceno_zkvm --bin e2e -- \
62-
# --platform=ceno --max-cycle-per-shard=1600 \
63-
# examples/target/riscv32im-ceno-zkvm-elf/release/examples/keccak_syscall
64-
# mkdir -p ceno_recursion/src/imported
65-
# mv proof.bin ceno_recursion/src/imported/
66-
# mv vk.bin ceno_recursion/src/imported/
67-
# cargo test --release --package ceno_recursion --tests -- --ignored
60+
# RUST_MIN_STACK: "33554432"
61+
# run: bash ceno_recursion_v2/scripts/run_e2e_test.sh
6862

6963
- name: run test
7064
env:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# Run the recursion v2 e2e integration test.
3+
# Generates base-layer proof fixtures if they don't exist.
4+
set -euo pipefail
5+
6+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
7+
FIXTURE_DIR="${CENO_RECURSION_V2_FIXTURE_DIR:-$REPO_ROOT/ceno_recursion_v2/src/imported}"
8+
PROOF_FILE="$FIXTURE_DIR/proof.bin"
9+
VK_FILE="$FIXTURE_DIR/vk.bin"
10+
FORCE_REGEN="${CENO_RECURSION_V2_FORCE_REGEN:-0}"
11+
MAX_CYCLE_PER_SHARD="${CENO_RECURSION_V2_MAX_CYCLE_PER_SHARD:-16000}"
12+
HINTS="${CENO_RECURSION_V2_HINTS:-10}"
13+
PUBLIC_IO="${CENO_RECURSION_V2_PUBLIC_IO:-4191}"
14+
15+
# --- Step 1: ensure fixtures exist ---
16+
17+
if [[ "$FORCE_REGEN" != "1" && -f "$PROOF_FILE" && -f "$VK_FILE" ]]; then
18+
echo "[fixtures] found:"
19+
ls -lh "$PROOF_FILE" "$VK_FILE"
20+
else
21+
echo "[fixtures] generating base-layer proofs (fibonacci, --max-cycle-per-shard=$MAX_CYCLE_PER_SHARD) ..."
22+
23+
ELF="$REPO_ROOT/examples/target/riscv32im-ceno-zkvm-elf/release/examples/fibonacci"
24+
if [[ ! -f "$ELF" ]]; then
25+
echo "[fixtures] building example ELFs..."
26+
cargo build --release --manifest-path "$REPO_ROOT/examples/Cargo.toml" --examples
27+
fi
28+
29+
mkdir -p "$FIXTURE_DIR"
30+
cargo run --release --package ceno_zkvm --bin e2e -- \
31+
--platform=ceno \
32+
--pcs=basefold \
33+
--max-cycle-per-shard="$MAX_CYCLE_PER_SHARD" \
34+
--hints="$HINTS" \
35+
--public-io="$PUBLIC_IO" \
36+
"$ELF" \
37+
"$PROOF_FILE" \
38+
"$VK_FILE"
39+
40+
echo "[fixtures] generated:"
41+
ls -lh "$PROOF_FILE" "$VK_FILE"
42+
fi
43+
44+
# --- Step 2: run the e2e integration test ---
45+
46+
echo ""
47+
echo "[test] running recursion v2 e2e tests ..."
48+
cd "$REPO_ROOT/ceno_recursion_v2"
49+
CENO_RECURSION_V2_FIXTURE_DIR="$FIXTURE_DIR" \
50+
RUST_MIN_STACK=33554432 \
51+
cargo test --release \
52+
'continuation::tests::prover_integration' \
53+
-- --nocapture

ceno_recursion_v2/src/batch_constraint/expr_eval/symbolic_expression/trace.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,7 @@ impl RowMajorChip<F> for SymbolicExpressionTraceGenerator {
227227

228228
let mut node_idx = constraints.nodes.len();
229229
for unused_var in &vk.unused_variables {
230-
if matches!(
231-
unused_var.entry,
232-
Entry::Public | Entry::Challenge
233-
) {
230+
if matches!(unused_var.entry, Entry::Public | Entry::Challenge) {
234231
continue;
235232
}
236233
let mut args = [F::ZERO; 2 * D_EF];

ceno_recursion_v2/src/batch_constraint/mod.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ use openvm_cpu_backend::CpuBackend;
55
use openvm_poseidon2_air::POSEIDON2_WIDTH;
66
use openvm_stark_backend::{
77
AirRef, FiatShamirTranscript, StarkEngine, StarkProtocolConfig, TranscriptHistory,
8-
prover::{CommittedTraceData, TraceCommitter},
8+
prover::{AirProvingContext, CommittedTraceData, TraceCommitter},
99
};
1010
use openvm_stark_sdk::config::baby_bear_poseidon2::{BabyBearPoseidon2Config, EF, F};
1111
use p3_field::PrimeCharacteristicRing;
12-
use p3_matrix::dense::RowMajorMatrix;
12+
use p3_matrix::{Matrix, dense::RowMajorMatrix};
1313

1414
use crate::{
1515
batch_constraint::{
@@ -19,10 +19,7 @@ use crate::{
1919
},
2020
expr_eval::{
2121
ConstraintsFoldingAir, ConstraintsFoldingCols,
22-
symbolic_expression::{
23-
CachedSymbolicExpressionColumns, SingleMainSymbolicExpressionColumns,
24-
SymbolicExpressionAir,
25-
},
22+
symbolic_expression::{SingleMainSymbolicExpressionColumns, SymbolicExpressionAir},
2623
},
2724
expression_claim::{ExpressionClaimAir, ExpressionClaimCols},
2825
},
@@ -184,14 +181,47 @@ impl BatchConstraintModule {
184181
};
185182
}
186183

187-
fn placeholder_air_widths(&self) -> [usize; 3] {
184+
fn placeholder_common_widths(&self) -> [usize; 3] {
188185
[
189-
CachedSymbolicExpressionColumns::<u8>::width()
190-
+ SingleMainSymbolicExpressionColumns::<u8>::width() * self.max_num_proofs,
186+
SingleMainSymbolicExpressionColumns::<u8>::width() * self.max_num_proofs,
191187
ConstraintsFoldingCols::<u8>::width(),
192188
ExpressionClaimCols::<u8>::width(),
193189
]
194190
}
191+
192+
pub(crate) fn generate_placeholder_proving_ctxs<SC: StarkProtocolConfig<F = F>>(
193+
&self,
194+
cached_symbolic_expr_trace: CommittedTraceData<CpuBackend<SC>>,
195+
required_heights: Option<&[usize]>,
196+
) -> Option<Vec<AirProvingContext<CpuBackend<SC>>>> {
197+
let widths = self.placeholder_common_widths();
198+
let air_count = required_heights
199+
.map(|heights| heights.len())
200+
.unwrap_or(self.num_airs());
201+
let cached_height = cached_symbolic_expr_trace.trace.height();
202+
203+
(0..air_count)
204+
.map(|idx| {
205+
let height = required_heights
206+
.and_then(|heights| heights.get(idx).copied())
207+
.unwrap_or_else(|| if idx == 0 { cached_height } else { 2 });
208+
if height < 2 {
209+
return None;
210+
}
211+
let width = widths.get(idx).copied().unwrap_or(1).max(1);
212+
let matrix = RowMajorMatrix::new(vec![F::ZERO; height * width], width);
213+
Some(if idx == 0 {
214+
AirProvingContext {
215+
cached_mains: vec![cached_symbolic_expr_trace.clone()],
216+
common_main: matrix,
217+
public_values: vec![],
218+
}
219+
} else {
220+
AirProvingContext::simple_no_pis(matrix)
221+
})
222+
})
223+
.collect::<Option<Vec<_>>>()
224+
}
195225
}
196226

197227
impl AirModule for BatchConstraintModule {
@@ -251,7 +281,7 @@ impl<SC: StarkProtocolConfig<F = F>> TraceGenModule<GlobalCtxCpu, CpuBackend<SC>
251281
_ctx: &Self::ModuleSpecificCtx<'_>,
252282
required_heights: Option<&[usize]>,
253283
) -> Option<Vec<openvm_stark_backend::prover::AirProvingContext<CpuBackend<SC>>>> {
254-
let widths = self.placeholder_air_widths();
284+
let widths = self.placeholder_common_widths();
255285
let air_count = required_heights
256286
.map(|heights| heights.len())
257287
.unwrap_or(self.num_airs());

ceno_recursion_v2/src/circuit/inner/verifier/air.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,17 +246,15 @@ impl<AB: AirBuilder + InteractionBuilder + AirBuilderWithPublicValues> Air<AB> f
246246
// local.is_valid * is_internal,
247247
// );
248248

249-
// We provide proof metadata for lookup here to ensure consistency between AIRs that
250-
// process public values.
251-
// self.pvs_air_consistency_bus.add_key_with_lookups(
252-
// builder,
253-
// local.proof_idx,
254-
// PvsAirConsistencyMessage {
255-
// deferral_flag,
256-
// has_verifier_pvs: local.has_verifier_pvs.into(),
257-
// },
258-
// local.is_valid * consistency_mult,
259-
// );
249+
self.pvs_air_consistency_bus.add_key_with_lookups(
250+
builder,
251+
local.proof_idx,
252+
PvsAirConsistencyMessage {
253+
deferral_flag,
254+
has_verifier_pvs: local.has_verifier_pvs.into(),
255+
},
256+
local.is_valid * consistency_mult,
257+
);
260258

261259
// Finally, we need to constrain that the public values this AIR produces are consistent
262260
// with the child's. Note that we only impose constraints for layers below the current

ceno_recursion_v2/src/continuation/prover/inner/mod.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::Arc;
1+
use std::{sync::Arc, time::Instant};
22

33
use ceno_zkvm::scheme::ZKVMProof;
44
use continuations_v2::SC;
@@ -9,7 +9,10 @@ use openvm_stark_backend::{
99
StarkEngine,
1010
keygen::types::{MultiStarkProvingKey, MultiStarkVerifyingKey},
1111
proof::Proof,
12-
prover::{CommittedTraceData, DeviceMultiStarkProvingKey, ProverBackend, ProvingContext},
12+
prover::{
13+
AirProvingContext, CommittedTraceData, DeviceMultiStarkProvingKey, MatrixDimensions,
14+
ProverBackend, ProvingContext,
15+
},
1316
};
1417
use openvm_stark_sdk::config::baby_bear_poseidon2::{
1518
Digest, EF, F, default_duplex_sponge_recorder,
@@ -183,18 +186,32 @@ where
183186
proofs: &[ZKVMProof<RecursionField, Basefold<RecursionField, BasefoldRSParams>>],
184187
child_vk_kind: ChildVkKind,
185188
) -> Result<Proof<SC>> {
189+
let tracegen_start = Instant::now();
186190
let ctx = self.generate_proving_ctx(proofs, child_vk_kind, ProofsType::Vm, None);
187-
if tracing::enabled!(tracing::Level::DEBUG) {
188-
// TODO enable trace height
189-
// trace_heights_tracing_info::<_, SC>(&ctx.per_trace, &self.circuit.airs());
191+
tracing::info!(
192+
elapsed_ms = tracegen_start.elapsed().as_secs_f64() * 1000.0,
193+
num_traces = ctx.per_trace.len(),
194+
"generated recursion proving context"
195+
);
196+
if tracing::enabled!(tracing::Level::INFO) {
197+
trace_heights_tracing_info::<PB, SC>(&ctx.per_trace, &self.circuit.airs());
190198
}
191199

192200
let engine = E::new(self.pk.params.clone());
193201
#[cfg(debug_assertions)]
194202
debug_constraints(&self.circuit, &ctx, &engine);
203+
let prove_start = Instant::now();
195204
let proof = engine.prove(&self.d_pk, ctx)?;
196-
#[cfg(debug_assertions)]
205+
tracing::info!(
206+
elapsed_ms = prove_start.elapsed().as_secs_f64() * 1000.0,
207+
"proved recursion aggregation"
208+
);
209+
let verify_start = Instant::now();
197210
engine.verify(&self.vk, &proof)?;
211+
tracing::info!(
212+
elapsed_ms = verify_start.elapsed().as_secs_f64() * 1000.0,
213+
"verified recursion aggregation proof"
214+
);
198215
Ok(proof)
199216
}
200217

@@ -272,10 +289,42 @@ where
272289
self.vk.clone()
273290
}
274291

292+
#[cfg(test)]
293+
pub(crate) fn air_names(&self) -> Vec<String> {
294+
<InnerCircuit<S> as Circuit<SC>>::airs(self.circuit.as_ref())
295+
.iter()
296+
.map(|air| air.name().to_string())
297+
.collect()
298+
}
299+
275300
pub fn get_self_vk_pcs_data(&self) -> Option<CommittedTraceData<PB>>
276301
where
277302
CommittedTraceData<PB>: Clone,
278303
{
279304
self.self_vk_pcs_data.clone()
280305
}
281306
}
307+
308+
fn trace_heights_tracing_info<PB: ProverBackend, SC: openvm_stark_backend::StarkProtocolConfig>(
309+
ctxs: &[(usize, AirProvingContext<PB>)],
310+
airs: &[openvm_stark_backend::AirRef<SC>],
311+
) {
312+
let mut total_cells = 0usize;
313+
let mut total_width = 0usize;
314+
for ((air_id, ctx), air) in ctxs.iter().zip(airs) {
315+
let height = ctx.common_main.height();
316+
let width = ctx.common_main.width();
317+
let cells = height * width;
318+
tracing::info!(
319+
air_id,
320+
air_name = air.name(),
321+
height,
322+
width,
323+
cells,
324+
"recursion trace dimensions"
325+
);
326+
total_cells += cells;
327+
total_width += width;
328+
}
329+
tracing::info!(total_cells, total_width, "recursion trace totals");
330+
}

0 commit comments

Comments
 (0)