openvm_recursion_circuit/system/
mod.rs

1//! Traits and types describing the core interfaces of the verifier sub-circuit. The verifier
2//! sub-circuit verifies multiple proofs for the same child verifying key. It supports **recursive**
3//! verification, where the child verifying key is equal to the verifying key of the verifier
4//! circuit itself.
5use std::{iter, sync::Arc};
6
7use openvm_cpu_backend::CpuBackend;
8#[cfg(feature = "cuda")]
9use openvm_cuda_common::stream::GpuDeviceCtx;
10use openvm_poseidon2_air::POSEIDON2_WIDTH;
11use openvm_stark_backend::{
12    interaction::BusIndex,
13    keygen::types::{LinearConstraint, MultiStarkVerifyingKey},
14    proof::{Proof, TraceVData},
15    prover::{AirProvingContext, CommittedTraceData, ProverBackend},
16    AirRef, EngineDeviceCtx, FiatShamirTranscript, StarkEngine, StarkProtocolConfig,
17    TranscriptHistory, TranscriptLog,
18};
19use openvm_stark_sdk::config::baby_bear_poseidon2::{BabyBearPoseidon2Config, CHUNK, EF, F};
20use p3_field::BasedVectorSpace;
21use p3_maybe_rayon::prelude::*;
22
23use crate::{
24    batch_constraint::{
25        expr_eval::CachedTraceRecord, BatchConstraintModule, LOCAL_SYMBOLIC_EXPRESSION_AIR_IDX,
26    },
27    bus::{
28        AirPresenceBus, AirShapeBus, BatchConstraintModuleBus, CachedCommitBus, ColumnClaimsBus,
29        CommitmentsBus, ConstraintSumcheckRandomnessBus, ConstraintsFoldingInputBus, Eq3bShapeBus,
30        EqNegBaseRandBus, EqNegResultBus, EqNsNLogupMaxBus, ExpressionClaimNMaxBus,
31        FinalTranscriptStateBus, FractionFolderInputBus, GkrModuleBus, HyperdimBus,
32        InteractionsFoldingInputBus, LiftedHeightsBus, MerkleVerifyBus, NLiftBus,
33        Poseidon2CompressBus, Poseidon2PermuteBus, PreHashBus, PublicValuesBus, SelUniBus,
34        StackingIndicesBus, StackingModuleBus, TranscriptBus, WhirModuleBus, WhirMuBus,
35        WhirOpeningPointBus, WhirOpeningPointLookupBus, XiRandomnessBus,
36    },
37    gkr::GkrModule,
38    primitives::{
39        bus::{ExpBitsLenBus, PowerCheckerBus, RangeCheckerBus, RightShiftBus},
40        exp_bits_len::{ExpBitsLenAir, ExpBitsLenCpuTraceGenerator},
41        pow::{PowerCheckerAir, PowerCheckerCpuTraceGenerator},
42    },
43    proof_shape::ProofShapeModule,
44    stacking::StackingModule,
45    transcript::TranscriptModule,
46    utils::poseidon2_hash_slice_with_states,
47    whir::WhirModule,
48};
49
50mod dummy;
51pub(crate) mod frame;
52
53pub use frame::check_param_compatibility;
54
55const BATCH_CONSTRAINT_MOD_IDX: usize = 0;
56pub(crate) const POW_CHECKER_HEIGHT: usize = 32;
57
58pub enum CachedTraceCtx<PB: ProverBackend> {
59    PcsData(CommittedTraceData<PB>),
60    Records(CachedTraceRecord),
61}
62
63#[derive(Debug, Copy, Clone)]
64pub struct VerifierConfig {
65    pub continuations_enabled: bool,
66    pub final_state_bus_enabled: bool,
67    pub has_cached: bool,
68}
69
70impl Default for VerifierConfig {
71    fn default() -> Self {
72        Self {
73            continuations_enabled: false,
74            final_state_bus_enabled: false,
75            has_cached: true,
76        }
77    }
78}
79
80#[derive(Debug)]
81pub struct VerifierExternalData<'a> {
82    pub poseidon2_compress_inputs: &'a Vec<[F; POSEIDON2_WIDTH]>,
83    pub poseidon2_permute_inputs: &'a Vec<[F; POSEIDON2_WIDTH]>,
84    pub range_check_inputs: &'a Vec<usize>,
85    pub power_check_inputs: &'a Vec<usize>,
86    pub required_heights: Option<&'a [usize]>,
87    pub final_transcript_state: Option<&'a mut [F; POSEIDON2_WIDTH]>,
88}
89
90// Trait to make tracegen functions generic on ProverBackend.
91// `DC` is the device context type (e.g., `GpuDeviceCtx` for GPU, `()` for CPU).
92pub trait VerifierTraceGen<
93    PB: ProverBackend,
94    SC: StarkProtocolConfig<F = F>,
95    DC: Clone + Send + Sync,
96>
97{
98    fn new(
99        child_mvk: Arc<MultiStarkVerifyingKey<BabyBearPoseidon2Config>>,
100        config: VerifierConfig,
101    ) -> Self;
102
103    fn commit_child_vk<E: StarkEngine<SC = SC, PB = PB>>(
104        &self,
105        engine: &E,
106        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
107    ) -> CommittedTraceData<PB>
108    where
109        DC: From<EngineDeviceCtx<E>>;
110
111    fn cached_trace_record(
112        &self,
113        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
114    ) -> CachedTraceRecord;
115
116    /// The generic `TS` allows using different transcript implementations for debugging purposes.
117    /// The default type to use is `DuplexSpongeRecorder`.
118    #[allow(clippy::ptr_arg)]
119    fn generate_proving_ctxs<
120        TS: FiatShamirTranscript<BabyBearPoseidon2Config>
121            + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
122    >(
123        &self,
124        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
125        cached_trace_ctx: CachedTraceCtx<PB>,
126        proofs: &[Proof<BabyBearPoseidon2Config>],
127        external_data: &mut VerifierExternalData,
128        device_ctx: &DC,
129        initial_transcript: TS,
130    ) -> Option<Vec<AirProvingContext<PB>>>;
131
132    fn generate_proving_ctxs_base<
133        TS: FiatShamirTranscript<BabyBearPoseidon2Config>
134            + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
135    >(
136        &self,
137        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
138        cached_trace_ctx: CachedTraceCtx<PB>,
139        proofs: &[Proof<BabyBearPoseidon2Config>],
140        device_ctx: &DC,
141        initial_transcript: TS,
142    ) -> Vec<AirProvingContext<PB>> {
143        let poseidon2_compress_inputs = vec![];
144        let range_check_inputs = vec![];
145        let power_check_inputs = vec![];
146
147        let mut external_data = VerifierExternalData {
148            poseidon2_compress_inputs: &poseidon2_compress_inputs,
149            poseidon2_permute_inputs: &poseidon2_compress_inputs,
150            range_check_inputs: &range_check_inputs,
151            power_check_inputs: &power_check_inputs,
152            required_heights: None,
153            final_transcript_state: None,
154        };
155
156        self.generate_proving_ctxs::<TS>(
157            child_vk,
158            cached_trace_ctx,
159            proofs,
160            &mut external_data,
161            device_ctx,
162            initial_transcript,
163        )
164        .unwrap()
165    }
166}
167
168// Trait to help make AIR generation generic
169pub trait AggregationSubCircuit {
170    fn airs<SC: StarkProtocolConfig<F = F>>(&self) -> Vec<AirRef<SC>>;
171    fn bus_inventory(&self) -> &BusInventory;
172    fn next_bus_idx(&self) -> BusIndex;
173    fn max_num_proofs(&self) -> usize;
174}
175
176pub trait AirModule {
177    fn num_airs(&self) -> usize;
178    fn airs<SC: StarkProtocolConfig<F = F>>(&self) -> Vec<AirRef<SC>>;
179}
180
181/// Trait defining the types for the global input shared across modules for trace generation. These
182/// types are specialized per hardware backend.
183pub trait GlobalTraceGenCtx {
184    /// Verifying key of the child proof to be verified. This is a multi-trace verifying key.
185    type ChildVerifyingKey;
186    /// Type for a collection of proofs.
187    type MultiProof: ?Sized;
188    /// Preflight records corresponding to an instance of `MultiProof`.
189    // NOTE[jpw]: we can add lifetimes if necessary
190    type PreflightRecords: ?Sized;
191}
192
193/// Trait for generating the trace matrices, on device, for a given AIR module.
194/// The module has a view of all proofs being verified as well as the global preflight records from
195/// each proof.
196///
197/// This function should be expected to be called in parallel, one logical thread per module.
198pub trait TraceGenModule<GC: GlobalTraceGenCtx, PB: ProverBackend>: Send + Sync {
199    type ModuleSpecificCtx<'a>;
200
201    fn generate_proving_ctxs(
202        &self,
203        child_vk: &GC::ChildVerifyingKey,
204        proofs: &GC::MultiProof,
205        preflights: &GC::PreflightRecords,
206        ctx: &Self::ModuleSpecificCtx<'_>,
207        required_heights: Option<&[usize]>,
208    ) -> Option<Vec<AirProvingContext<PB>>>;
209}
210
211pub struct GlobalCtxCpu;
212
213impl GlobalTraceGenCtx for GlobalCtxCpu {
214    type ChildVerifyingKey = MultiStarkVerifyingKey<BabyBearPoseidon2Config>;
215    type MultiProof = [Proof<BabyBearPoseidon2Config>];
216    type PreflightRecords = [Preflight];
217}
218
219#[derive(Clone, Copy, Debug, Default)]
220pub struct BusIndexManager {
221    /// All existing buses use indices in [0, bus_idx_max)
222    bus_idx_max: BusIndex,
223}
224
225impl BusIndexManager {
226    pub fn new() -> Self {
227        Self { bus_idx_max: 0 }
228    }
229
230    pub fn new_bus_idx(&mut self) -> BusIndex {
231        let idx = self.bus_idx_max;
232        self.bus_idx_max = self.bus_idx_max.checked_add(1).unwrap();
233        idx
234    }
235}
236
237#[derive(Clone, Debug)]
238pub struct BusInventory {
239    // Control flow buses
240    pub transcript_bus: TranscriptBus,
241    pub poseidon2_permute_bus: Poseidon2PermuteBus,
242    pub poseidon2_compress_bus: Poseidon2CompressBus,
243    pub merkle_verify_bus: MerkleVerifyBus,
244    pub gkr_module_bus: GkrModuleBus,
245    pub bc_module_bus: BatchConstraintModuleBus,
246    pub stacking_module_bus: StackingModuleBus,
247    pub whir_module_bus: WhirModuleBus,
248    pub whir_mu_bus: WhirMuBus,
249
250    // Data buses
251    pub air_shape_bus: AirShapeBus,
252    pub air_presence_bus: AirPresenceBus,
253    pub hyperdim_bus: HyperdimBus,
254    pub lifted_heights_bus: LiftedHeightsBus,
255    pub stacking_indices_bus: StackingIndicesBus,
256    pub commitments_bus: CommitmentsBus,
257    pub public_values_bus: PublicValuesBus,
258    pub column_claims_bus: ColumnClaimsBus,
259    pub range_checker_bus: RangeCheckerBus,
260    pub power_checker_bus: PowerCheckerBus,
261    pub expression_claim_n_max_bus: ExpressionClaimNMaxBus,
262    pub constraints_folding_input_bus: ConstraintsFoldingInputBus,
263    pub interactions_folding_input_bus: InteractionsFoldingInputBus,
264    pub fraction_folder_input_bus: FractionFolderInputBus,
265    pub n_lift_bus: NLiftBus,
266    pub eq_n_logup_n_max_bus: EqNsNLogupMaxBus,
267    pub eq_3b_shape_bus: Eq3bShapeBus,
268
269    // Randomness buses
270    pub xi_randomness_bus: XiRandomnessBus,
271    pub constraint_randomness_bus: ConstraintSumcheckRandomnessBus,
272    pub whir_opening_point_bus: WhirOpeningPointBus,
273    pub whir_opening_point_lookup_bus: WhirOpeningPointLookupBus,
274
275    // Compute buses
276    pub exp_bits_len_bus: ExpBitsLenBus,
277    pub right_shift_bus: RightShiftBus,
278    pub sel_uni_bus: SelUniBus,
279    pub eq_neg_result_bus: EqNegResultBus,
280    pub eq_neg_base_rand_bus: EqNegBaseRandBus,
281
282    // Continuations buses
283    pub cached_commit_bus: CachedCommitBus,
284    pub pre_hash_bus: PreHashBus,
285    pub final_state_bus: FinalTranscriptStateBus,
286}
287
288/// The records from global recursion preflight on CPU for verifying a single proof.
289#[derive(Clone, Debug, Default)]
290pub struct Preflight {
291    /// The concatenated sequence of observes/samples. Not available during preflight; populated
292    /// after.
293    pub transcript: TranscriptLog<F, [F; POSEIDON2_WIDTH]>,
294    pub proof_shape: ProofShapePreflight,
295    pub gkr: GkrPreflight,
296    pub batch_constraint: BatchConstraintPreflight,
297    pub stacking: StackingPreflight,
298    pub whir: WhirPreflight,
299    pub poseidon2_perm_inputs: Vec<[F; POSEIDON2_WIDTH]>,
300    pub poseidon2_compress_inputs: Vec<[F; POSEIDON2_WIDTH]>,
301    pub initial_row_states: Vec<Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>>>,
302    /// Indexed by `[round][query][coset]`. Stores post-permutation state.
303    pub codeword_states: Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>>,
304}
305
306#[derive(Clone, Debug, Default)]
307pub struct ProofShapePreflight {
308    pub sorted_trace_vdata: Vec<(usize, TraceVData<BabyBearPoseidon2Config>)>,
309    pub starting_tidx: Vec<usize>,
310    pub pvs_tidx: Vec<usize>,
311    pub post_tidx: usize,
312    pub n_max: usize,
313    pub n_logup: usize,
314    pub l_skip: usize,
315}
316
317impl ProofShapePreflight {
318    pub fn n_global(&self) -> usize {
319        self.n_max.max(self.n_logup)
320    }
321}
322
323#[derive(Clone, Debug, Default)]
324pub struct GkrPreflight {
325    pub post_tidx: usize,
326    pub xi: Vec<(usize, EF)>,
327}
328
329#[derive(Clone, Debug, Default)]
330pub struct BatchConstraintPreflight {
331    pub lambda_tidx: usize,
332    pub tidx_before_univariate: usize,
333    pub tidx_before_multilinear: usize,
334    pub tidx_before_column_openings: usize,
335    pub post_tidx: usize,
336    pub xi: Vec<EF>,
337    pub sumcheck_rnd: Vec<EF>,
338    pub eq_ns: Vec<EF>,
339    pub eq_sharp_ns: Vec<EF>,
340    pub eq_ns_frontloaded: Vec<EF>,
341    pub eq_sharp_ns_frontloaded: Vec<EF>,
342}
343
344#[derive(Clone, Debug, Default)]
345pub struct StackingPreflight {
346    pub intermediate_tidx: [usize; 3],
347    pub post_tidx: usize,
348    pub univariate_poly_rand_eval: EF,
349    pub stacking_batching_challenge: EF,
350    /// PoW witness for μ batching challenge.
351    pub mu_pow_witness: F,
352    /// PoW sample for μ batching challenge.
353    pub mu_pow_sample: F,
354    pub lambda: EF,
355    pub sumcheck_rnd: Vec<EF>,
356}
357
358#[derive(Clone, Debug, Default)]
359pub struct WhirPreflight {
360    pub whir_round_tidx_per_round: Vec<usize>,
361    pub query_tidx_per_round: Vec<usize>,
362    pub alphas: Vec<EF>,
363    pub z0s: Vec<EF>,
364    pub gammas: Vec<EF>,
365    pub folding_pow_samples: Vec<F>,
366    pub query_pow_samples: Vec<F>,
367    pub queries: Vec<F>,
368}
369
370impl BusInventory {
371    pub(crate) fn new(b: &mut BusIndexManager) -> Self {
372        Self {
373            transcript_bus: TranscriptBus::new(b.new_bus_idx()),
374            poseidon2_permute_bus: Poseidon2PermuteBus::new(b.new_bus_idx()),
375            poseidon2_compress_bus: Poseidon2CompressBus::new(b.new_bus_idx()),
376            merkle_verify_bus: MerkleVerifyBus::new(b.new_bus_idx()),
377
378            // Control flow buses
379            gkr_module_bus: GkrModuleBus::new(b.new_bus_idx()),
380            bc_module_bus: BatchConstraintModuleBus::new(b.new_bus_idx()),
381            stacking_module_bus: StackingModuleBus::new(b.new_bus_idx()),
382            whir_module_bus: WhirModuleBus::new(b.new_bus_idx()),
383            whir_mu_bus: WhirMuBus::new(b.new_bus_idx()),
384
385            // Data buses
386            air_shape_bus: AirShapeBus::new(b.new_bus_idx()),
387            air_presence_bus: AirPresenceBus::new(b.new_bus_idx()),
388            hyperdim_bus: HyperdimBus::new(b.new_bus_idx()),
389            lifted_heights_bus: LiftedHeightsBus::new(b.new_bus_idx()),
390            stacking_indices_bus: StackingIndicesBus::new(b.new_bus_idx()),
391            commitments_bus: CommitmentsBus::new(b.new_bus_idx()),
392            public_values_bus: PublicValuesBus::new(b.new_bus_idx()),
393            sel_uni_bus: SelUniBus::new(b.new_bus_idx()),
394            range_checker_bus: RangeCheckerBus::new(b.new_bus_idx()),
395            power_checker_bus: PowerCheckerBus::new(b.new_bus_idx()),
396            expression_claim_n_max_bus: ExpressionClaimNMaxBus::new(b.new_bus_idx()),
397            constraints_folding_input_bus: ConstraintsFoldingInputBus::new(b.new_bus_idx()),
398            interactions_folding_input_bus: InteractionsFoldingInputBus::new(b.new_bus_idx()),
399            fraction_folder_input_bus: FractionFolderInputBus::new(b.new_bus_idx()),
400            n_lift_bus: NLiftBus::new(b.new_bus_idx()),
401            eq_n_logup_n_max_bus: EqNsNLogupMaxBus::new(b.new_bus_idx()),
402            eq_3b_shape_bus: Eq3bShapeBus::new(b.new_bus_idx()),
403
404            // Randomness buses
405            xi_randomness_bus: XiRandomnessBus::new(b.new_bus_idx()),
406            constraint_randomness_bus: ConstraintSumcheckRandomnessBus::new(b.new_bus_idx()),
407            whir_opening_point_bus: WhirOpeningPointBus::new(b.new_bus_idx()),
408            whir_opening_point_lookup_bus: WhirOpeningPointLookupBus::new(b.new_bus_idx()),
409
410            // Claims buses
411            column_claims_bus: ColumnClaimsBus::new(b.new_bus_idx()),
412
413            exp_bits_len_bus: ExpBitsLenBus::new(b.new_bus_idx()),
414            right_shift_bus: RightShiftBus::new(b.new_bus_idx()),
415            eq_neg_base_rand_bus: EqNegBaseRandBus::new(b.new_bus_idx()),
416            eq_neg_result_bus: EqNegResultBus::new(b.new_bus_idx()),
417
418            // Continuation buses
419            cached_commit_bus: CachedCommitBus::new(b.new_bus_idx()),
420            pre_hash_bus: PreHashBus::new(b.new_bus_idx()),
421            final_state_bus: FinalTranscriptStateBus::new(b.new_bus_idx()),
422        }
423    }
424}
425
426/// A pre-state/post-state pair for a single Poseidon permutation.
427#[repr(C)]
428#[derive(Clone, Copy, Debug)]
429pub struct PoseidonStatePair {
430    pub pre_state: [F; POSEIDON2_WIDTH],
431    pub post_state: [F; POSEIDON2_WIDTH],
432}
433
434struct MerklePrecomputation {
435    poseidon2_perm_inputs: Vec<[F; POSEIDON2_WIDTH]>,
436    poseidon2_compress_inputs: Vec<[F; POSEIDON2_WIDTH]>,
437    initial_row_states: Vec<Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>>>,
438    codeword_states: Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>>,
439}
440
441#[derive(Clone, Copy, strum_macros::Display)]
442enum TraceModuleRef<'a> {
443    Transcript(&'a TranscriptModule),
444    ProofShape(&'a ProofShapeModule),
445    Gkr(&'a GkrModule),
446    BatchConstraint(&'a BatchConstraintModule),
447    Stacking(&'a StackingModule),
448    Whir(&'a WhirModule),
449}
450
451impl<'a> TraceModuleRef<'a> {
452    #[tracing::instrument(
453        name = "wrapper.run_preflight",
454        level = "trace",
455        skip_all,
456        fields(air_module = %self)
457    )]
458    fn run_preflight<TS>(
459        self,
460        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
461        proof: &Proof<BabyBearPoseidon2Config>,
462        preflight: &mut Preflight,
463        sponge: &mut TS,
464    ) where
465        TS: FiatShamirTranscript<BabyBearPoseidon2Config>
466            + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
467    {
468        match self {
469            TraceModuleRef::ProofShape(module) => {
470                module.run_preflight(child_vk, proof, preflight, sponge)
471            }
472            TraceModuleRef::Gkr(module) => module.run_preflight(proof, preflight, sponge),
473            TraceModuleRef::BatchConstraint(module) => {
474                module.run_preflight(child_vk, proof, preflight, sponge)
475            }
476            TraceModuleRef::Stacking(module) => module.run_preflight(proof, preflight, sponge),
477            TraceModuleRef::Whir(module) => module.run_preflight(proof, preflight, sponge),
478            _ => panic!("TraceModuleRef::run_preflight called with invalid module"),
479        }
480    }
481
482    #[allow(clippy::too_many_arguments)]
483    #[tracing::instrument(
484        name = "wrapper.generate_proving_ctxs",
485        level = "trace",
486        skip_all,
487        fields(air_module = %self)
488    )]
489    fn generate_cpu_ctxs<SC: StarkProtocolConfig<F = F>>(
490        self,
491        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
492        proofs: &[Proof<BabyBearPoseidon2Config>],
493        preflights: &[Preflight],
494        cached_trace_record: Option<&CachedTraceRecord>,
495        pow_checker_gen: &Arc<PowerCheckerCpuTraceGenerator<2, POW_CHECKER_HEIGHT>>,
496        exp_bits_len_gen: &ExpBitsLenCpuTraceGenerator,
497        external_data: &VerifierExternalData,
498        required_heights: Option<&[usize]>,
499    ) -> Option<Vec<AirProvingContext<CpuBackend<SC>>>> {
500        match self {
501            TraceModuleRef::Transcript(module) => module.generate_proving_ctxs(
502                child_vk,
503                proofs,
504                preflights,
505                &(
506                    external_data.poseidon2_permute_inputs,
507                    external_data.poseidon2_compress_inputs,
508                ),
509                required_heights,
510            ),
511            TraceModuleRef::ProofShape(module) => module.generate_proving_ctxs(
512                child_vk,
513                proofs,
514                preflights,
515                &(
516                    pow_checker_gen.clone(),
517                    external_data.range_check_inputs.as_slice(),
518                ),
519                required_heights,
520            ),
521            TraceModuleRef::Gkr(module) => module.generate_proving_ctxs(
522                child_vk,
523                proofs,
524                preflights,
525                exp_bits_len_gen,
526                required_heights,
527            ),
528            TraceModuleRef::BatchConstraint(module) => module.generate_proving_ctxs(
529                child_vk,
530                proofs,
531                preflights,
532                &(cached_trace_record, pow_checker_gen.clone()),
533                required_heights,
534            ),
535            TraceModuleRef::Stacking(module) => {
536                module.generate_proving_ctxs(child_vk, proofs, preflights, &(), required_heights)
537            }
538            TraceModuleRef::Whir(module) => module.generate_proving_ctxs(
539                child_vk,
540                proofs,
541                preflights,
542                exp_bits_len_gen,
543                required_heights,
544            ),
545        }
546    }
547}
548
549/// The recursive verifier sub-circuit consists of multiple chips, grouped into **modules**.
550///
551/// This circuit supports child verifying keys with at most 256 AIRs. `ProofShapeAir`
552/// range-checks AIR-index gaps with an 8-bit lookup when enforcing sorted proof-shape rows.
553///
554/// This struct is stateful.
555pub struct VerifierSubCircuit<const MAX_NUM_PROOFS: usize> {
556    bus_inventory: BusInventory,
557    bus_idx_manager: BusIndexManager,
558
559    transcript: TranscriptModule,
560    proof_shape: ProofShapeModule,
561    gkr: GkrModule,
562    batch_constraint: BatchConstraintModule,
563    stacking: StackingModule,
564    whir: WhirModule,
565}
566
567impl<const MAX_NUM_PROOFS: usize> VerifierSubCircuit<MAX_NUM_PROOFS> {
568    pub fn new(child_mvk: Arc<MultiStarkVerifyingKey<BabyBearPoseidon2Config>>) -> Self {
569        Self::new_with_options(child_mvk, VerifierConfig::default())
570    }
571
572    pub fn new_with_options(
573        child_mvk: Arc<MultiStarkVerifyingKey<BabyBearPoseidon2Config>>,
574        config: VerifierConfig,
575    ) -> Self {
576        // The verifier must enforce the child VK's linear `trace_height_constraints`.
577        //
578        // This recursion verifier circuit enforces one summary-row in-circuit bound:
579        //   sum_i(num_interactions[i] * lifted_height[i]) < max_interaction_count
580        // with `lifted_height[i] = max(trace_height[i], 2^l_skip)`.
581        //
582        // At verifier-circuit construction time, each child `trace_height_constraint` must be
583        // implied by this bound. If not, we panic and refuse to construct the circuit.
584        let proof_shape_constraint = LinearConstraint {
585            coefficients: child_mvk
586                .inner
587                .per_air
588                .iter()
589                .map(|avk| avk.num_interactions() as u32)
590                .collect(),
591            threshold: child_mvk.inner.params.logup.max_interaction_count,
592        };
593        for (i, constraint) in child_mvk.inner.trace_height_constraints.iter().enumerate() {
594            assert!(
595                constraint.is_implied_by(&proof_shape_constraint),
596                "child_vk trace_height_constraint[{i}] is not implied by ProofShapeAir's check. \
597                 The recursion circuit cannot enforce this constraint. \
598                 Constraint: coefficients={:?}, threshold={}",
599                constraint.coefficients,
600                constraint.threshold,
601            );
602        }
603
604        let mut bus_idx_manager = BusIndexManager::new();
605        let bus_inventory = BusInventory::new(&mut bus_idx_manager);
606
607        let transcript = TranscriptModule::new(
608            bus_inventory.clone(),
609            child_mvk.inner.params.clone(),
610            config.final_state_bus_enabled,
611        );
612        let child_mvk_frame = child_mvk.as_ref().into();
613        let proof_shape = ProofShapeModule::new(
614            &child_mvk_frame,
615            &mut bus_idx_manager,
616            bus_inventory.clone(),
617            config.continuations_enabled,
618        );
619        let gkr = GkrModule::new(&child_mvk, &mut bus_idx_manager, bus_inventory.clone());
620        let batch_constraint = BatchConstraintModule::new(
621            &child_mvk,
622            &mut bus_idx_manager,
623            bus_inventory.clone(),
624            MAX_NUM_PROOFS,
625            config.has_cached,
626        );
627        let stacking = StackingModule::new(&child_mvk, &mut bus_idx_manager, bus_inventory.clone());
628        let whir = WhirModule::new(&child_mvk, &mut bus_idx_manager, bus_inventory.clone());
629
630        VerifierSubCircuit {
631            bus_inventory,
632            bus_idx_manager,
633            transcript,
634            proof_shape,
635            gkr,
636            batch_constraint,
637            stacking,
638            whir,
639        }
640    }
641
642    #[tracing::instrument(name = "execute_preflight", skip_all)]
643    fn run_preflight_without_merkle<TS>(
644        &self,
645        mut sponge: TS,
646        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
647        proof: &Proof<BabyBearPoseidon2Config>,
648    ) -> Preflight
649    where
650        TS: FiatShamirTranscript<BabyBearPoseidon2Config>
651            + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
652    {
653        let mut preflight = Preflight::default();
654
655        // NOTE: it is not required that we group preflight into modules
656        let preflight_modules = [
657            TraceModuleRef::ProofShape(&self.proof_shape),
658            TraceModuleRef::Gkr(&self.gkr),
659            TraceModuleRef::BatchConstraint(&self.batch_constraint),
660            TraceModuleRef::Stacking(&self.stacking),
661            TraceModuleRef::Whir(&self.whir),
662        ];
663        for module in &preflight_modules {
664            module.run_preflight(child_vk, proof, &mut preflight, &mut sponge);
665        }
666        preflight.transcript = sponge.into_log();
667
668        preflight
669    }
670
671    /// Runs preflight for a single proof.
672    #[tracing::instrument(name = "execute_preflight", skip_all)]
673    pub fn run_preflight<TS>(
674        &self,
675        sponge: TS,
676        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
677        proof: &Proof<BabyBearPoseidon2Config>,
678    ) -> Preflight
679    where
680        TS: FiatShamirTranscript<BabyBearPoseidon2Config>
681            + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
682    {
683        let mut preflight = self.run_preflight_without_merkle(sponge, child_vk, proof);
684        Self::apply_merkle_precomputation_cpu(proof, &mut preflight);
685
686        preflight
687    }
688
689    fn apply_merkle_precomputation_cpu(
690        proof: &Proof<BabyBearPoseidon2Config>,
691        preflight: &mut Preflight,
692    ) {
693        let merkle_precomputation = Self::compute_merkle_precomputation(proof);
694        preflight.poseidon2_perm_inputs = merkle_precomputation.poseidon2_perm_inputs;
695        preflight.poseidon2_compress_inputs = merkle_precomputation.poseidon2_compress_inputs;
696        preflight.initial_row_states = merkle_precomputation.initial_row_states;
697        preflight.codeword_states = merkle_precomputation.codeword_states;
698    }
699
700    #[cfg(feature = "cuda")]
701    #[tracing::instrument(name = "apply_merkle_precomputation", skip_all)]
702    fn apply_merkle_precomputation(
703        proof: &Proof<BabyBearPoseidon2Config>,
704        preflight: &mut Preflight,
705        device_ctx: &GpuDeviceCtx,
706    ) {
707        let merkle_precomputation = Self::compute_merkle_precomputation_cuda(proof, device_ctx);
708        preflight.poseidon2_perm_inputs = merkle_precomputation.poseidon2_perm_inputs;
709        preflight.poseidon2_compress_inputs = merkle_precomputation.poseidon2_compress_inputs;
710        preflight.initial_row_states = merkle_precomputation.initial_row_states;
711        preflight.codeword_states = merkle_precomputation.codeword_states;
712    }
713
714    #[cfg_attr(feature = "cuda", allow(dead_code))]
715    #[tracing::instrument(name = "compute_merkle_precomputation", level = "info", skip_all)]
716    fn compute_merkle_precomputation(
717        proof: &Proof<BabyBearPoseidon2Config>,
718    ) -> MerklePrecomputation {
719        let initial_chunks: usize = proof
720            .whir_proof
721            .initial_round_opened_rows
722            .iter()
723            .flat_map(|c| c.iter().flat_map(|q| q.iter()))
724            .map(|row| row.len().div_ceil(CHUNK))
725            .sum();
726        let codeword_chunks: usize = proof
727            .whir_proof
728            .codeword_opened_values
729            .iter()
730            .map(|r| r.iter().map(|q| q.len()).sum::<usize>())
731            .sum();
732
733        // InitialOpenedValuesAir (initial_row_states) does Poseidon2 *permute* lookups per chunk.
734        // NonInitialOpenedValuesAir (codeword_states) does Poseidon2 *compress* lookups per value.
735        let mut poseidon2_perm_inputs = Vec::with_capacity(initial_chunks);
736        let mut poseidon2_compress_inputs = Vec::with_capacity(codeword_chunks);
737
738        let initial_row_states: Vec<Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>>> = proof
739            .whir_proof
740            .initial_round_opened_rows
741            .iter()
742            .map(|opened_rows_per_commit| {
743                opened_rows_per_commit
744                    .iter()
745                    .map(|opened_rows_per_query| {
746                        opened_rows_per_query
747                            .iter()
748                            .map(|opened_row| {
749                                let (_leaf_hash, pre_states, post_states) =
750                                    poseidon2_hash_slice_with_states(opened_row);
751                                poseidon2_perm_inputs.extend(pre_states);
752                                post_states
753                            })
754                            .collect()
755                    })
756                    .collect()
757            })
758            .collect();
759
760        let codeword_states = proof
761            .whir_proof
762            .codeword_opened_values
763            .iter()
764            .map(|round_values| {
765                round_values
766                    .iter()
767                    .map(|opened_values_per_query| {
768                        opened_values_per_query
769                            .iter()
770                            .map(|opened_value| {
771                                let (_leaf_hash, pre_states, post_states) =
772                                    poseidon2_hash_slice_with_states(
773                                        opened_value.as_basis_coefficients_slice(),
774                                    );
775                                // This is not quite a compression, but the AIR will constrain that
776                                // the padded pre_state gets
777                                // compressed into _leaf_hash via Poseidon2CompressBus.
778                                poseidon2_compress_inputs.extend(pre_states);
779                                debug_assert_eq!(post_states.len(), 1);
780                                post_states.into_iter().next().unwrap()
781                            })
782                            .collect()
783                    })
784                    .collect()
785            })
786            .collect();
787
788        MerklePrecomputation {
789            poseidon2_perm_inputs,
790            poseidon2_compress_inputs,
791            initial_row_states,
792            codeword_states,
793        }
794    }
795
796    #[cfg(feature = "cuda")]
797    #[tracing::instrument(name = "compute_merkle_precomputation_cuda", level = "info", skip_all)]
798    fn compute_merkle_precomputation_cuda(
799        proof: &Proof<BabyBearPoseidon2Config>,
800        device_ctx: &GpuDeviceCtx,
801    ) -> MerklePrecomputation {
802        use openvm_cuda_common::{
803            copy::{MemCopyD2H, MemCopyH2D},
804            d_buffer::DeviceBuffer,
805        };
806
807        use crate::cuda::abi::{merkle_precomputation_hash_vectors, VectorDescriptor};
808
809        let num_chunks = |len: usize| len.div_ceil(CHUNK);
810
811        let mut num_vectors = 0usize;
812        let mut total_data_len = 0usize;
813        let mut total_chunks = 0usize;
814
815        for row in proof
816            .whir_proof
817            .initial_round_opened_rows
818            .iter()
819            .flat_map(|per_commit| per_commit.iter().flat_map(|per_query| per_query.iter()))
820        {
821            num_vectors += 1;
822            total_data_len += row.len();
823            total_chunks += num_chunks(row.len());
824        }
825        let num_perm_chunks = total_chunks;
826
827        for opened_value in proof
828            .whir_proof
829            .codeword_opened_values
830            .iter()
831            .flat_map(|per_round| per_round.iter().flat_map(|per_query| per_query.iter()))
832        {
833            let len = <EF as BasedVectorSpace<F>>::as_basis_coefficients_slice(opened_value).len();
834            num_vectors += 1;
835            total_data_len += len;
836            total_chunks += num_chunks(len);
837        }
838
839        let mut flat_data = Vec::with_capacity(total_data_len);
840        let mut descriptors = Vec::with_capacity(num_vectors);
841        let mut output_offset_chunks = 0usize;
842
843        let mut push_vector = |data: &[F]| {
844            let len = data.len();
845            let chunks = num_chunks(len);
846            descriptors.push(VectorDescriptor {
847                data_offset: flat_data.len(),
848                len,
849                output_offset: output_offset_chunks,
850            });
851            output_offset_chunks += chunks;
852            flat_data.extend_from_slice(data);
853        };
854
855        for row in proof
856            .whir_proof
857            .initial_round_opened_rows
858            .iter()
859            .flat_map(|per_commit| per_commit.iter().flat_map(|per_query| per_query.iter()))
860        {
861            push_vector(row);
862        }
863        for opened_value in proof
864            .whir_proof
865            .codeword_opened_values
866            .iter()
867            .flat_map(|per_round| per_round.iter().flat_map(|per_query| per_query.iter()))
868        {
869            push_vector(opened_value.as_basis_coefficients_slice());
870        }
871
872        debug_assert_eq!(descriptors.len(), num_vectors);
873        debug_assert_eq!(flat_data.len(), total_data_len);
874        debug_assert_eq!(output_offset_chunks, total_chunks);
875
876        // Upload to GPU and run kernel on the caller-owned stream.
877        let d_data = flat_data
878            .to_device_on(device_ctx)
879            .expect("failed to upload data");
880        let d_descriptors = descriptors
881            .to_device_on(device_ctx)
882            .expect("failed to upload descriptors");
883        let d_pre_states =
884            DeviceBuffer::<F>::with_capacity_on(total_chunks * POSEIDON2_WIDTH, device_ctx);
885        let d_post_states =
886            DeviceBuffer::<F>::with_capacity_on(total_chunks * POSEIDON2_WIDTH, device_ctx);
887
888        unsafe {
889            merkle_precomputation_hash_vectors(
890                &d_data,
891                &d_descriptors,
892                num_vectors,
893                &d_pre_states,
894                &d_post_states,
895                device_ctx.stream.as_raw(),
896            )
897            .expect("hash_vectors kernel failed");
898        }
899
900        // Download results
901        let pre_states_flat = d_pre_states
902            .to_host_on(device_ctx)
903            .expect("failed to download pre_states");
904        let post_states_flat = d_post_states
905            .to_host_on(device_ctx)
906            .expect("failed to download post_states");
907        debug_assert_eq!(pre_states_flat.len(), total_chunks * POSEIDON2_WIDTH);
908        debug_assert_eq!(post_states_flat.len(), total_chunks * POSEIDON2_WIDTH);
909
910        // Split pre_states into poseidon permute and compress inputs
911        let (perm_flat, compress_flat) =
912            pre_states_flat.split_at(num_perm_chunks * POSEIDON2_WIDTH);
913        let poseidon2_perm_inputs: Vec<[F; POSEIDON2_WIDTH]> = perm_flat
914            .chunks_exact(POSEIDON2_WIDTH)
915            .map(|chunk| chunk.try_into().unwrap())
916            .collect();
917        let poseidon2_compress_inputs: Vec<[F; POSEIDON2_WIDTH]> = compress_flat
918            .chunks_exact(POSEIDON2_WIDTH)
919            .map(|chunk| chunk.try_into().unwrap())
920            .collect();
921
922        let mut post_iter = post_states_flat.chunks_exact(POSEIDON2_WIDTH);
923
924        let initial_row_states: Vec<Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>>> = proof
925            .whir_proof
926            .initial_round_opened_rows
927            .iter()
928            .map(|per_commit| {
929                per_commit
930                    .iter()
931                    .map(|per_query| {
932                        per_query
933                            .iter()
934                            .map(|row| {
935                                (0..num_chunks(row.len()))
936                                    .map(|_| post_iter.next().unwrap().try_into().unwrap())
937                                    .collect()
938                            })
939                            .collect()
940                    })
941                    .collect()
942            })
943            .collect();
944
945        let codeword_states: Vec<Vec<Vec<[F; POSEIDON2_WIDTH]>>> = proof
946            .whir_proof
947            .codeword_opened_values
948            .iter()
949            .map(|per_round| {
950                per_round
951                    .iter()
952                    .map(|per_query| {
953                        per_query
954                            .iter()
955                            .map(|opened_value| {
956                                debug_assert_eq!(
957                                    num_chunks(
958                                        <EF as BasedVectorSpace<F>>::as_basis_coefficients_slice(
959                                            opened_value
960                                        )
961                                        .len()
962                                    ),
963                                    1
964                                );
965                                post_iter.next().unwrap().try_into().unwrap()
966                            })
967                            .collect()
968                    })
969                    .collect()
970            })
971            .collect();
972
973        debug_assert_eq!(post_iter.len(), 0);
974
975        MerklePrecomputation {
976            poseidon2_perm_inputs,
977            poseidon2_compress_inputs,
978            initial_row_states,
979            codeword_states,
980        }
981    }
982
983    /// Utility function to split a slice of required trace heights per-module. Fails
984    /// an assert if the slice length doesn't match the number of AIRs.
985    #[allow(clippy::type_complexity)]
986    fn split_required_heights<'a>(
987        &self,
988        required_heights: Option<&'a [usize]>,
989    ) -> (Vec<Option<&'a [usize]>>, Option<usize>, Option<usize>) {
990        let bc_n = self.batch_constraint.num_airs();
991        let t_n = self.transcript.num_airs();
992        let ps_n = self.proof_shape.num_airs();
993        let gkr_n = self.gkr.num_airs();
994        let st_n = self.stacking.num_airs();
995        let w_n = self.whir.num_airs();
996        let module_air_counts = [bc_n, t_n, ps_n, gkr_n, st_n, w_n];
997
998        let Some(heights) = required_heights else {
999            return (vec![None; module_air_counts.len()], None, None);
1000        };
1001
1002        let total_module_airs: usize = module_air_counts.iter().sum();
1003        let total = total_module_airs + 2; // PowerChecker + ExpBitsLen
1004        assert_eq!(heights.len(), total);
1005
1006        let mut offset = 0usize;
1007        let mut per_module = Vec::with_capacity(module_air_counts.len());
1008        for n in module_air_counts {
1009            per_module.push(Some(&heights[offset..offset + n]));
1010            offset += n;
1011        }
1012        debug_assert_eq!(heights.len() - offset, 2);
1013
1014        (per_module, Some(heights[offset]), Some(heights[offset + 1]))
1015    }
1016}
1017
1018impl<const MAX_NUM_PROOFS: usize> AggregationSubCircuit for VerifierSubCircuit<MAX_NUM_PROOFS> {
1019    fn airs<SC: StarkProtocolConfig<F = F>>(&self) -> Vec<AirRef<SC>> {
1020        let exp_bits_len_air = ExpBitsLenAir::new(
1021            self.bus_inventory.exp_bits_len_bus,
1022            self.bus_inventory.right_shift_bus,
1023        );
1024        let power_checker_air = PowerCheckerAir::<2, POW_CHECKER_HEIGHT> {
1025            pow_bus: self.bus_inventory.power_checker_bus,
1026            range_bus: self.bus_inventory.range_checker_bus,
1027        };
1028
1029        // WARNING: SymbolicExpressionAir MUST be the first AIR in verifier circuit
1030        iter::empty()
1031            .chain(self.batch_constraint.airs())
1032            .chain(self.transcript.airs())
1033            .chain(self.proof_shape.airs())
1034            .chain(self.gkr.airs())
1035            .chain(self.stacking.airs())
1036            .chain(self.whir.airs())
1037            .chain([
1038                Arc::new(power_checker_air) as AirRef<_>,
1039                Arc::new(exp_bits_len_air) as AirRef<_>,
1040            ])
1041            .collect()
1042    }
1043
1044    fn bus_inventory(&self) -> &BusInventory {
1045        &self.bus_inventory
1046    }
1047
1048    fn next_bus_idx(&self) -> BusIndex {
1049        self.bus_idx_manager.bus_idx_max
1050    }
1051
1052    fn max_num_proofs(&self) -> usize {
1053        MAX_NUM_PROOFS
1054    }
1055}
1056
1057impl<SC: StarkProtocolConfig<F = F>, const MAX_NUM_PROOFS: usize>
1058    VerifierTraceGen<CpuBackend<SC>, SC, ()> for VerifierSubCircuit<MAX_NUM_PROOFS>
1059{
1060    fn new(
1061        child_mvk: Arc<MultiStarkVerifyingKey<BabyBearPoseidon2Config>>,
1062        config: VerifierConfig,
1063    ) -> Self {
1064        Self::new_with_options(child_mvk, config)
1065    }
1066
1067    fn commit_child_vk<E: StarkEngine<SC = SC, PB = CpuBackend<SC>>>(
1068        &self,
1069        engine: &E,
1070        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
1071    ) -> CommittedTraceData<CpuBackend<SC>>
1072    where
1073        (): From<EngineDeviceCtx<E>>,
1074    {
1075        crate::batch_constraint::commit_child_vk(engine, child_vk, self.batch_constraint.has_cached)
1076    }
1077
1078    fn cached_trace_record(
1079        &self,
1080        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
1081    ) -> CachedTraceRecord {
1082        crate::batch_constraint::expr_eval::build_cached_trace_record(
1083            child_vk,
1084            self.batch_constraint.has_cached,
1085        )
1086    }
1087
1088    #[tracing::instrument(name = "subcircuit_generate_proving_ctxs", skip_all)]
1089    fn generate_proving_ctxs<
1090        TS: FiatShamirTranscript<BabyBearPoseidon2Config>
1091            + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
1092    >(
1093        &self,
1094        child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
1095        cached_trace_ctx: CachedTraceCtx<CpuBackend<SC>>,
1096        proofs: &[Proof<BabyBearPoseidon2Config>],
1097        external_data: &mut VerifierExternalData,
1098        _device_ctx: &(),
1099        initial_transcript: TS,
1100    ) -> Option<Vec<AirProvingContext<CpuBackend<SC>>>> {
1101        debug_assert!(proofs.len() <= MAX_NUM_PROOFS);
1102        // Use std::thread::scope for preflight parallelism. With only 3-4 proofs max, this avoids
1103        // Rayon's thread pool overhead (wake-up, work stealing, synchronization) while still
1104        // getting parallelism with minimal overhead.
1105        let span = tracing::Span::current();
1106        let mut preflights = std::thread::scope(|s| {
1107            let handles: Vec<_> = proofs
1108                .iter()
1109                .map(|proof| {
1110                    let sponge = initial_transcript.clone();
1111                    let span = span.clone();
1112                    s.spawn(move || {
1113                        let _guard = span.enter();
1114                        #[cfg(feature = "cuda")]
1115                        {
1116                            self.run_preflight_without_merkle(sponge, child_vk, proof)
1117                        }
1118                        #[cfg(not(feature = "cuda"))]
1119                        {
1120                            self.run_preflight(sponge, child_vk, proof)
1121                        }
1122                    })
1123                })
1124                .collect();
1125            handles
1126                .into_iter()
1127                .map(|h| h.join().unwrap())
1128                .collect::<Vec<_>>()
1129        });
1130        #[cfg(feature = "cuda")]
1131        for (proof, preflight) in proofs.iter().zip(preflights.iter_mut()) {
1132            Self::apply_merkle_precomputation_cpu(proof, preflight);
1133        }
1134
1135        if let Some(final_transcript_state) = &mut external_data.final_transcript_state {
1136            // WARNING: For convenience we use the fact that the last transcript operation should be
1137            // a sample. If this is not the case, we will have to reconstruct final_transcript_state
1138            // from the last perm state and observe values.
1139            debug_assert_eq!(preflights.len(), 1);
1140            debug_assert!(preflights[0].transcript.samples().last().unwrap());
1141            let state = *preflights[0].transcript.perm_results().last().unwrap();
1142            *(*final_transcript_state) = state;
1143            preflights[0].poseidon2_compress_inputs.push(state);
1144        }
1145
1146        let power_checker_gen =
1147            Arc::new(PowerCheckerCpuTraceGenerator::<2, POW_CHECKER_HEIGHT>::default());
1148        for &log in external_data.power_check_inputs {
1149            power_checker_gen.add_pow(log);
1150        }
1151        let exp_bits_len_gen = ExpBitsLenCpuTraceGenerator::default();
1152
1153        let (module_required, power_checker_required, exp_bits_len_required) =
1154            self.split_required_heights(external_data.required_heights);
1155
1156        // WARNING: SymbolicExpressionAir MUST be the first AIR in verifier circuit
1157        let modules = vec![
1158            TraceModuleRef::BatchConstraint(&self.batch_constraint),
1159            TraceModuleRef::Transcript(&self.transcript),
1160            TraceModuleRef::ProofShape(&self.proof_shape),
1161            TraceModuleRef::Gkr(&self.gkr),
1162            TraceModuleRef::Stacking(&self.stacking),
1163            TraceModuleRef::Whir(&self.whir),
1164        ];
1165        let cached_trace_record = match &cached_trace_ctx {
1166            CachedTraceCtx::Records(cached_trace_record) => Some(cached_trace_record),
1167            _ => None,
1168        };
1169
1170        let span = tracing::Span::current();
1171        let ctxs_by_module = modules
1172            .into_par_iter()
1173            .zip(module_required)
1174            .map(|(module, required_heights)| {
1175                let _guard = span.enter();
1176                module.generate_cpu_ctxs(
1177                    child_vk,
1178                    proofs,
1179                    &preflights,
1180                    cached_trace_record,
1181                    &power_checker_gen,
1182                    &exp_bits_len_gen,
1183                    external_data,
1184                    required_heights,
1185                )
1186            })
1187            .collect::<Vec<_>>();
1188
1189        let mut ctxs_by_module: Vec<Vec<AirProvingContext<CpuBackend<SC>>>> =
1190            ctxs_by_module.into_iter().collect::<Option<Vec<_>>>()?;
1191        match cached_trace_ctx {
1192            CachedTraceCtx::PcsData(child_vk_pcs_data) => {
1193                assert!(self.batch_constraint.has_cached);
1194                ctxs_by_module[BATCH_CONSTRAINT_MOD_IDX][LOCAL_SYMBOLIC_EXPRESSION_AIR_IDX]
1195                    .cached_mains = vec![child_vk_pcs_data];
1196            }
1197            CachedTraceCtx::Records(cached_trace_record) => {
1198                assert!(!self.batch_constraint.has_cached);
1199                ctxs_by_module[BATCH_CONSTRAINT_MOD_IDX][LOCAL_SYMBOLIC_EXPRESSION_AIR_IDX]
1200                    .public_values = cached_trace_record.dag_commit_info.unwrap().commit.to_vec();
1201            }
1202        };
1203
1204        let mut ctx_per_trace = ctxs_by_module.into_iter().flatten().collect::<Vec<_>>();
1205        if power_checker_required.is_some_and(|h| h != POW_CHECKER_HEIGHT) {
1206            return None;
1207        }
1208        // Caution: this must be done after GKR and WHIR tracegen
1209        tracing::trace_span!("wrapper.generate_proving_ctxs", air_module = "Primitives",).in_scope(
1210            || {
1211                tracing::trace_span!("wrapper.generate_trace", air = "PowerChecker").in_scope(
1212                    || {
1213                        ctx_per_trace.push(AirProvingContext::simple_no_pis(
1214                            power_checker_gen.generate_trace_row_major(),
1215                        ));
1216                    },
1217                );
1218            },
1219        );
1220        let exp_bits_trace_rm = tracing::trace_span!("wrapper.generate_trace", air = "ExpBitsLen")
1221            .in_scope(|| exp_bits_len_gen.generate_trace_row_major(exp_bits_len_required))?;
1222        ctx_per_trace.push(AirProvingContext::simple_no_pis(exp_bits_trace_rm));
1223        Some(ctx_per_trace)
1224    }
1225}
1226
1227#[cfg(feature = "cuda")]
1228pub mod cuda_tracegen {
1229    use std::iter::zip;
1230
1231    use openvm_cuda_backend::{hash_scheme::GpuHashScheme, GenericGpuBackend, GpuBackend};
1232
1233    use super::*;
1234    use crate::{
1235        cuda::{preflight::PreflightGpu, proof::ProofGpu, vk::VerifyingKeyGpu},
1236        primitives::{
1237            exp_bits_len::ExpBitsLenTraceGenerator as GpuExpBitsLenTraceGenerator,
1238            pow::cuda::PowerCheckerGpuTraceGenerator,
1239        },
1240    };
1241
1242    impl<'a> TraceModuleRef<'a> {
1243        #[allow(clippy::too_many_arguments)]
1244        #[tracing::instrument(
1245            name = "wrapper.generate_proving_ctxs",
1246            level = "trace",
1247            skip_all,
1248            fields(air_module = %self)
1249        )]
1250        fn generate_gpu_proving_ctxs(
1251            self,
1252            child_vk: &VerifyingKeyGpu,
1253            proofs: &[ProofGpu],
1254            preflights: &[PreflightGpu],
1255            device_ctx: &openvm_cuda_common::stream::GpuDeviceCtx,
1256            cached_trace_record: Option<&CachedTraceRecord>,
1257            pow_checker_gen: &Arc<PowerCheckerGpuTraceGenerator<2, POW_CHECKER_HEIGHT>>,
1258            exp_bits_len_gen: &GpuExpBitsLenTraceGenerator,
1259            external_data: &VerifierExternalData,
1260            required_heights: Option<&[usize]>,
1261        ) -> Option<Vec<AirProvingContext<GpuBackend>>> {
1262            match self {
1263                TraceModuleRef::Transcript(module) => module.generate_proving_ctxs(
1264                    child_vk,
1265                    proofs,
1266                    preflights,
1267                    &(
1268                        external_data.poseidon2_permute_inputs,
1269                        external_data.poseidon2_compress_inputs,
1270                        device_ctx,
1271                    ),
1272                    required_heights,
1273                ),
1274                TraceModuleRef::ProofShape(module) => module.generate_proving_ctxs(
1275                    child_vk,
1276                    proofs,
1277                    preflights,
1278                    &(
1279                        pow_checker_gen.clone(),
1280                        external_data.range_check_inputs.as_slice(),
1281                        device_ctx,
1282                    ),
1283                    required_heights,
1284                ),
1285                TraceModuleRef::Gkr(module) => module.generate_proving_ctxs(
1286                    child_vk,
1287                    proofs,
1288                    preflights,
1289                    &(exp_bits_len_gen, device_ctx),
1290                    required_heights,
1291                ),
1292                TraceModuleRef::BatchConstraint(module) => module.generate_proving_ctxs(
1293                    child_vk,
1294                    proofs,
1295                    preflights,
1296                    &(
1297                        cached_trace_record,
1298                        pow_checker_gen.cpu_checker().unwrap(),
1299                        device_ctx,
1300                    ),
1301                    required_heights,
1302                ),
1303                TraceModuleRef::Stacking(module) => module.generate_proving_ctxs(
1304                    child_vk,
1305                    proofs,
1306                    preflights,
1307                    device_ctx,
1308                    required_heights,
1309                ),
1310                TraceModuleRef::Whir(module) => module.generate_proving_ctxs(
1311                    child_vk,
1312                    proofs,
1313                    preflights,
1314                    &(exp_bits_len_gen, device_ctx),
1315                    required_heights,
1316                ),
1317            }
1318        }
1319    }
1320
1321    /// Coerces an `AirProvingContext<GpuBackend>` to `AirProvingContext<GenericGpuBackend<HS>>`.
1322    ///
1323    /// Safe because all GPU backends share `Val = BabyBear` and `Matrix = DeviceMatrix<F>`.
1324    /// Panics in debug builds if `cached_mains` is non-empty (commitments differ by hash scheme).
1325    fn coerce_gpu_proving_ctx<HS: GpuHashScheme>(
1326        ctx: AirProvingContext<GpuBackend>,
1327    ) -> AirProvingContext<GenericGpuBackend<HS>> {
1328        debug_assert!(ctx.cached_mains.is_empty());
1329        AirProvingContext {
1330            cached_mains: vec![],
1331            common_main: ctx.common_main,
1332            public_values: ctx.public_values,
1333        }
1334    }
1335
1336    impl<HS: GpuHashScheme, const MAX_NUM_PROOFS: usize>
1337        VerifierTraceGen<GenericGpuBackend<HS>, HS::SC, openvm_cuda_common::stream::GpuDeviceCtx>
1338        for VerifierSubCircuit<MAX_NUM_PROOFS>
1339    {
1340        fn new(
1341            child_mvk: Arc<MultiStarkVerifyingKey<BabyBearPoseidon2Config>>,
1342            config: VerifierConfig,
1343        ) -> Self {
1344            Self::new_with_options(child_mvk, config)
1345        }
1346
1347        fn commit_child_vk<E: StarkEngine<SC = HS::SC, PB = GenericGpuBackend<HS>>>(
1348            &self,
1349            engine: &E,
1350            child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
1351        ) -> CommittedTraceData<GenericGpuBackend<HS>>
1352        where
1353            GpuDeviceCtx: From<EngineDeviceCtx<E>>,
1354        {
1355            crate::batch_constraint::commit_child_vk(
1356                engine,
1357                child_vk,
1358                self.batch_constraint.has_cached,
1359            )
1360        }
1361
1362        fn cached_trace_record(
1363            &self,
1364            child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
1365        ) -> CachedTraceRecord {
1366            crate::batch_constraint::expr_eval::build_cached_trace_record(
1367                child_vk,
1368                self.batch_constraint.has_cached,
1369            )
1370        }
1371
1372        #[tracing::instrument(name = "subcircuit_generate_proving_ctxs", skip_all)]
1373        fn generate_proving_ctxs<
1374            TS: FiatShamirTranscript<BabyBearPoseidon2Config>
1375                + TranscriptHistory<F = F, State = [F; POSEIDON2_WIDTH]>,
1376        >(
1377            &self,
1378            child_vk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
1379            cached_trace_ctx: CachedTraceCtx<GenericGpuBackend<HS>>,
1380            proofs: &[Proof<BabyBearPoseidon2Config>],
1381            external_data: &mut VerifierExternalData,
1382            device_ctx: &openvm_cuda_common::stream::GpuDeviceCtx,
1383            initial_transcript: TS,
1384        ) -> Option<Vec<AirProvingContext<GenericGpuBackend<HS>>>> {
1385            debug_assert!(proofs.len() <= MAX_NUM_PROOFS);
1386            let child_vk_gpu = VerifyingKeyGpu::new(child_vk, device_ctx);
1387            let proofs_gpu = proofs
1388                .iter()
1389                .map(|proof_cpu| ProofGpu::new(child_vk, proof_cpu, device_ctx))
1390                .collect::<Vec<_>>();
1391            // Use std::thread::scope for preflight parallelism. With only 3-4 proofs max, this
1392            // avoids Rayon's thread pool overhead while still getting parallelism.
1393            let span = tracing::Span::current();
1394            let mut preflights_cpu = std::thread::scope(|s| {
1395                let handles: Vec<_> = proofs
1396                    .iter()
1397                    .map(|proof| {
1398                        let sponge = initial_transcript.clone();
1399                        let span = span.clone();
1400                        s.spawn(move || {
1401                            let _guard = span.enter();
1402                            self.run_preflight_without_merkle(sponge, child_vk, proof)
1403                        })
1404                    })
1405                    .collect();
1406                handles
1407                    .into_iter()
1408                    .map(|h| h.join().unwrap())
1409                    .collect::<Vec<_>>()
1410            });
1411
1412            // Merkle precomputation launches CUDA kernels, so run serially on main thread.
1413            for (proof, preflight) in proofs.iter().zip(preflights_cpu.iter_mut()) {
1414                Self::apply_merkle_precomputation(proof, preflight, device_ctx);
1415            }
1416
1417            if let Some(final_transcript_state) = &mut external_data.final_transcript_state {
1418                // WARNING: For convenience we use the fact that the last transcript operation
1419                // should be a sample. If this is not the case, we will have to reconstruct
1420                // final_transcript_state from the last perm state and observe values.
1421                debug_assert_eq!(preflights_cpu.len(), 1);
1422                debug_assert!(preflights_cpu[0].transcript.samples().last().unwrap());
1423                let state = *preflights_cpu[0].transcript.perm_results().last().unwrap();
1424                *(*final_transcript_state) = state;
1425                preflights_cpu[0].poseidon2_compress_inputs.push(state);
1426            }
1427
1428            let power_checker_gen = Arc::new(
1429                PowerCheckerGpuTraceGenerator::<2, POW_CHECKER_HEIGHT>::hybrid(device_ctx.clone()),
1430            );
1431            if let Some(cpu_checker) = power_checker_gen.cpu_checker() {
1432                for &log in external_data.power_check_inputs {
1433                    cpu_checker.add_pow(log);
1434                }
1435            }
1436            let exp_bits_len_gen = GpuExpBitsLenTraceGenerator::new(device_ctx.clone());
1437
1438            let (module_required, power_checker_required, exp_bits_len_required) =
1439                self.split_required_heights(external_data.required_heights);
1440
1441            // NOTE: avoid par_iter for now so H2D transfer all happens on same stream to avoid sync
1442            // issues
1443            let preflights_gpu = zip(proofs, preflights_cpu)
1444                .map(|(proof, preflight_cpu)| {
1445                    PreflightGpu::new(child_vk, proof, &preflight_cpu, device_ctx)
1446                })
1447                .collect::<Vec<_>>();
1448            let modules = vec![
1449                TraceModuleRef::BatchConstraint(&self.batch_constraint),
1450                TraceModuleRef::Transcript(&self.transcript),
1451                TraceModuleRef::ProofShape(&self.proof_shape),
1452                TraceModuleRef::Gkr(&self.gkr),
1453                TraceModuleRef::Stacking(&self.stacking),
1454                TraceModuleRef::Whir(&self.whir),
1455            ];
1456            let cached_trace_record = match &cached_trace_ctx {
1457                CachedTraceCtx::Records(cached_trace_record) => Some(cached_trace_record),
1458                _ => None,
1459            };
1460
1461            // PERF[jpw]: we avoid par_iter so that kernel launches occur on the same stream.
1462            // This can be parallelized to separate streams for more CUDA stream parallelism, but it
1463            // will require recording events so streams properly sync for cudaMemcpyAsync and kernel
1464            // launches
1465            let mut ctxs_by_module_gpu = Vec::with_capacity(modules.len());
1466            for (module, required_heights) in modules.into_iter().zip(module_required) {
1467                ctxs_by_module_gpu.push(module.generate_gpu_proving_ctxs(
1468                    &child_vk_gpu,
1469                    &proofs_gpu,
1470                    &preflights_gpu,
1471                    device_ctx,
1472                    cached_trace_record,
1473                    &power_checker_gen,
1474                    &exp_bits_len_gen,
1475                    external_data,
1476                    required_heights,
1477                )?);
1478            }
1479            device_ctx.stream.synchronize().unwrap();
1480            let mut ctxs_by_module: Vec<Vec<AirProvingContext<GenericGpuBackend<HS>>>> =
1481                ctxs_by_module_gpu
1482                    .into_iter()
1483                    .map(|module_ctxs| {
1484                        module_ctxs
1485                            .into_iter()
1486                            .map(coerce_gpu_proving_ctx::<HS>)
1487                            .collect()
1488                    })
1489                    .collect();
1490            match cached_trace_ctx {
1491                CachedTraceCtx::PcsData(child_vk_pcs_data) => {
1492                    assert!(self.batch_constraint.has_cached);
1493                    ctxs_by_module[BATCH_CONSTRAINT_MOD_IDX][LOCAL_SYMBOLIC_EXPRESSION_AIR_IDX]
1494                        .cached_mains = vec![child_vk_pcs_data];
1495                }
1496                CachedTraceCtx::Records(cached_trace_record) => {
1497                    assert!(!self.batch_constraint.has_cached);
1498                    ctxs_by_module[BATCH_CONSTRAINT_MOD_IDX][LOCAL_SYMBOLIC_EXPRESSION_AIR_IDX]
1499                        .public_values =
1500                        cached_trace_record.dag_commit_info.unwrap().commit.to_vec();
1501                }
1502            }
1503
1504            let mut ctx_per_trace = ctxs_by_module.into_iter().flatten().collect::<Vec<_>>();
1505            if power_checker_required.is_some_and(|h| h != POW_CHECKER_HEIGHT) {
1506                return None;
1507            }
1508            // Caution: this must be done after GKR and WHIR tracegen
1509            tracing::trace_span!("wrapper.generate_proving_ctxs", air_module = "Primitives",)
1510                .in_scope(|| {
1511                    tracing::trace_span!("wrapper.generate_trace", air = "PowerChecker").in_scope(
1512                        || {
1513                            let pow_bits_trace = power_checker_gen.generate_trace();
1514                            ctx_per_trace.push(AirProvingContext::simple_no_pis(pow_bits_trace));
1515                        },
1516                    );
1517                });
1518            let exp_bits_trace = tracing::trace_span!("wrapper.generate_trace", air = "ExpBitsLen")
1519                .in_scope(|| exp_bits_len_gen.generate_trace_device(exp_bits_len_required))?;
1520            ctx_per_trace.push(AirProvingContext::simple_no_pis(exp_bits_trace));
1521            Some(ctx_per_trace)
1522        }
1523    }
1524}