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