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