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::{borrow::Cow, vec::Vec};
10use core::borrow::Borrow;
11
12use miden_core::{
13    WORD_SIZE, Word,
14    deferred::DeferredRoot,
15    field::ExtensionField,
16    program::{
17        KernelDescriptor, MIN_STACK_DEPTH, NUM_CLAIM_ELEMENTS, ProgramInfo, StackInputs,
18        StackOutputs,
19    },
20};
21use miden_crypto::stark::{
22    air::{ReductionError, WindowAccess},
23    challenger::CanObserve,
24};
25#[cfg(feature = "arbitrary")]
26use proptest::prelude::*;
27
28pub mod ace;
29pub mod config;
30mod constraints;
31pub mod lookup;
32pub mod memory;
33mod proof_order;
34pub mod security;
35pub mod trace;
36
37/// Miden VM-specific LogUp lookup argument: bus identifiers and bus message types.
38///
39/// [`crate::MidenAir`] is the single `LiftedAir`/`LookupAir` type for the multi-AIR
40/// statement; it dispatches per-trace work to Core, Chiplets, Eidos compression, and
41/// byte-pair lookup AIRs.
42/// [`crate::MidenMultiAir`] is the `MultiAir` carrying the cross-AIR reduction.
43/// The generic LogUp framework lives in [`crate::lookup`].
44pub mod logup {
45    pub use crate::constraints::lookup::{
46        BusId, MIDEN_MAX_MESSAGE_WIDTH, messages::*, miden_air::NUM_LOGUP_COMMITTED_FINALS,
47    };
48}
49
50pub use constraints::{
51    and8_lookup,
52    and8_lookup::columns::{And8LookupCols, And8LookupPreprocessedCols},
53    chiplets::columns::{
54        AceCols, AceEvalCols, AceReadCols, AeadStreamCols, AeadStreamHighFirstCols,
55        AeadStreamHighSecondCols, AeadStreamLowSecondCols, AeadStreamReadCols, BitwiseCols,
56        ControllerCols, KernelRomCols, MemoryCols,
57    },
58    columns::{ChipletCols, CoreCols},
59    decoder::columns::DecoderCols,
60    eidos_compression,
61    eidos_compression::{EidosCompressionCols, NUM_COLS as NUM_EIDOS_COMPRESSION_COLS},
62    ext_field::QuadFeltExpr,
63    stack::columns::StackCols,
64    system::columns::SystemCols,
65};
66use constraints::{
67    eidos_compression::{
68        constraints::{
69            enforce_footer_rows, enforce_fused_rows, enforce_inactive_footer_input_aux,
70            enforce_inactive_footer_output_aux,
71        },
72        lookup::{
73            EIDOS_COMPRESSION_LOOKUP_COLUMN_SHAPE, EidosCompressionLookupBuilder,
74            emit_lookup_columns,
75        },
76        periodic::get_periodic_column_values,
77        selectors::EidosCompressionSelectors,
78    },
79    lookup::{
80        and8_lookup_air::And8LookupBuilder,
81        chiplet_air::ChipletLookupBuilder,
82        main_air::{MainLookupAir, MainLookupBuilder},
83    },
84};
85use logup::{BusId, MIDEN_MAX_MESSAGE_WIDTH};
86use lookup::{
87    BoundaryBuilder, Challenges, ConstraintLookupBuilder, LookupAir, LookupMessage,
88    build_logup_aux_trace_with_preprocessed,
89};
90use miden_core::utils::RowMajorMatrix;
91
92// RE-EXPORTS
93// ================================================================================================
94mod export {
95    pub use miden_core::{
96        Felt,
97        serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
98        utils::ToElements,
99    };
100    pub use miden_crypto::stark::{
101        StarkConfig,
102        air::{
103            AirBuilder, BaseAir, ConstraintCounts, ConstraintDegrees, ExtensionBuilder, LiftedAir,
104            LiftedAirBuilder, MultiAir, PermutationAirBuilder, ProverStatement, Statement,
105        },
106        debug,
107        pcs::PcsParams,
108    };
109}
110
111pub use export::*;
112pub use proof_order::{AIRS, MIDEN_AIR_COUNT, PROOF_ORDER_COUNT, ProofOrder};
113
114// MIDEN AIR BUILDER
115// ================================================================================================
116
117/// Convenience super-trait that pins `LiftedAirBuilder` to the Miden base field.
118///
119/// Constraint functions use `AB: MidenAirBuilder` instead of the longer
120/// `LiftedAirBuilder<F = Felt>` bound.
121pub trait MidenAirBuilder: LiftedAirBuilder<F = Felt> {}
122impl<T: LiftedAirBuilder<F = Felt>> MidenAirBuilder for T {}
123
124// PUBLIC INPUTS
125// ================================================================================================
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128#[cfg_attr(
129    all(feature = "arbitrary", test),
130    miden_test_serialization_macros::serialization_test
131)]
132pub struct PublicInputs {
133    program_info: ProgramInfo,
134    stack_inputs: StackInputs,
135    stack_outputs: StackOutputs,
136    deferred_root: DeferredRoot,
137}
138
139impl PublicInputs {
140    /// Creates a new instance of `PublicInputs` from program information, stack inputs and outputs,
141    /// and the final deferred root.
142    pub fn new(
143        program_info: ProgramInfo,
144        stack_inputs: StackInputs,
145        stack_outputs: StackOutputs,
146        deferred_root: DeferredRoot,
147    ) -> Self {
148        Self {
149            program_info,
150            stack_inputs,
151            stack_outputs,
152            deferred_root,
153        }
154    }
155
156    pub fn stack_inputs(&self) -> StackInputs {
157        self.stack_inputs
158    }
159
160    pub fn stack_outputs(&self) -> StackOutputs {
161        self.stack_outputs
162    }
163
164    pub fn program_info(&self) -> ProgramInfo {
165        self.program_info.clone()
166    }
167
168    /// Returns the final deferred root.
169    pub fn deferred_root(&self) -> DeferredRoot {
170        self.deferred_root
171    }
172
173    /// Returns the canonical commitment to the kernel of the verified statement: the value the
174    /// recursive verifier observes into the transcript in place of the raw kernel-procedure
175    /// digest list. See
176    /// [`KernelDescriptor::commitment`](miden_core::program::KernelDescriptor::commitment).
177    pub fn kernel_commitment(&self) -> Word {
178        self.program_info.kernel_commitment()
179    }
180
181    /// Returns the AIR public values (`air_inputs`) and statement inputs (`aux_inputs`).
182    ///
183    /// `air_inputs` (read by Core; all other AIRs have empty slices) layout:
184    ///   [0..16]  stack inputs
185    ///   [16..32] stack outputs
186    ///
187    /// `aux_inputs` (statement inputs not read by the AIRs, consumed only by `observe` and
188    /// `eval_external`) layout:
189    ///   [0..4]   program hash
190    ///   [4..8]   final deferred root
191    ///   [8..]    kernel procedure digests (concatenated words, variable length)
192    pub fn to_air_inputs(&self) -> (Vec<Felt>, Vec<Felt>) {
193        let mut air_inputs = Vec::with_capacity(NUM_PUBLIC_VALUES);
194        air_inputs.extend_from_slice(self.stack_inputs.as_ref());
195        air_inputs.extend_from_slice(self.stack_outputs.as_ref());
196
197        let kernel_felts = Word::words_as_elements(self.program_info.kernel_procedures());
198        let mut aux_inputs = Vec::with_capacity(AUX_KERNEL_DIGESTS + kernel_felts.len());
199        aux_inputs.extend_from_slice(self.program_info.program_hash().as_elements());
200        aux_inputs.extend_from_slice(self.deferred_root.as_ref());
201        aux_inputs.extend_from_slice(kernel_felts);
202
203        (air_inputs, aux_inputs)
204    }
205
206    /// Converts public inputs into a vector of field elements (Felt) in the canonical order:
207    /// - program info elements (including kernel procedure hashes)
208    /// - stack inputs
209    /// - stack outputs
210    /// - final deferred root
211    pub fn to_elements(&self) -> Vec<Felt> {
212        let mut result = self.program_info.to_elements();
213        result.extend_from_slice(self.stack_inputs.as_ref());
214        result.extend_from_slice(self.stack_outputs.as_ref());
215        result.extend_from_slice(self.deferred_root.as_ref());
216        result
217    }
218}
219
220#[cfg(feature = "arbitrary")]
221impl Arbitrary for PublicInputs {
222    type Parameters = ();
223    type Strategy = BoxedStrategy<Self>;
224
225    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
226        fn felt_strategy() -> impl Strategy<Value = Felt> {
227            any::<u32>().prop_map(Felt::from)
228        }
229
230        fn word_strategy() -> impl Strategy<Value = Word> {
231            any::<[u32; WORD_SIZE]>().prop_map(|values| Word::new(values.map(Felt::from)))
232        }
233
234        let program_info = word_strategy()
235            .prop_map(|program_hash| ProgramInfo::new(program_hash, KernelDescriptor::default()));
236        let stack_inputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
237            .prop_map(|values| StackInputs::new(&values).expect("generated stack inputs fit"));
238        let stack_outputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
239            .prop_map(|values| StackOutputs::new(&values).expect("generated stack outputs fit"));
240
241        (program_info, stack_inputs, stack_outputs, word_strategy())
242            .prop_map(|(program_info, stack_inputs, stack_outputs, deferred_root)| {
243                Self::new(program_info, stack_inputs, stack_outputs, deferred_root)
244            })
245            .boxed()
246    }
247}
248
249// SERIALIZATION
250// ================================================================================================
251
252impl Serializable for PublicInputs {
253    fn write_into<W: ByteWriter>(&self, target: &mut W) {
254        self.program_info.write_into(target);
255        self.stack_inputs.write_into(target);
256        self.stack_outputs.write_into(target);
257        self.deferred_root.write_into(target);
258    }
259}
260
261impl Deserializable for PublicInputs {
262    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
263        let program_info = ProgramInfo::read_from(source)?;
264        let stack_inputs = StackInputs::read_from(source)?;
265        let stack_outputs = StackOutputs::read_from(source)?;
266        let deferred_root = DeferredRoot::read_from(source)?;
267
268        Ok(PublicInputs {
269            program_info,
270            stack_inputs,
271            stack_outputs,
272            deferred_root,
273        })
274    }
275}
276
277// PROCESSOR AIR
278// ================================================================================================
279
280/// Number of public values read by Core; the other Miden VM AIRs have no public values.
281///
282/// Layout (32 Felts total):
283///   [0..16]  stack inputs
284///   [16..32] stack outputs
285///
286/// The program hash and final deferred root are not read by any AIR constraint, so they
287/// are carried as `aux_inputs` and consumed only by [`MidenMultiAir::observe`] and
288/// [`MidenMultiAir::eval_external`].
289pub const NUM_PUBLIC_VALUES: usize = MIN_STACK_DEPTH + MIN_STACK_DEPTH;
290
291/// Combined LogUp width used by constraint test harnesses: 4 core columns, 4 chiplet
292/// columns, and the byte-pair table column. Standalone Eidos compression tests size their aux
293/// matrix from the AIR directly.
294pub const LOGUP_AUX_TRACE_WIDTH: usize = 9;
295
296// `aux_inputs` layout offsets — statement inputs that the AIRs do not read. The fixed program
297// hash and final deferred root occupy the first two words; the variable-length kernel-procedure
298// digests follow.
299const AUX_PROGRAM_HASH: usize = 0;
300const AUX_DEFERRED_ROOT: usize = WORD_SIZE;
301const AUX_KERNEL_DIGESTS: usize = 2 * WORD_SIZE;
302
303// CORE AIR
304// ================================================================================================
305
306/// Core trace AIR.
307///
308/// Enforces the system, decoder, stack, and range-check constraints.
309#[derive(Copy, Clone, Debug, Default)]
310pub struct CoreAir;
311
312impl CoreAir {
313    fn width(self) -> usize {
314        constraints::columns::NUM_CORE_COLS
315    }
316
317    fn periodic_columns(self) -> Vec<Vec<Felt>> {
318        Vec::new()
319    }
320
321    fn aux_width(self) -> usize {
322        constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
323    }
324
325    /// LogUp boundary correction for the core trace: the running sum of every
326    /// boundary interaction reduced to its denominator contribution. The single var-len slice
327    /// carries `[program_hash (4) | deferred_root (4)]`, the statement inputs the core
328    /// boundary cancels against the block-hash and log-deferred buses.
329    fn boundary_correction<EF: ExtensionField<Felt>>(
330        self,
331        challenges: &Challenges<EF>,
332        boundary_inputs: &[&[Felt]],
333    ) -> Result<EF, ReductionError> {
334        if boundary_inputs.len() != 1 {
335            return Err(format!(
336                "CoreAir expects 1 boundary input slice, got {}",
337                boundary_inputs.len()
338            )
339            .into());
340        }
341        if boundary_inputs[0].len() != 2 * WORD_SIZE {
342            return Err(format!(
343                "CoreAir expects {} boundary felts (program hash + deferred root), got {}",
344                2 * WORD_SIZE,
345                boundary_inputs[0].len()
346            )
347            .into());
348        }
349
350        let mut reducer = ReduceBoundaryBuilder {
351            challenges,
352            var_len_public_inputs: boundary_inputs,
353            sum: EF::ZERO,
354            error: None,
355        };
356        constraints::lookup::miden_air::emit_core_boundary(&mut reducer);
357        reducer.finalize()
358    }
359
360    fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
361        let main = builder.main();
362        let local: &CoreCols<AB::Var> = (*main.current_slice()).borrow();
363        let next: &CoreCols<AB::Var> = (*main.next_slice()).borrow();
364
365        let op_flags =
366            constraints::op_flags::OpFlags::new(&local.decoder, &local.stack, &next.decoder);
367
368        constraints::enforce_core(builder, local, next, &op_flags);
369        constraints::public_inputs::enforce_main(builder, local);
370
371        let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::Core);
372        self.lookup_eval(&mut lb);
373        lb.finish();
374    }
375
376    fn lookup_column_shape(self) -> &'static [usize] {
377        &constraints::lookup::main_air::MAIN_COLUMN_SHAPE
378    }
379
380    fn lookup_max_message_width(self) -> usize {
381        MIDEN_MAX_MESSAGE_WIDTH
382    }
383
384    fn lookup_num_bus_ids(self) -> usize {
385        BusId::COUNT
386    }
387
388    fn lookup_eval<LB: MainLookupBuilder>(self, builder: &mut LB) {
389        MainLookupAir.eval(builder);
390    }
391
392    fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
393        constraints::lookup::miden_air::emit_core_boundary(boundary);
394    }
395}
396
397// CHIPLETS AIR
398// ================================================================================================
399
400/// Chiplets trace AIR.
401///
402/// Enforces the chiplet selector hierarchy, chiplet transition constraints, and
403/// chiplet-side LogUp buses.
404#[derive(Copy, Clone, Debug, Default)]
405pub struct ChipletsAir;
406
407impl ChipletsAir {
408    fn width(self) -> usize {
409        constraints::columns::NUM_CHIPLETS_COLS
410    }
411
412    fn periodic_columns(self) -> Vec<Vec<Felt>> {
413        constraints::chiplets::columns::PeriodicCols::periodic_columns()
414    }
415
416    fn aux_width(self) -> usize {
417        constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
418    }
419
420    /// LogUp boundary correction for the chiplets trace.
421    ///
422    /// The boundary input slice contains the kernel-procedure digests; these statement values
423    /// close the kernel-ROM bus.
424    fn boundary_correction<EF: ExtensionField<Felt>>(
425        self,
426        challenges: &Challenges<EF>,
427        boundary_inputs: &[&[Felt]],
428    ) -> Result<EF, ReductionError> {
429        if boundary_inputs.len() != 1 {
430            return Err(format!(
431                "ChipletsAir expects 1 boundary input slice, got {}",
432                boundary_inputs.len()
433            )
434            .into());
435        }
436        if !boundary_inputs[0].len().is_multiple_of(WORD_SIZE) {
437            return Err(format!(
438                "kernel digest felts length {} is not a multiple of {}",
439                boundary_inputs[0].len(),
440                WORD_SIZE
441            )
442            .into());
443        }
444
445        let mut reducer = ReduceBoundaryBuilder {
446            challenges,
447            var_len_public_inputs: boundary_inputs,
448            sum: EF::ZERO,
449            error: None,
450        };
451        constraints::lookup::miden_air::emit_chiplets_boundary(&mut reducer);
452        reducer.finalize()
453    }
454
455    fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
456        let main = builder.main();
457        let local: &ChipletCols<AB::Var> = (*main.current_slice()).borrow();
458        let next: &ChipletCols<AB::Var> = (*main.next_slice()).borrow();
459
460        let selectors =
461            constraints::chiplets::selectors::build_chiplet_selectors(builder, local, next);
462
463        constraints::enforce_chiplets(builder, local, next, &selectors);
464
465        let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::Chiplets);
466        self.lookup_eval(&mut lb);
467        lb.finish();
468    }
469
470    fn lookup_column_shape(self) -> &'static [usize] {
471        &constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE
472    }
473
474    fn lookup_max_message_width(self) -> usize {
475        MIDEN_MAX_MESSAGE_WIDTH
476    }
477
478    fn lookup_num_bus_ids(self) -> usize {
479        BusId::COUNT
480    }
481
482    fn lookup_eval<LB: ChipletLookupBuilder>(self, builder: &mut LB) {
483        let main = builder.main();
484        let local: &ChipletCols<_> = main.current_slice().borrow();
485        let next: &ChipletCols<_> = main.next_slice().borrow();
486
487        constraints::lookup::chiplet_air::emit_chiplet_lookup_columns(builder, local, next);
488    }
489
490    fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
491        constraints::lookup::miden_air::emit_chiplets_boundary(boundary);
492    }
493}
494
495// EIDOS COMPRESSION AIR
496// ================================================================================================
497
498/// Standalone Eidos compression AIR.
499#[derive(Copy, Clone, Debug, Default)]
500pub struct EidosCompressionAir;
501
502impl EidosCompressionAir {
503    fn width(self) -> usize {
504        NUM_EIDOS_COMPRESSION_COLS
505    }
506
507    fn periodic_columns(self) -> Vec<Vec<Felt>> {
508        get_periodic_column_values()
509    }
510
511    fn aux_width(self) -> usize {
512        EIDOS_COMPRESSION_LOOKUP_COLUMN_SHAPE.len()
513    }
514
515    fn boundary_correction<EF: ExtensionField<Felt>>(
516        self,
517        _challenges: &Challenges<EF>,
518        boundary_inputs: &[&[Felt]],
519    ) -> Result<EF, ReductionError> {
520        if !boundary_inputs.is_empty() {
521            return Err(format!(
522                "EidosCompressionAir expects 0 boundary input slices, got {}",
523                boundary_inputs.len()
524            )
525            .into());
526        }
527        Ok(EF::ZERO)
528    }
529
530    fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
531        {
532            let main = builder.main();
533            let local = main.current_slice();
534            let next = main.next_slice();
535            let periodic_values: Vec<AB::Expr> =
536                builder.periodic_values().iter().map(|value| (*value).into()).collect();
537            let selectors = EidosCompressionSelectors::new(&periodic_values, 0);
538
539            enforce_fused_rows(builder, local, next, &selectors);
540            enforce_footer_rows(builder, local, next, &selectors);
541            enforce_inactive_footer_input_aux(builder, &selectors);
542            enforce_inactive_footer_output_aux(builder, local, &selectors);
543        }
544        let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::EidosCompression);
545        self.lookup_eval(&mut lb);
546        lb.finish();
547    }
548
549    fn lookup_column_shape(self) -> &'static [usize] {
550        &EIDOS_COMPRESSION_LOOKUP_COLUMN_SHAPE
551    }
552
553    fn lookup_max_message_width(self) -> usize {
554        MIDEN_MAX_MESSAGE_WIDTH
555    }
556
557    fn lookup_num_bus_ids(self) -> usize {
558        BusId::COUNT
559    }
560
561    fn lookup_eval<LB: EidosCompressionLookupBuilder>(self, builder: &mut LB) {
562        let main = builder.main();
563        let local: &EidosCompressionCols<_> = main.current_slice().borrow();
564        let next: &EidosCompressionCols<_> = main.next_slice().borrow();
565        let periodic_values: Vec<LB::Expr> =
566            builder.periodic_values().iter().map(|value| (*value).into()).collect();
567        let selectors = EidosCompressionSelectors::new(&periodic_values, 0);
568
569        emit_lookup_columns(builder, local, next, &selectors);
570    }
571
572    fn lookup_eval_boundary<B: BoundaryBuilder>(self, _boundary: &mut B) {}
573}
574
575// BYTE-PAIR LOOKUP AIR
576// ================================================================================================
577
578/// Standalone byte-AND and range lookup-table AIR.
579#[derive(Copy, Clone, Debug, Default)]
580pub struct And8LookupAir;
581
582impl And8LookupAir {
583    fn width(self) -> usize {
584        and8_lookup::columns::NUM_AND8_LOOKUP_COLS
585    }
586
587    fn preprocessed_trace(self) -> RowMajorMatrix<Felt> {
588        and8_lookup::preprocessed_trace()
589    }
590
591    fn preprocessed_width(self) -> usize {
592        and8_lookup::columns::NUM_AND8_LOOKUP_PREPROCESSED_COLS
593    }
594
595    fn aux_width(self) -> usize {
596        constraints::lookup::and8_lookup_air::AND8_LOOKUP_COLUMN_SHAPE.len()
597    }
598
599    fn boundary_correction<EF: ExtensionField<Felt>>(
600        self,
601        _challenges: &Challenges<EF>,
602        boundary_inputs: &[&[Felt]],
603    ) -> Result<EF, ReductionError> {
604        if !boundary_inputs.is_empty() {
605            return Err(format!(
606                "And8LookupAir expects 0 boundary input slices, got {}",
607                boundary_inputs.len()
608            )
609            .into());
610        }
611        Ok(EF::ZERO)
612    }
613
614    fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
615        let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::And8Lookup);
616        self.lookup_eval(&mut lb);
617        lb.finish();
618    }
619
620    fn lookup_column_shape(self) -> &'static [usize] {
621        &constraints::lookup::and8_lookup_air::AND8_LOOKUP_COLUMN_SHAPE
622    }
623
624    fn lookup_max_message_width(self) -> usize {
625        MIDEN_MAX_MESSAGE_WIDTH
626    }
627
628    fn lookup_num_bus_ids(self) -> usize {
629        BusId::COUNT
630    }
631
632    fn lookup_eval<LB: And8LookupBuilder>(self, builder: &mut LB) {
633        let main = builder.main();
634        let local: &And8LookupCols<_> = main.current_slice().borrow();
635        constraints::lookup::and8_lookup_air::emit_and8_lookup_columns(builder, local);
636    }
637
638    fn lookup_eval_boundary<B: BoundaryBuilder>(self, _boundary: &mut B) {}
639}
640
641// MIDEN AIR
642// ================================================================================================
643
644/// AIR instance identifier for the Miden multi-AIR statement.
645///
646/// [`MultiAir::Air`](miden_crypto::stark::air::MultiAir) is a single associated type, so every
647/// instance in the multi-AIR proof must have the same type. This enum identifies which concrete
648/// AIR logic to dispatch to.
649#[derive(Copy, Clone, Debug, Eq, PartialEq)]
650pub enum MidenAir {
651    Core,
652    Chiplets,
653    EidosCompression,
654    And8Lookup,
655}
656
657impl MidenAir {
658    pub const CORE: Self = Self::Core;
659    pub const CHIPLETS: Self = Self::Chiplets;
660    pub const EIDOS_COMPRESSION: Self = Self::EidosCompression;
661    pub const AND8_LOOKUP: Self = Self::And8Lookup;
662
663    pub const fn instance_index(self) -> usize {
664        match self {
665            Self::Core => 0,
666            Self::Chiplets => 1,
667            Self::EidosCompression => 2,
668            Self::And8Lookup => 3,
669        }
670    }
671
672    pub const fn name(self) -> &'static str {
673        match self {
674            Self::Core => "Core",
675            Self::Chiplets => "Chiplets",
676            Self::EidosCompression => "EidosCompression",
677            Self::And8Lookup => "And8Lookup",
678        }
679    }
680
681    /// Lookup fractions emitted per row, one entry per auxiliary lookup column.
682    ///
683    /// Available without naming a lookup builder, so callers that only need the shape — such as
684    /// the security estimator — do not have to pick an unrelated builder type to read it.
685    pub fn column_shape(self) -> &'static [usize] {
686        match self {
687            Self::Core => CoreAir.lookup_column_shape(),
688            Self::Chiplets => ChipletsAir.lookup_column_shape(),
689            Self::EidosCompression => EidosCompressionAir.lookup_column_shape(),
690            Self::And8Lookup => And8LookupAir.lookup_column_shape(),
691        }
692    }
693
694    pub const fn file_token(self) -> &'static str {
695        match self {
696            Self::Core => "core",
697            Self::Chiplets => "chiplets",
698            Self::EidosCompression => "eidos_compression",
699            Self::And8Lookup => "and8_lookup",
700        }
701    }
702
703    fn boundary_correction<EF: ExtensionField<Felt>>(
704        self,
705        challenges: &Challenges<EF>,
706        aux_inputs: &[Felt],
707    ) -> Result<EF, ReductionError> {
708        if aux_inputs.len() < AUX_KERNEL_DIGESTS {
709            return Err(format!(
710                "aux_inputs length {} is shorter than the fixed prefix {AUX_KERNEL_DIGESTS}",
711                aux_inputs.len()
712            )
713            .into());
714        }
715
716        match self {
717            Self::Core => {
718                CoreAir.boundary_correction(challenges, &[&aux_inputs[..AUX_KERNEL_DIGESTS]])
719            },
720            Self::Chiplets => {
721                ChipletsAir.boundary_correction(challenges, &[&aux_inputs[AUX_KERNEL_DIGESTS..]])
722            },
723            Self::EidosCompression => EidosCompressionAir.boundary_correction(challenges, &[]),
724            Self::And8Lookup => And8LookupAir.boundary_correction(challenges, &[]),
725        }
726    }
727
728    /// Evaluate the hand-written constraint definitions.
729    ///
730    /// [`LiftedAir::eval`] runs the generated evaluators instead; use this entry
731    /// point (via [`HandwrittenMidenAir`] where an AIR value is expected) when
732    /// the hand-written source itself is required — regenerating the evaluators,
733    /// or checking them for drift.
734    pub fn eval_handwritten<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
735        match self {
736            Self::Core => CoreAir.eval(builder),
737            Self::Chiplets => ChipletsAir.eval(builder),
738            Self::EidosCompression => EidosCompressionAir.eval(builder),
739            Self::And8Lookup => And8LookupAir.eval(builder),
740        }
741    }
742}
743
744/// [`MidenAir`] with `eval` routed to the hand-written constraint definitions.
745///
746/// Symbolic capture that feeds artifact generation, or anchors an equivalence
747/// oracle, must consume the hand-written source — a generated evaluator as input
748/// makes regeneration self-referential and severs the chain back to the auditable
749/// source. Passing this wrapper enforces that by construction.
750#[derive(Copy, Clone, Debug)]
751pub struct HandwrittenMidenAir(pub MidenAir);
752
753impl BaseAir<Felt> for HandwrittenMidenAir {
754    fn width(&self) -> usize {
755        self.0.width()
756    }
757
758    fn preprocessed_trace(&self) -> Option<RowMajorMatrix<Felt>> {
759        self.0.preprocessed_trace()
760    }
761
762    fn preprocessed_width(&self) -> usize {
763        self.0.preprocessed_width()
764    }
765
766    fn num_public_values(&self) -> usize {
767        BaseAir::<Felt>::num_public_values(&self.0)
768    }
769
770    fn periodic_columns(&self) -> Cow<'_, [Vec<Felt>]> {
771        self.0.periodic_columns()
772    }
773}
774
775impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for HandwrittenMidenAir {
776    fn num_randomness(&self) -> usize {
777        LiftedAir::<Felt, EF>::num_randomness(&self.0)
778    }
779
780    fn aux_width(&self) -> usize {
781        LiftedAir::<Felt, EF>::aux_width(&self.0)
782    }
783
784    fn num_aux_values(&self) -> usize {
785        LiftedAir::<Felt, EF>::num_aux_values(&self.0)
786    }
787
788    fn build_aux_trace(
789        &self,
790        main: &RowMajorMatrix<Felt>,
791        air_inputs: &[Felt],
792        aux_inputs: &[Felt],
793        challenges: &[EF],
794    ) -> (RowMajorMatrix<EF>, Vec<EF>) {
795        self.0.build_aux_trace(main, air_inputs, aux_inputs, challenges)
796    }
797
798    fn build_aux_trace_with_preprocessed(
799        &self,
800        main: &RowMajorMatrix<Felt>,
801        preprocessed: Option<&RowMajorMatrix<Felt>>,
802        air_inputs: &[Felt],
803        aux_inputs: &[Felt],
804        challenges: &[EF],
805    ) -> (RowMajorMatrix<EF>, Vec<EF>) {
806        self.0.build_aux_trace_with_preprocessed(
807            main,
808            preprocessed,
809            air_inputs,
810            aux_inputs,
811            challenges,
812        )
813    }
814
815    fn constraint_degree(&self) -> ConstraintDegrees {
816        LiftedAir::<Felt, EF>::constraint_degree(&self.0)
817    }
818
819    fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
820        self.0.eval_handwritten(builder)
821    }
822}
823
824impl BaseAir<Felt> for MidenAir {
825    fn width(&self) -> usize {
826        match self {
827            Self::Core => CoreAir.width(),
828            Self::Chiplets => ChipletsAir.width(),
829            Self::EidosCompression => EidosCompressionAir.width(),
830            Self::And8Lookup => And8LookupAir.width(),
831        }
832    }
833
834    fn preprocessed_trace(&self) -> Option<RowMajorMatrix<Felt>> {
835        match self {
836            Self::And8Lookup => Some(And8LookupAir.preprocessed_trace()),
837            Self::Core | Self::Chiplets | Self::EidosCompression => None,
838        }
839    }
840
841    fn preprocessed_width(&self) -> usize {
842        match self {
843            Self::And8Lookup => And8LookupAir.preprocessed_width(),
844            Self::Core | Self::Chiplets | Self::EidosCompression => 0,
845        }
846    }
847
848    fn num_public_values(&self) -> usize {
849        match self {
850            Self::Core => NUM_PUBLIC_VALUES,
851            Self::Chiplets | Self::EidosCompression | Self::And8Lookup => 0,
852        }
853    }
854
855    fn periodic_columns(&self) -> Cow<'_, [Vec<Felt>]> {
856        Cow::Owned(match self {
857            Self::Core => CoreAir.periodic_columns(),
858            Self::Chiplets => ChipletsAir.periodic_columns(),
859            Self::EidosCompression => EidosCompressionAir.periodic_columns(),
860            Self::And8Lookup => Vec::new(),
861        })
862    }
863}
864
865impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for MidenAir {
866    fn num_randomness(&self) -> usize {
867        // Instance-level: every AIR shares the same LogUp challenge set.
868        trace::AUX_TRACE_RAND_CHALLENGES
869    }
870
871    fn aux_width(&self) -> usize {
872        match self {
873            Self::Core => CoreAir.aux_width(),
874            Self::Chiplets => ChipletsAir.aux_width(),
875            Self::EidosCompression => EidosCompressionAir.aux_width(),
876            Self::And8Lookup => And8LookupAir.aux_width(),
877        }
878    }
879
880    fn num_aux_values(&self) -> usize {
881        // One committed normalized LogUp sum `sigma_prime = sigma / n` per AIR instance.
882        1
883    }
884
885    fn build_aux_trace(
886        &self,
887        main: &RowMajorMatrix<Felt>,
888        air_inputs: &[Felt],
889        aux_inputs: &[Felt],
890        challenges: &[EF],
891    ) -> (RowMajorMatrix<EF>, Vec<EF>) {
892        let preprocessed = self.preprocessed_trace();
893        self.build_aux_trace_with_preprocessed(
894            main,
895            preprocessed.as_ref(),
896            air_inputs,
897            aux_inputs,
898            challenges,
899        )
900    }
901
902    fn build_aux_trace_with_preprocessed(
903        &self,
904        main: &RowMajorMatrix<Felt>,
905        preprocessed: Option<&RowMajorMatrix<Felt>>,
906        _air_inputs: &[Felt],
907        _aux_inputs: &[Felt],
908        challenges: &[EF],
909    ) -> (RowMajorMatrix<EF>, Vec<EF>) {
910        let (aux_trace, committed) =
911            build_logup_aux_trace_with_preprocessed(self, main, preprocessed, challenges);
912        debug_assert_eq!(
913            committed.len(),
914            1,
915            "build_logup_aux_trace returns one normalized LogUp sum per AIR"
916        );
917        (aux_trace, committed)
918    }
919
920    fn constraint_degree(&self) -> ConstraintDegrees {
921        match self {
922            Self::Core | Self::Chiplets => ConstraintDegrees { base: 9, ext: 9 },
923            Self::EidosCompression => ConstraintDegrees { base: 3, ext: 3 },
924            Self::And8Lookup => ConstraintDegrees { base: 0, ext: 3 },
925        }
926    }
927
928    fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
929        // The generated, globally-CSE'd evaluators: same constraint values in
930        // the same global order as the hand-written definitions, machine-checked
931        // by tests/generated_drift.rs. The definitions themselves remain
932        // available via `eval_handwritten`.
933        match self {
934            Self::Core => constraints::generated::eval_core(builder),
935            Self::Chiplets => constraints::generated::eval_chiplets(builder),
936            Self::EidosCompression => constraints::generated::eval_eidos_compression(builder),
937            Self::And8Lookup => constraints::generated::eval_and8_lookup(builder),
938        }
939    }
940}
941
942impl<LB> LookupAir<LB> for MidenAir
943where
944    LB: MainLookupBuilder
945        + ChipletLookupBuilder
946        + EidosCompressionLookupBuilder
947        + And8LookupBuilder,
948{
949    fn column_shape(&self) -> &[usize] {
950        MidenAir::column_shape(*self)
951    }
952
953    fn max_message_width(&self) -> usize {
954        match self {
955            Self::Core => CoreAir.lookup_max_message_width(),
956            Self::Chiplets => ChipletsAir.lookup_max_message_width(),
957            Self::EidosCompression => EidosCompressionAir.lookup_max_message_width(),
958            Self::And8Lookup => And8LookupAir.lookup_max_message_width(),
959        }
960    }
961
962    fn num_bus_ids(&self) -> usize {
963        match self {
964            Self::Core => CoreAir.lookup_num_bus_ids(),
965            Self::Chiplets => ChipletsAir.lookup_num_bus_ids(),
966            Self::EidosCompression => EidosCompressionAir.lookup_num_bus_ids(),
967            Self::And8Lookup => And8LookupAir.lookup_num_bus_ids(),
968        }
969    }
970
971    fn eval(&self, builder: &mut LB) {
972        match self {
973            Self::Core => CoreAir.lookup_eval(builder),
974            Self::Chiplets => ChipletsAir.lookup_eval(builder),
975            Self::EidosCompression => EidosCompressionAir.lookup_eval(builder),
976            Self::And8Lookup => And8LookupAir.lookup_eval(builder),
977        }
978    }
979
980    fn eval_boundary<B>(&self, boundary: &mut B)
981    where
982        B: BoundaryBuilder<F = LB::F, EF = LB::EF>,
983    {
984        match self {
985            Self::Core => CoreAir.lookup_eval_boundary(boundary),
986            Self::Chiplets => ChipletsAir.lookup_eval_boundary(boundary),
987            Self::EidosCompression => EidosCompressionAir.lookup_eval_boundary(boundary),
988            Self::And8Lookup => And8LookupAir.lookup_eval_boundary(boundary),
989        }
990    }
991}
992
993// MIDEN MULTI-AIR
994// ================================================================================================
995
996/// The cross-AIR statement for the Miden VM proof.
997///
998/// AIR instances come from [`AIRS`], and the external reduction combines the trace-length-weighted
999/// normalized LogUp sums with the open-bus boundary corrections.
1000///
1001/// Instance order is `[Core, Chiplets, EidosCompression, And8Lookup]`; every per-AIR slice follows
1002/// that ordering.
1003#[derive(Copy, Clone, Debug)]
1004pub struct MidenMultiAir;
1005
1006impl MidenMultiAir {
1007    /// Construct the Miden multi-AIR statement marker.
1008    pub const fn new() -> Self {
1009        Self
1010    }
1011}
1012
1013impl Default for MidenMultiAir {
1014    fn default() -> Self {
1015        Self::new()
1016    }
1017}
1018
1019impl<EF: ExtensionField<Felt>> MultiAir<Felt, EF> for MidenMultiAir {
1020    type Air = MidenAir;
1021
1022    fn airs(&self) -> &[MidenAir] {
1023        &AIRS
1024    }
1025
1026    fn max_aux_inputs(&self) -> usize {
1027        // aux_inputs = program hash (1 word) + deferred root (1 word) + the var-len
1028        // kernel-digest group: one `Word` per kernel procedure, capped at
1029        // `KernelDescriptor::MAX_NUM_PROCEDURES`.
1030        AUX_KERNEL_DIGESTS + KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE
1031    }
1032
1033    /// Absorb statement-owned public inputs into the Fiat-Shamir challenger.
1034    ///
1035    /// One complete Eidos block: `[CLAIM_HASH (4) | deferred_root (4)]`, where `CLAIM_HASH` is the
1036    /// canonical execution-claim commitment (see `miden_core::program::ExecutionClaim`) over
1037    /// `program_hash ‖ kernel_H ‖ stack_inputs ‖ stack_outputs`. With the relation digest
1038    /// pre-loaded in the challenger (see `config`), the transcript state after this block
1039    /// realizes the factored statement binding `H(RELATION_DIGEST ‖ CLAIM_HASH ‖ D)`.
1040    ///
1041    /// The kernel digests enter the transcript only through `kernel_H` (see
1042    /// [`hash_kernel_digests`]); the raw stack I/O and digest list remain public values for
1043    /// constraint evaluation and are not separately absorbed.
1044    fn observe<C: CanObserve<Felt>>(
1045        &self,
1046        challenger: &mut C,
1047        air_inputs: &[Vec<Felt>],
1048        aux_inputs: &[Felt],
1049        _log_trace_heights: &[u8],
1050    ) {
1051        let core_inputs = &air_inputs[0];
1052        assert_eq!(core_inputs.len(), NUM_PUBLIC_VALUES, "unexpected public-value count");
1053        assert!(
1054            aux_inputs.len() >= AUX_KERNEL_DIGESTS,
1055            "aux inputs shorter than the fixed program-hash + deferred-root prefix"
1056        );
1057
1058        let kernel_h = hash_kernel_digests(&aux_inputs[AUX_KERNEL_DIGESTS..]);
1059        let program_hash = &aux_inputs[AUX_PROGRAM_HASH..AUX_PROGRAM_HASH + WORD_SIZE];
1060        let deferred_root = &aux_inputs[AUX_DEFERRED_ROOT..AUX_DEFERRED_ROOT + WORD_SIZE];
1061
1062        // Canonical claim encoding P ‖ K ‖ I ‖ O; the offset layout of
1063        // `ExecutionClaim::to_elements`, pinned by `observe_matches_execution_claim_commitment`.
1064        let mut claim = [Felt::ZERO; NUM_CLAIM_ELEMENTS];
1065        claim[0..WORD_SIZE].copy_from_slice(program_hash);
1066        claim[WORD_SIZE..2 * WORD_SIZE].copy_from_slice(&kernel_h);
1067        claim[2 * WORD_SIZE..].copy_from_slice(core_inputs);
1068        let claim_hash = miden_core::program::claim_commitment(&claim);
1069
1070        for &v in claim_hash.as_elements().iter().chain(deferred_root) {
1071            challenger.observe(v);
1072        }
1073    }
1074
1075    /// Cross-AIR LogUp closure: each AIR commits `sigma_prime_i = sigma_i / n_i`, so the
1076    /// trace-length-weighted sum `sum_i(n_i * sigma_prime_i)` plus the per-trace boundary
1077    /// corrections must vanish. Boundary corrections are added once and are not trace-length
1078    /// scaled. `aux_values` and `log_trace_heights` are parallel in instance order. `aux_inputs`
1079    /// carries the program hash and deferred root followed by kernel digests.
1080    fn eval_external(
1081        &self,
1082        challenges: &[EF],
1083        air_inputs: &[Vec<Felt>],
1084        aux_inputs: &[Felt],
1085        aux_values: &[&[EF]],
1086        log_trace_heights: &[u8],
1087    ) -> Result<Vec<EF>, ReductionError> {
1088        if aux_values.len() != AIRS.len() {
1089            return Err(format!(
1090                "expected aux values for {} AIRs, got {}",
1091                AIRS.len(),
1092                aux_values.len()
1093            )
1094            .into());
1095        }
1096        if log_trace_heights.len() != AIRS.len() {
1097            return Err(format!(
1098                "expected log heights for {} AIRs, got {}",
1099                AIRS.len(),
1100                log_trace_heights.len()
1101            )
1102            .into());
1103        }
1104        if challenges.len() != trace::AUX_TRACE_RAND_CHALLENGES {
1105            return Err(format!(
1106                "expected {} aux trace challenges, got {}",
1107                trace::AUX_TRACE_RAND_CHALLENGES,
1108                challenges.len()
1109            )
1110            .into());
1111        }
1112        if air_inputs[0].len() != NUM_PUBLIC_VALUES {
1113            return Err(format!(
1114                "expected {NUM_PUBLIC_VALUES} public values, got {}",
1115                air_inputs[0].len()
1116            )
1117            .into());
1118        }
1119        if aux_inputs.len() < AUX_KERNEL_DIGESTS {
1120            return Err(format!(
1121                "aux_inputs length {} is shorter than the fixed prefix {AUX_KERNEL_DIGESTS}",
1122                aux_inputs.len()
1123            )
1124            .into());
1125        }
1126        let max_aux_inputs = <MidenMultiAir as MultiAir<Felt, EF>>::max_aux_inputs(self);
1127        if aux_inputs.len() > max_aux_inputs {
1128            return Err(format!(
1129                "aux_inputs length {} exceeds maximum {max_aux_inputs}",
1130                aux_inputs.len()
1131            )
1132            .into());
1133        }
1134        let challenges = Challenges::<EF>::new(
1135            challenges[0],
1136            challenges[1],
1137            MIDEN_MAX_MESSAGE_WIDTH,
1138            BusId::COUNT,
1139        );
1140
1141        let mut weighted_aux_sum = EF::ZERO;
1142        let mut boundary_correction = EF::ZERO;
1143        for ((air, values), &log_height) in
1144            AIRS.iter().copied().zip(aux_values.iter()).zip(log_trace_heights)
1145        {
1146            boundary_correction += air.boundary_correction(&challenges, aux_inputs)?;
1147            let expected = <MidenAir as LiftedAir<Felt, EF>>::num_aux_values(&air);
1148            if values.len() != expected {
1149                return Err(format!(
1150                    "{} expects {expected} aux boundary values, got {}",
1151                    air.name(),
1152                    values.len()
1153                )
1154                .into());
1155            }
1156
1157            let trace_length = 1_u64.checked_shl(u32::from(log_height)).ok_or_else(|| {
1158                ReductionError::from(format!(
1159                    "{} log trace height {log_height} does not fit in u64",
1160                    air.name()
1161                ))
1162            })?;
1163            weighted_aux_sum +=
1164                values.iter().copied().sum::<EF>() * Felt::new_unchecked(trace_length);
1165        }
1166
1167        Ok(vec![weighted_aux_sum + boundary_correction])
1168    }
1169}
1170
1171// KERNEL DIGEST SUMMARY HASH
1172// ================================================================================================
1173
1174/// Computes `kernel_H`, the fixed-size commitment to the kernel-procedure digests.
1175///
1176/// This is the canonical [`KernelDescriptor::commitment`] value expressed over the flattened digest
1177/// felts: the domain-tagged linear hash (`hash_elements_in_domain` with
1178/// [`miden_core::program::KERNEL_DOMAIN_TAG`]) of `kernel_felts`. The empty digest list yields
1179/// the canonical empty-input value under the same domain.
1180///
1181/// `kernel_H` is absorbed into the Fiat-Shamir transcript in place of the unbounded kernel
1182/// digest list, committing to the kernel with a fixed-size value.
1183pub fn hash_kernel_digests(kernel_felts: &[Felt]) -> [Felt; WORD_SIZE] {
1184    assert!(
1185        kernel_felts.len().is_multiple_of(WORD_SIZE),
1186        "kernel digest felts must be whole words"
1187    );
1188    assert!(
1189        kernel_felts.len() <= KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE,
1190        "kernel digest felts exceed KernelDescriptor::MAX_NUM_PROCEDURES"
1191    );
1192
1193    hash_kernel_input_felts(kernel_felts)
1194}
1195
1196fn hash_kernel_input_felts(kernel_felts: &[Felt]) -> [Felt; WORD_SIZE] {
1197    miden_core::chiplets::hasher::hash_elements_in_domain(
1198        kernel_felts,
1199        miden_core::program::domain::KERNEL_COMMITMENT,
1200    )
1201    .into()
1202}
1203
1204// REDUCED-AUX BOUNDARY BUILDER
1205// ================================================================================================
1206
1207/// `BoundaryBuilder` impl that reduces each emitted interaction to its LogUp denominator
1208/// contribution `multiplicity / encode(msg)` and sums them into a running `EF` accumulator.
1209///
1210/// Boundary correction is computed from the structured boundary messages emitted by each AIR.
1211///
1212/// Denominators are `alpha + sum_i beta^i * field_i` with random `alpha, beta`; on any
1213/// legitimate proof they are non-zero with overwhelming probability. A malformed proof can still
1214/// drive a denominator to zero, so the reducer captures the first failure and surfaces it as a
1215/// [`ReductionError`] to the verifier rather than panicking.
1216struct ReduceBoundaryBuilder<'a, EF: ExtensionField<Felt>> {
1217    challenges: &'a Challenges<EF>,
1218    var_len_public_inputs: &'a [&'a [Felt]],
1219    sum: EF,
1220    error: Option<ReductionError>,
1221}
1222
1223impl<'a, EF: ExtensionField<Felt>> ReduceBoundaryBuilder<'a, EF> {
1224    fn finalize(self) -> Result<EF, ReductionError> {
1225        match self.error {
1226            Some(err) => Err(err),
1227            None => Ok(self.sum),
1228        }
1229    }
1230}
1231
1232impl<'a, EF: ExtensionField<Felt>> BoundaryBuilder for ReduceBoundaryBuilder<'a, EF> {
1233    type F = Felt;
1234    type EF = EF;
1235
1236    fn var_len_public_inputs(&self) -> &[&[Felt]] {
1237        self.var_len_public_inputs
1238    }
1239
1240    fn insert<M>(&mut self, _name: &'static str, multiplicity: Felt, msg: M)
1241    where
1242        M: LookupMessage<Felt, EF>,
1243    {
1244        if self.error.is_some() {
1245            return;
1246        }
1247        match msg.encode(self.challenges).try_inverse() {
1248            Some(inv) => self.sum += inv * multiplicity,
1249            None => {
1250                self.error = Some("LogUp boundary denominator was zero".into());
1251            },
1252        }
1253    }
1254}
1255
1256// TESTS
1257// ================================================================================================
1258
1259#[cfg(test)]
1260mod tests {
1261    use alloc::string::ToString;
1262
1263    use miden_core::field::{PrimeCharacteristicRing, QuadFelt};
1264
1265    use super::*;
1266
1267    /// Guards the static `constraint_degree` override: if an AIR change moves the symbolic
1268    /// degree away from the declared value, the override must be updated.
1269    #[test]
1270    fn constraint_degree_override_matches_symbolic() {
1271        for air in AIRS {
1272            let symbolic = ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&air);
1273            let declared = <MidenAir as LiftedAir<Felt, QuadFelt>>::constraint_degree(&air);
1274            assert_eq!(declared, symbolic, "static constraint_degree override is stale");
1275        }
1276    }
1277
1278    #[test]
1279    fn core_shape_and_degree_match_design() {
1280        let air = MidenAir::CORE;
1281        let symbolic = ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&HandwrittenMidenAir(air));
1282
1283        assert_eq!(air.width(), 48);
1284        assert_eq!(constraints::columns::NUM_DECODER_COLS, 23);
1285        assert_eq!(LiftedAir::<Felt, QuadFelt>::aux_width(&air), 4);
1286        assert_eq!(symbolic, ConstraintDegrees { base: 9, ext: 9 });
1287        assert_eq!(
1288            LiftedAir::<Felt, QuadFelt>::constraint_degree(&air),
1289            symbolic,
1290            "Core's declared degree must match its handwritten constraints",
1291        );
1292        assert_eq!(miden_crypto::stark::log_quotient_degree::<Felt, QuadFelt, _>(&air), 3);
1293    }
1294
1295    #[test]
1296    fn and8_lookup_shape_and_degree_match_design() {
1297        let air = MidenAir::AND8_LOOKUP;
1298
1299        assert_eq!(air.width(), 7);
1300        assert_eq!(air.preprocessed_width(), 5);
1301        assert_eq!(LiftedAir::<Felt, QuadFelt>::aux_width(&air), 4);
1302        assert_eq!(constraints::lookup::and8_lookup_air::AND8_LOOKUP_COLUMN_SHAPE, [1, 2, 2, 2],);
1303        assert_eq!(
1304            LiftedAir::<Felt, QuadFelt>::constraint_degree(&air),
1305            ConstraintDegrees { base: 0, ext: 3 }
1306        );
1307        assert_eq!(miden_crypto::stark::log_quotient_degree::<Felt, QuadFelt, _>(&air), 1);
1308    }
1309
1310    #[test]
1311    fn eval_external_weights_normalized_sums_by_trace_length() {
1312        let multi_air = MidenMultiAir::new();
1313        let raw_challenges = [QuadFelt::from_u32(7), QuadFelt::from_u32(11)];
1314        let air_inputs = [vec![Felt::ZERO; NUM_PUBLIC_VALUES], vec![], vec![], vec![]];
1315        let aux_inputs = vec![Felt::ZERO; AUX_KERNEL_DIGESTS];
1316        let normalized_sums = [
1317            QuadFelt::from_u32(13),
1318            QuadFelt::from_u32(17),
1319            QuadFelt::from_u32(19),
1320            QuadFelt::from_u32(23),
1321        ];
1322        let core_values = [normalized_sums[0]];
1323        let chiplets_values = [normalized_sums[1]];
1324        let eidos_compression_values = [normalized_sums[2]];
1325        let and8_values = [normalized_sums[3]];
1326        let aux_values: [&[QuadFelt]; MIDEN_AIR_COUNT] =
1327            [&core_values, &chiplets_values, &eidos_compression_values, &and8_values];
1328        let log_trace_heights = [6, 9, 7, 16];
1329
1330        let lookup_challenges = Challenges::new(
1331            raw_challenges[0],
1332            raw_challenges[1],
1333            MIDEN_MAX_MESSAGE_WIDTH,
1334            BusId::COUNT,
1335        );
1336        let mut boundary_correction = QuadFelt::ZERO;
1337        for air in AIRS {
1338            boundary_correction +=
1339                air.boundary_correction(&lookup_challenges, &aux_inputs).unwrap();
1340        }
1341
1342        let result = <MidenMultiAir as MultiAir<Felt, QuadFelt>>::eval_external(
1343            &multi_air,
1344            &raw_challenges,
1345            &air_inputs,
1346            &aux_inputs,
1347            &aux_values,
1348            &log_trace_heights,
1349        )
1350        .unwrap();
1351
1352        let weighted_sum = normalized_sums
1353            .iter()
1354            .zip(log_trace_heights)
1355            .map(|(&value, log_height)| value * Felt::new_unchecked(1_u64 << u32::from(log_height)))
1356            .sum::<QuadFelt>();
1357        assert_eq!(result, vec![weighted_sum + boundary_correction]);
1358    }
1359
1360    #[test]
1361    fn eval_external_rejects_partial_kernel_digest() {
1362        let challenges =
1363            [QuadFelt::from(Felt::new_unchecked(3)), QuadFelt::from(Felt::new_unchecked(5))];
1364        let air_inputs = [vec![Felt::ZERO; NUM_PUBLIC_VALUES], vec![], vec![], vec![]];
1365        let mut aux_inputs = vec![Felt::ZERO; AUX_KERNEL_DIGESTS];
1366        aux_inputs.push(Felt::ONE);
1367        let zero = QuadFelt::from(Felt::ZERO);
1368        let core_aux = [zero];
1369        let chiplets_aux = [zero];
1370        let eidos_compression_aux = [zero];
1371        let and8_aux = [zero];
1372        let aux_values = [
1373            core_aux.as_slice(),
1374            chiplets_aux.as_slice(),
1375            eidos_compression_aux.as_slice(),
1376            and8_aux.as_slice(),
1377        ];
1378
1379        let err = MidenMultiAir::new()
1380            .eval_external(&challenges, &air_inputs, &aux_inputs, &aux_values, &[8, 8, 8, 16])
1381            .unwrap_err();
1382
1383        assert!(err.to_string().contains("kernel digest felts length 1 is not a multiple of 4"));
1384    }
1385
1386    #[test]
1387    fn eval_external_rejects_too_many_kernel_digests() {
1388        let challenges =
1389            [QuadFelt::from(Felt::new_unchecked(3)), QuadFelt::from(Felt::new_unchecked(5))];
1390        let air_inputs = [vec![Felt::ZERO; NUM_PUBLIC_VALUES], vec![], vec![], vec![]];
1391        let max_aux_inputs = AUX_KERNEL_DIGESTS + KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE;
1392        let actual_aux_inputs = max_aux_inputs + WORD_SIZE;
1393        let aux_inputs = vec![Felt::ZERO; actual_aux_inputs];
1394        let zero = QuadFelt::from(Felt::ZERO);
1395        let core_aux = [zero];
1396        let chiplets_aux = [zero];
1397        let eidos_compression_aux = [zero];
1398        let and8_aux = [zero];
1399        let aux_values = [
1400            core_aux.as_slice(),
1401            chiplets_aux.as_slice(),
1402            eidos_compression_aux.as_slice(),
1403            and8_aux.as_slice(),
1404        ];
1405
1406        let err = MidenMultiAir::new()
1407            .eval_external(&challenges, &air_inputs, &aux_inputs, &aux_values, &[8, 8, 8, 16])
1408            .unwrap_err();
1409
1410        assert!(err.to_string().contains(&format!(
1411            "aux_inputs length {actual_aux_inputs} exceeds maximum {max_aux_inputs}"
1412        )));
1413    }
1414
1415    #[test]
1416    fn hash_kernel_digests_matches_kernel_descriptor_commitment() {
1417        // The transcript-side helper and `KernelDescriptor::commitment` are two computations of
1418        // the same normative value; this pins them together (including the empty kernel).
1419        use miden_core::Word;
1420
1421        let word = |a: u64| -> Word {
1422            [Felt::new_unchecked(a), Felt::new_unchecked(a + 1), Felt::ZERO, Felt::ONE].into()
1423        };
1424        for procs in [vec![], vec![word(10)], vec![word(10), word(20), word(30)]] {
1425            let descriptor = KernelDescriptor::from_hashes(procs).unwrap();
1426            let flattened: Vec<Felt> =
1427                descriptor.proc_hashes().iter().flat_map(|w| w.as_elements().to_vec()).collect();
1428            assert_eq!(
1429                Word::new(hash_kernel_digests(&flattened)),
1430                descriptor.commitment(),
1431                "hash_kernel_digests diverged from KernelDescriptor::commitment"
1432            );
1433        }
1434    }
1435
1436    #[test]
1437    #[should_panic(expected = "kernel digest felts exceed KernelDescriptor::MAX_NUM_PROCEDURES")]
1438    fn hash_kernel_digests_rejects_too_many_digest_felts() {
1439        let kernel_felts = vec![Felt::ZERO; (KernelDescriptor::MAX_NUM_PROCEDURES + 1) * WORD_SIZE];
1440
1441        let _ = hash_kernel_digests(&kernel_felts);
1442    }
1443
1444    #[test]
1445    fn observe_matches_execution_claim_commitment() {
1446        // The transcript's statement block must open with exactly
1447        // `ExecutionClaim::commitment()` for the same statement, followed by the deferred
1448        // root — pinning `observe`'s inline claim encoding to the canonical one.
1449        use miden_core::{field::QuadFelt, program::ExecutionClaim};
1450
1451        #[derive(Default)]
1452        struct FeltSink {
1453            observed: Vec<Felt>,
1454        }
1455        impl CanObserve<Felt> for FeltSink {
1456            fn observe(&mut self, value: Felt) {
1457                self.observed.push(value);
1458            }
1459        }
1460
1461        let word = |a: u64| -> Word {
1462            [
1463                Felt::new_unchecked(a),
1464                Felt::new_unchecked(a + 1),
1465                Felt::new_unchecked(a + 2),
1466                Felt::new_unchecked(a + 3),
1467            ]
1468            .into()
1469        };
1470        let kernel = KernelDescriptor::from_hashes(vec![word(50), word(60)]).unwrap();
1471        let program_hash = word(1);
1472        let stack_inputs =
1473            StackInputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(6)]).unwrap();
1474        let stack_outputs = StackOutputs::new(&[Felt::new_unchecked(7)]).unwrap();
1475        let deferred_root = word(90);
1476
1477        let claim = ExecutionClaim::from_program_info(
1478            ProgramInfo::new(program_hash, kernel.clone()),
1479            stack_inputs,
1480            stack_outputs,
1481        );
1482
1483        // air_inputs = I ‖ O; aux_inputs = P ‖ D ‖ kernel digest felts.
1484        let mut air_inputs = [Felt::ZERO; NUM_PUBLIC_VALUES];
1485        air_inputs[0..MIN_STACK_DEPTH].copy_from_slice(&stack_inputs[..]);
1486        air_inputs[MIN_STACK_DEPTH..].copy_from_slice(&stack_outputs[..]);
1487        let air_inputs = [air_inputs.to_vec(), vec![], vec![], vec![]];
1488        let mut aux_inputs: Vec<Felt> = Vec::new();
1489        aux_inputs.extend(program_hash.as_elements());
1490        aux_inputs.extend(deferred_root.as_elements());
1491        aux_inputs.extend(Word::words_as_elements(kernel.proc_hashes()));
1492
1493        let mut sink = FeltSink::default();
1494        <MidenMultiAir as MultiAir<Felt, QuadFelt>>::observe(
1495            &MidenMultiAir::new(),
1496            &mut sink,
1497            &air_inputs,
1498            &aux_inputs,
1499            &[10, 10, 10, 16],
1500        );
1501
1502        let mut expected: Vec<Felt> = claim.commitment().as_elements().to_vec();
1503        expected.extend(deferred_root.as_elements());
1504        assert_eq!(sink.observed, expected, "observe must emit [CLAIM_HASH | D]");
1505    }
1506
1507    #[test]
1508    #[should_panic(
1509        expected = "aux inputs shorter than the fixed program-hash + deferred-root prefix"
1510    )]
1511    fn observe_rejects_short_aux_inputs() {
1512        #[derive(Default)]
1513        struct FeltSink {
1514            observed: Vec<Felt>,
1515        }
1516
1517        impl CanObserve<Felt> for FeltSink {
1518            fn observe(&mut self, value: Felt) {
1519                self.observed.push(value);
1520            }
1521        }
1522
1523        let mut challenger = FeltSink::default();
1524        let air_inputs = [vec![Felt::ZERO; NUM_PUBLIC_VALUES], vec![], vec![], vec![]];
1525        let multi_air = MidenMultiAir::new();
1526
1527        <MidenMultiAir as MultiAir<Felt, QuadFelt>>::observe(
1528            &multi_air,
1529            &mut challenger,
1530            &air_inputs,
1531            &[],
1532            &[8, 8, 8, 16],
1533        );
1534    }
1535}