Skip to main content

miden_air/
lib.rs

1#![no_std]
2
3#[macro_use]
4extern crate alloc;
5
6#[cfg(feature = "std")]
7extern crate std;
8
9use alloc::vec::Vec;
10use core::borrow::Borrow;
11
12use miden_core::{
13    WORD_SIZE, Word,
14    field::ExtensionField,
15    precompile::PrecompileTranscriptState,
16    program::{Kernel, MIN_STACK_DEPTH, ProgramInfo, StackInputs, StackOutputs},
17};
18use miden_crypto::stark::{
19    air::{ReductionError, WindowAccess},
20    challenger::CanObserve,
21};
22#[cfg(feature = "arbitrary")]
23use proptest::prelude::*;
24
25pub mod ace;
26pub mod config;
27mod constraints;
28pub mod lookup;
29pub mod trace;
30
31/// Miden VM-specific LogUp lookup argument: bus identifiers and bus message types.
32///
33/// [`crate::MidenAir`] is the single `LiftedAir`/`LookupAir` for the multi-AIR
34/// instance; it dispatches per-trace work to [`crate::CoreAir`] / [`crate::ChipletsAir`].
35/// [`crate::MidenMultiAir`] is the `MultiAir` carrying the cross-AIR reduction.
36/// The generic LogUp framework this builds on lives in [`crate::lookup`] and is free of
37/// Miden-specific types so it can be extracted into its own crate.
38pub mod logup {
39    pub use crate::constraints::lookup::{
40        BusId, MIDEN_MAX_MESSAGE_WIDTH, messages::*, miden_air::NUM_LOGUP_COMMITTED_FINALS,
41    };
42}
43
44use constraints::lookup::{
45    chiplet_air::ChipletLookupBuilder,
46    main_air::{MainLookupAir, MainLookupBuilder},
47};
48pub use constraints::{
49    chiplets::columns::{
50        AceCols, AceEvalCols, AceReadCols, BitwiseCols, ControllerCols, KernelRomCols, MemoryCols,
51        PermutationCols,
52    },
53    columns::{ChipletCols, CoreCols},
54    decoder::columns::DecoderCols,
55    ext_field::QuadFeltExpr,
56    range::columns::RangeCols,
57    stack::columns::StackCols,
58    system::columns::SystemCols,
59};
60use logup::{BusId, MIDEN_MAX_MESSAGE_WIDTH};
61use lookup::{
62    BoundaryBuilder, Challenges, ConstraintLookupBuilder, LookupAir, LookupMessage,
63    build_logup_aux_trace,
64};
65use miden_core::utils::RowMajorMatrix;
66
67// RE-EXPORTS
68// ================================================================================================
69mod export {
70    pub use miden_core::{
71        Felt,
72        serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
73        utils::ToElements,
74    };
75    pub use miden_crypto::stark::{
76        StarkConfig,
77        air::{
78            AirBuilder, BaseAir, ConstraintDegrees, ExtensionBuilder, LiftedAir, LiftedAirBuilder,
79            MultiAir, PermutationAirBuilder, ProverStatement, Statement,
80        },
81        debug,
82    };
83}
84
85pub use export::*;
86
87// MIDEN AIR BUILDER
88// ================================================================================================
89
90/// Convenience super-trait that pins `LiftedAirBuilder` to our field.
91///
92/// All constraint functions in this crate should be generic over `AB: MidenAirBuilder`
93/// instead of spelling out the full `LiftedAirBuilder<F = Felt>` bound.
94pub trait MidenAirBuilder: LiftedAirBuilder<F = Felt> {}
95impl<T: LiftedAirBuilder<F = Felt>> MidenAirBuilder for T {}
96
97// PUBLIC INPUTS
98// ================================================================================================
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101#[cfg_attr(
102    all(feature = "arbitrary", test),
103    miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false))
104)]
105pub struct PublicInputs {
106    program_info: ProgramInfo,
107    stack_inputs: StackInputs,
108    stack_outputs: StackOutputs,
109    pc_transcript_state: PrecompileTranscriptState,
110}
111
112impl PublicInputs {
113    /// Creates a new instance of `PublicInputs` from program information, stack inputs and outputs,
114    /// and the precompile transcript state (rolling digest of all recorded commitments).
115    pub fn new(
116        program_info: ProgramInfo,
117        stack_inputs: StackInputs,
118        stack_outputs: StackOutputs,
119        pc_transcript_state: PrecompileTranscriptState,
120    ) -> Self {
121        Self {
122            program_info,
123            stack_inputs,
124            stack_outputs,
125            pc_transcript_state,
126        }
127    }
128
129    pub fn stack_inputs(&self) -> StackInputs {
130        self.stack_inputs
131    }
132
133    pub fn stack_outputs(&self) -> StackOutputs {
134        self.stack_outputs
135    }
136
137    pub fn program_info(&self) -> ProgramInfo {
138        self.program_info.clone()
139    }
140
141    /// Returns the precompile transcript state.
142    pub fn pc_transcript_state(&self) -> PrecompileTranscriptState {
143        self.pc_transcript_state
144    }
145
146    /// Returns the canonical commitment to the kernel of the verified statement: the value the
147    /// recursive verifier observes into the transcript in place of the raw kernel-procedure
148    /// digest list. See [`Kernel::commitment`](miden_core::program::Kernel::commitment).
149    pub fn kernel_commitment(&self) -> Word {
150        self.program_info.kernel_commitment()
151    }
152
153    /// Returns the AIR public values (`air_inputs`) and the statement `aux_inputs` as flat
154    /// slices of `Felt`s.
155    ///
156    /// `air_inputs` (the values read by the AIR constraints) layout:
157    ///   [0..16]  stack inputs
158    ///   [16..32] stack outputs
159    ///
160    /// `aux_inputs` (statement inputs not read by the AIRs, consumed only by `observe` and
161    /// `eval_external`) layout:
162    ///   [0..4]   program hash
163    ///   [4..8]   precompile transcript state
164    ///   [8..]    kernel procedure digests (concatenated words, variable length)
165    pub fn to_air_inputs(&self) -> (Vec<Felt>, Vec<Felt>) {
166        let mut air_inputs = Vec::with_capacity(NUM_PUBLIC_VALUES);
167        air_inputs.extend_from_slice(self.stack_inputs.as_ref());
168        air_inputs.extend_from_slice(self.stack_outputs.as_ref());
169
170        let kernel_felts = Word::words_as_elements(self.program_info.kernel_procedures());
171        let mut aux_inputs = Vec::with_capacity(AUX_KERNEL_DIGESTS + kernel_felts.len());
172        aux_inputs.extend_from_slice(self.program_info.program_hash().as_elements());
173        aux_inputs.extend_from_slice(self.pc_transcript_state.as_ref());
174        aux_inputs.extend_from_slice(kernel_felts);
175
176        (air_inputs, aux_inputs)
177    }
178
179    /// Converts public inputs into a vector of field elements (Felt) in the canonical order:
180    /// - program info elements (including kernel procedure hashes)
181    /// - stack inputs
182    /// - stack outputs
183    /// - precompile transcript state
184    pub fn to_elements(&self) -> Vec<Felt> {
185        let mut result = self.program_info.to_elements();
186        result.extend_from_slice(self.stack_inputs.as_ref());
187        result.extend_from_slice(self.stack_outputs.as_ref());
188        result.extend_from_slice(self.pc_transcript_state.as_ref());
189        result
190    }
191}
192
193#[cfg(feature = "arbitrary")]
194impl Arbitrary for PublicInputs {
195    type Parameters = ();
196    type Strategy = BoxedStrategy<Self>;
197
198    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
199        fn felt_strategy() -> impl Strategy<Value = Felt> {
200            any::<u32>().prop_map(Felt::from)
201        }
202
203        fn word_strategy() -> impl Strategy<Value = Word> {
204            any::<[u32; WORD_SIZE]>().prop_map(|values| Word::new(values.map(Felt::from)))
205        }
206
207        let program_info = word_strategy()
208            .prop_map(|program_hash| ProgramInfo::new(program_hash, Kernel::default()));
209        let stack_inputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
210            .prop_map(|values| StackInputs::new(&values).expect("generated stack inputs fit"));
211        let stack_outputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
212            .prop_map(|values| StackOutputs::new(&values).expect("generated stack outputs fit"));
213
214        (program_info, stack_inputs, stack_outputs, word_strategy())
215            .prop_map(|(program_info, stack_inputs, stack_outputs, pc_transcript_state)| {
216                Self::new(program_info, stack_inputs, stack_outputs, pc_transcript_state)
217            })
218            .boxed()
219    }
220}
221
222// SERIALIZATION
223// ================================================================================================
224
225impl Serializable for PublicInputs {
226    fn write_into<W: ByteWriter>(&self, target: &mut W) {
227        self.program_info.write_into(target);
228        self.stack_inputs.write_into(target);
229        self.stack_outputs.write_into(target);
230        self.pc_transcript_state.write_into(target);
231    }
232}
233
234impl Deserializable for PublicInputs {
235    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
236        let program_info = ProgramInfo::read_from(source)?;
237        let stack_inputs = StackInputs::read_from(source)?;
238        let stack_outputs = StackOutputs::read_from(source)?;
239        let pc_transcript_state = PrecompileTranscriptState::read_from(source)?;
240
241        Ok(PublicInputs {
242            program_info,
243            stack_inputs,
244            stack_outputs,
245            pc_transcript_state,
246        })
247    }
248}
249
250// PROCESSOR AIR
251// ================================================================================================
252
253/// Number of public values read by the Miden VM AIRs — the `air_inputs` shared by every AIR.
254///
255/// Layout (32 Felts total):
256///   [0..16]  stack inputs
257///   [16..32] stack outputs
258///
259/// The program hash and precompile transcript state are not read by any AIR constraint, so they
260/// are carried as `aux_inputs` and consumed only by [`MidenMultiAir::observe`] and
261/// [`MidenMultiAir::eval_external`].
262pub const NUM_PUBLIC_VALUES: usize = MIN_STACK_DEPTH + MIN_STACK_DEPTH;
263
264/// LogUp aux trace width: 4 main-trace columns + 3 chiplet-trace columns.
265pub const LOGUP_AUX_TRACE_WIDTH: usize = 7;
266
267// `aux_inputs` layout offsets — statement inputs that the AIRs do not read. The fixed program
268// hash and transcript state occupy the first two words; the variable-length kernel-procedure
269// digests follow.
270const AUX_PROGRAM_HASH: usize = 0;
271const AUX_TRANSCRIPT_STATE: usize = WORD_SIZE;
272const AUX_KERNEL_DIGESTS: usize = 2 * WORD_SIZE;
273
274// CORE AIR
275// ================================================================================================
276
277/// Core-trace AIR.
278///
279/// Owns the system, decoder, stack, and range-check segments. Paired with [`ChipletsAir`]
280/// for the two-AIR proving path.
281#[derive(Copy, Clone, Debug, Default)]
282pub struct CoreAir;
283
284impl CoreAir {
285    fn width(self) -> usize {
286        constraints::columns::NUM_CORE_COLS
287    }
288
289    fn periodic_columns(self) -> Vec<Vec<Felt>> {
290        // Core has no periodic columns; all periodic columns serve the chiplets.
291        Vec::new()
292    }
293
294    fn aux_width(self) -> usize {
295        constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
296    }
297
298    /// LogUp boundary correction for the core trace: the running sum of every
299    /// boundary interaction reduced to its denominator contribution. The single var-len slice
300    /// carries `[program_hash (4) | transcript_state (4)]`, the statement inputs the core
301    /// boundary cancels against the block-hash and log-precompile buses.
302    fn boundary_correction<EF: ExtensionField<Felt>>(
303        self,
304        challenges: &Challenges<EF>,
305        public_values: &[Felt],
306        var_len_public_inputs: &[&[Felt]],
307    ) -> Result<EF, ReductionError> {
308        if var_len_public_inputs.len() != 1 {
309            return Err(format!(
310                "CoreAir expects 1 var-len public input slice, got {}",
311                var_len_public_inputs.len()
312            )
313            .into());
314        }
315        if var_len_public_inputs[0].len() != 2 * WORD_SIZE {
316            return Err(format!(
317                "CoreAir expects {} boundary felts (program hash + transcript state), got {}",
318                2 * WORD_SIZE,
319                var_len_public_inputs[0].len()
320            )
321            .into());
322        }
323
324        let mut reducer = ReduceBoundaryBuilder {
325            challenges,
326            public_values,
327            var_len_public_inputs,
328            sum: EF::ZERO,
329            error: None,
330        };
331        constraints::lookup::miden_air::emit_core_boundary(&mut reducer);
332        reducer.finalize()
333    }
334
335    fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
336        let main = builder.main();
337        let local: &CoreCols<AB::Var> = (*main.current_slice()).borrow();
338        let next: &CoreCols<AB::Var> = (*main.next_slice()).borrow();
339
340        let op_flags =
341            constraints::op_flags::OpFlags::new(&local.decoder, &local.stack, &next.decoder);
342
343        constraints::enforce_core(builder, local, next, &op_flags);
344        constraints::public_inputs::enforce_main(builder, local);
345
346        let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::CORE);
347        self.lookup_eval(&mut lb);
348    }
349
350    fn lookup_num_columns(self) -> usize {
351        constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
352    }
353
354    fn lookup_column_shape(self) -> &'static [usize] {
355        &constraints::lookup::main_air::MAIN_COLUMN_SHAPE
356    }
357
358    fn lookup_max_message_width(self) -> usize {
359        MIDEN_MAX_MESSAGE_WIDTH
360    }
361
362    fn lookup_num_bus_ids(self) -> usize {
363        BusId::COUNT
364    }
365
366    fn lookup_eval<LB: MainLookupBuilder>(self, builder: &mut LB) {
367        MainLookupAir.eval(builder);
368    }
369
370    fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
371        constraints::lookup::miden_air::emit_core_boundary(boundary);
372    }
373}
374
375// CHIPLETS AIR
376// ================================================================================================
377
378/// Chiplets-trace AIR for the multi-AIR proving path.
379///
380/// Owns the chiplet section and its LogUp accumulator columns. Counterpart to [`CoreAir`].
381#[derive(Copy, Clone, Debug, Default)]
382pub struct ChipletsAir;
383
384/// Per-trace AIR logic. Like [`CoreAir`], `ChipletsAir` is not an AIR trait impl itself —
385/// [`MidenAir`] dispatches to these inherent (struct) methods for per-trace concerns.
386impl ChipletsAir {
387    fn width(self) -> usize {
388        constraints::columns::NUM_CHIPLETS_COLS
389    }
390
391    fn periodic_columns(self) -> Vec<Vec<Felt>> {
392        // All periodic columns (hasher round constants, bitwise operation table) belong to
393        // the chiplets trace.
394        constraints::chiplets::columns::PeriodicCols::periodic_columns()
395    }
396
397    fn aux_width(self) -> usize {
398        constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
399    }
400
401    /// LogUp boundary correction for the chiplets trace. The kernel digests are
402    /// the single var-len public input group; they boundary-cancel against the
403    /// kernel-rom bus, which lives on `CHIPLET_COLUMN_SHAPE[0]`. Consumed by
404    /// [`MidenMultiAir::eval_external`].
405    fn boundary_correction<EF: ExtensionField<Felt>>(
406        self,
407        challenges: &Challenges<EF>,
408        public_values: &[Felt],
409        var_len_public_inputs: &[&[Felt]],
410    ) -> Result<EF, ReductionError> {
411        if var_len_public_inputs.len() != 1 {
412            return Err(format!(
413                "ChipletsAir expects 1 var-len public input slice, got {}",
414                var_len_public_inputs.len()
415            )
416            .into());
417        }
418        if !var_len_public_inputs[0].len().is_multiple_of(WORD_SIZE) {
419            return Err(format!(
420                "kernel digest felts length {} is not a multiple of {}",
421                var_len_public_inputs[0].len(),
422                WORD_SIZE
423            )
424            .into());
425        }
426
427        let mut reducer = ReduceBoundaryBuilder {
428            challenges,
429            public_values,
430            var_len_public_inputs,
431            sum: EF::ZERO,
432            error: None,
433        };
434        constraints::lookup::miden_air::emit_chiplets_boundary(&mut reducer);
435        reducer.finalize()
436    }
437
438    fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
439        let main = builder.main();
440        let local: &ChipletCols<AB::Var> = (*main.current_slice()).borrow();
441        let next: &ChipletCols<AB::Var> = (*main.next_slice()).borrow();
442
443        let selectors =
444            constraints::chiplets::selectors::build_chiplet_selectors(builder, local, next);
445
446        constraints::enforce_chiplets(builder, local, next, &selectors);
447
448        let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::CHIPLETS);
449        self.lookup_eval(&mut lb);
450    }
451
452    fn lookup_num_columns(self) -> usize {
453        constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
454    }
455
456    fn lookup_column_shape(self) -> &'static [usize] {
457        &constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE
458    }
459
460    fn lookup_max_message_width(self) -> usize {
461        MIDEN_MAX_MESSAGE_WIDTH
462    }
463
464    fn lookup_num_bus_ids(self) -> usize {
465        BusId::COUNT
466    }
467
468    fn lookup_eval<LB: ChipletLookupBuilder>(self, builder: &mut LB) {
469        let main = builder.main();
470        let local: &ChipletCols<_> = main.current_slice().borrow();
471        let next: &ChipletCols<_> = main.next_slice().borrow();
472
473        constraints::lookup::chiplet_air::emit_chiplet_lookup_columns(builder, local, next);
474    }
475
476    fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
477        constraints::lookup::miden_air::emit_chiplets_boundary(boundary);
478    }
479}
480
481// MIDEN AIR (multi-AIR enum wrapper)
482// ================================================================================================
483
484/// Homogeneous wrapper that lets [`CoreAir`] and [`ChipletsAir`] share a single AIR type.
485/// [`MultiAir::Air`](miden_crypto::stark::air::MultiAir) is a single associated type, so every
486/// instance in the multi-AIR proof must be the same type; this enum dispatches per-trace work
487/// to the inner [`CoreAir`] / [`ChipletsAir`].
488#[derive(Copy, Clone, Debug)]
489pub enum MidenAir {
490    Core(CoreAir),
491    Chiplets(ChipletsAir),
492}
493
494impl MidenAir {
495    pub const CORE: Self = Self::Core(CoreAir);
496    pub const CHIPLETS: Self = Self::Chiplets(ChipletsAir);
497}
498
499impl BaseAir<Felt> for MidenAir {
500    fn width(&self) -> usize {
501        match self {
502            Self::Core(a) => a.width(),
503            Self::Chiplets(a) => a.width(),
504        }
505    }
506
507    fn num_public_values(&self) -> usize {
508        NUM_PUBLIC_VALUES
509    }
510
511    fn periodic_columns(&self) -> Vec<Vec<Felt>> {
512        match self {
513            Self::Core(a) => a.periodic_columns(),
514            Self::Chiplets(a) => a.periodic_columns(),
515        }
516    }
517}
518
519impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for MidenAir {
520    fn num_randomness(&self) -> usize {
521        // Instance-level: every AIR shares the same LogUp challenge set.
522        trace::AUX_TRACE_RAND_CHALLENGES
523    }
524
525    fn aux_width(&self) -> usize {
526        match self {
527            Self::Core(a) => a.aux_width(),
528            Self::Chiplets(a) => a.aux_width(),
529        }
530    }
531
532    fn num_aux_values(&self) -> usize {
533        // One real committed LogUp final per AIR instance.
534        1
535    }
536
537    fn build_aux_trace(
538        &self,
539        main: &RowMajorMatrix<Felt>,
540        _air_inputs: &[Felt],
541        _aux_inputs: &[Felt],
542        challenges: &[EF],
543    ) -> (RowMajorMatrix<EF>, Vec<EF>) {
544        let (aux_trace, committed) = build_logup_aux_trace(self, main, challenges);
545        debug_assert_eq!(
546            committed.len(),
547            1,
548            "build_logup_aux_trace returns one committed final per AIR (col 0's terminal sum)"
549        );
550        (aux_trace, committed)
551    }
552
553    fn constraint_degree(&self) -> ConstraintDegrees {
554        // All AIRs peak at degree 9 over base-field and extension-field constraints.
555        ConstraintDegrees { base: 9, ext: 9 }
556    }
557
558    fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
559        match self {
560            Self::Core(a) => a.eval(builder),
561            Self::Chiplets(a) => a.eval(builder),
562        }
563    }
564}
565
566impl<LB> LookupAir<LB> for MidenAir
567where
568    LB: MainLookupBuilder + ChipletLookupBuilder,
569{
570    fn num_columns(&self) -> usize {
571        match self {
572            Self::Core(a) => a.lookup_num_columns(),
573            Self::Chiplets(a) => a.lookup_num_columns(),
574        }
575    }
576
577    fn column_shape(&self) -> &[usize] {
578        match self {
579            Self::Core(a) => a.lookup_column_shape(),
580            Self::Chiplets(a) => a.lookup_column_shape(),
581        }
582    }
583
584    fn max_message_width(&self) -> usize {
585        match self {
586            Self::Core(a) => a.lookup_max_message_width(),
587            Self::Chiplets(a) => a.lookup_max_message_width(),
588        }
589    }
590
591    fn num_bus_ids(&self) -> usize {
592        match self {
593            Self::Core(a) => a.lookup_num_bus_ids(),
594            Self::Chiplets(a) => a.lookup_num_bus_ids(),
595        }
596    }
597
598    fn eval(&self, builder: &mut LB) {
599        match self {
600            Self::Core(a) => a.lookup_eval(builder),
601            Self::Chiplets(a) => a.lookup_eval(builder),
602        }
603    }
604
605    fn eval_boundary<B>(&self, boundary: &mut B)
606    where
607        B: BoundaryBuilder<F = LB::F, EF = LB::EF>,
608    {
609        match self {
610            Self::Core(a) => a.lookup_eval_boundary(boundary),
611            Self::Chiplets(a) => a.lookup_eval_boundary(boundary),
612        }
613    }
614}
615
616// MIDEN MULTI-AIR
617// ================================================================================================
618
619/// The cross-AIR statement for the `(Core, Chiplets)` proof: owns the AIR
620/// collection in instance order and carries the LogUp reduction over the
621/// committed aux finals.
622///
623/// Instance order is `[Core, Chiplets]`; every per-AIR slice follows that
624/// ordering.
625#[derive(Copy, Clone, Debug)]
626pub struct MidenMultiAir {
627    airs: [MidenAir; 2],
628}
629
630impl MidenMultiAir {
631    /// Instance-order AIR collection: `[Core, Chiplets]`.
632    pub const fn new() -> Self {
633        Self {
634            airs: [MidenAir::CORE, MidenAir::CHIPLETS],
635        }
636    }
637}
638
639impl Default for MidenMultiAir {
640    fn default() -> Self {
641        Self::new()
642    }
643}
644
645impl<EF: ExtensionField<Felt>> MultiAir<Felt, EF> for MidenMultiAir {
646    type Air = MidenAir;
647
648    fn airs(&self) -> &[MidenAir] {
649        &self.airs
650    }
651
652    fn num_air_inputs(&self) -> usize {
653        NUM_PUBLIC_VALUES
654    }
655
656    fn max_aux_inputs(&self) -> usize {
657        // aux_inputs = program hash (1 word) + transcript state (1 word) + the var-len
658        // kernel-digest group: one `Word` per kernel procedure, capped at
659        // `Kernel::MAX_NUM_PROCEDURES`.
660        AUX_KERNEL_DIGESTS + Kernel::MAX_NUM_PROCEDURES * WORD_SIZE
661    }
662
663    /// Absorb statement-owned public inputs into the Fiat-Shamir challenger.
664    ///
665    /// Replaces the default length-prefixed stream with a rate-aligned schedule (six 8-felt
666    /// blocks, 48 felts total) that the recursive verifier mirrors:
667    ///
668    /// ```text
669    /// [ kernel_H (4) | program_hash (4) ]
670    /// [ transcript_state (4) | 0,0,0,0 ]      trailing pad keeps the schedule rate-aligned
671    /// [ stack_inputs (16) ]                   two blocks
672    /// [ stack_outputs (16) ]                  two blocks
673    /// ```
674    ///
675    /// The kernel digests enter the transcript only through `kernel_H`
676    /// (see [`hash_kernel_digests`]), committing to the kernel with a fixed-size value instead
677    /// of the unbounded digest list.
678    fn observe<C: CanObserve<Felt>>(
679        &self,
680        challenger: &mut C,
681        air_inputs: &[Felt],
682        aux_inputs: &[Felt],
683        _log_trace_heights: &[u8],
684    ) {
685        assert_eq!(air_inputs.len(), NUM_PUBLIC_VALUES, "unexpected public-value count");
686        assert!(
687            aux_inputs.len() >= AUX_KERNEL_DIGESTS,
688            "aux inputs shorter than the fixed program-hash + transcript-state prefix"
689        );
690
691        let kernel_h = hash_kernel_digests(&aux_inputs[AUX_KERNEL_DIGESTS..]);
692        let program_hash = &aux_inputs[AUX_PROGRAM_HASH..AUX_PROGRAM_HASH + WORD_SIZE];
693        let transcript_state = &aux_inputs[AUX_TRANSCRIPT_STATE..AUX_TRANSCRIPT_STATE + WORD_SIZE];
694        let stack_io = air_inputs;
695
696        // Block 1: kernel_H | program_hash. Block 2: transcript_state | zero pad.
697        for &v in kernel_h.iter().chain(program_hash) {
698            challenger.observe(v);
699        }
700        for &v in transcript_state {
701            challenger.observe(v);
702        }
703        for _ in 0..WORD_SIZE {
704            challenger.observe(Felt::ZERO);
705        }
706        for &v in stack_io {
707            challenger.observe(v);
708        }
709    }
710
711    /// Cross-AIR LogUp closure: the sum of every committed aux final plus the
712    /// per-trace boundary corrections must vanish. `aux_inputs` carries the program hash and
713    /// transcript state (consumed by the core boundary) followed by the kernel digests
714    /// (consumed by the chiplets boundary).
715    fn eval_external(
716        &self,
717        challenges: &[EF],
718        air_inputs: &[Felt],
719        aux_inputs: &[Felt],
720        aux_values: &[&[EF]],
721        _log_trace_heights: &[u8],
722    ) -> Result<Vec<EF>, ReductionError> {
723        if aux_inputs.len() < AUX_KERNEL_DIGESTS {
724            return Err(format!(
725                "aux_inputs length {} is shorter than the fixed prefix {AUX_KERNEL_DIGESTS}",
726                aux_inputs.len()
727            )
728            .into());
729        }
730        let challenges = Challenges::<EF>::new(
731            challenges[0],
732            challenges[1],
733            MIDEN_MAX_MESSAGE_WIDTH,
734            BusId::COUNT,
735        );
736
737        let core_correction = CoreAir.boundary_correction(
738            &challenges,
739            air_inputs,
740            &[&aux_inputs[..AUX_KERNEL_DIGESTS]],
741        )?;
742        let chiplets_correction = ChipletsAir.boundary_correction(
743            &challenges,
744            air_inputs,
745            &[&aux_inputs[AUX_KERNEL_DIGESTS..]],
746        )?;
747
748        let aux_sum: EF = aux_values.iter().flat_map(|vals| vals.iter().copied()).sum();
749        Ok(vec![aux_sum + core_correction + chiplets_correction])
750    }
751}
752
753// KERNEL DIGEST SUMMARY HASH
754// ================================================================================================
755
756/// Computes `kernel_H`, the fixed-size commitment to the kernel-procedure digests.
757///
758/// This is the canonical [`Kernel::commitment`] value expressed over the flattened digest
759/// felts: the linear hash (`hash_elements`) of `kernel_felts`. The empty digest list yields
760/// `hash_elements(&[])`.
761///
762/// `kernel_H` is absorbed into the Fiat-Shamir transcript in place of the unbounded kernel
763/// digest list, committing to the kernel with a fixed-size value.
764pub fn hash_kernel_digests(kernel_felts: &[Felt]) -> [Felt; WORD_SIZE] {
765    assert!(
766        kernel_felts.len().is_multiple_of(WORD_SIZE),
767        "kernel digest felts must be whole words"
768    );
769
770    miden_core::chiplets::hasher::hash_elements(kernel_felts).into()
771}
772
773// REDUCED-AUX BOUNDARY BUILDER
774// ================================================================================================
775
776/// `BoundaryBuilder` impl that reduces each emitted interaction to its LogUp
777/// denominator contribution `multiplicity · encode(msg)⁻¹` and sums them into a
778/// running `EF` accumulator.
779///
780/// Lets the boundary correction reuse the structured boundary emissions from
781/// [`emit_miden_boundary`] — the same source consumed by the debug walker —
782/// instead of open-coding the three corrections a second time.
783///
784/// Denominators are `α + Σ βⁱ · field_i` with random `α, β`; on any legitimate proof they
785/// are non-zero with overwhelming probability. A malformed/adversarial proof can still
786/// drive a denominator to zero, so the reducer captures the first failure and surfaces it
787/// as a [`ReductionError`] to the verifier rather than panicking.
788struct ReduceBoundaryBuilder<'a, EF: ExtensionField<Felt>> {
789    challenges: &'a Challenges<EF>,
790    public_values: &'a [Felt],
791    var_len_public_inputs: &'a [&'a [Felt]],
792    sum: EF,
793    error: Option<ReductionError>,
794}
795
796impl<'a, EF: ExtensionField<Felt>> ReduceBoundaryBuilder<'a, EF> {
797    fn finalize(self) -> Result<EF, ReductionError> {
798        match self.error {
799            Some(err) => Err(err),
800            None => Ok(self.sum),
801        }
802    }
803}
804
805impl<'a, EF: ExtensionField<Felt>> BoundaryBuilder for ReduceBoundaryBuilder<'a, EF> {
806    type F = Felt;
807    type EF = EF;
808
809    fn public_values(&self) -> &[Felt] {
810        self.public_values
811    }
812
813    fn var_len_public_inputs(&self) -> &[&[Felt]] {
814        self.var_len_public_inputs
815    }
816
817    fn insert<M>(&mut self, _name: &'static str, multiplicity: Felt, msg: M)
818    where
819        M: LookupMessage<Felt, EF>,
820    {
821        if self.error.is_some() {
822            return;
823        }
824        match msg.encode(self.challenges).try_inverse() {
825            Some(inv) => self.sum += inv * multiplicity,
826            None => {
827                self.error = Some("LogUp boundary denominator was zero".into());
828            },
829        }
830    }
831}
832
833// TESTS
834// ================================================================================================
835
836#[cfg(test)]
837mod tests {
838    use miden_core::field::QuadFelt;
839
840    use super::*;
841
842    /// Guards the static `constraint_degree` override: if an AIR change moves the symbolic
843    /// degree away from the declared value, the override must be updated.
844    #[test]
845    fn constraint_degree_override_matches_symbolic() {
846        for air in [MidenAir::CORE, MidenAir::CHIPLETS] {
847            let symbolic = ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&air);
848            let declared = <MidenAir as LiftedAir<Felt, QuadFelt>>::constraint_degree(&air);
849            assert_eq!(declared, symbolic, "static constraint_degree override is stale");
850        }
851    }
852}