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
37pub 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
92mod 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
114pub trait MidenAirBuilder: LiftedAirBuilder<F = Felt> {}
122impl<T: LiftedAirBuilder<F = Felt>> MidenAirBuilder for T {}
123
124#[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 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 pub fn deferred_root(&self) -> DeferredRoot {
170 self.deferred_root
171 }
172
173 pub fn kernel_commitment(&self) -> Word {
178 self.program_info.kernel_commitment()
179 }
180
181 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 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
249impl 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
277pub const NUM_PUBLIC_VALUES: usize = MIN_STACK_DEPTH + MIN_STACK_DEPTH;
290
291pub const LOGUP_AUX_TRACE_WIDTH: usize = 9;
295
296const AUX_PROGRAM_HASH: usize = 0;
300const AUX_DEFERRED_ROOT: usize = WORD_SIZE;
301const AUX_KERNEL_DIGESTS: usize = 2 * WORD_SIZE;
302
303#[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 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#[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 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#[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#[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#[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 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 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#[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 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 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 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#[derive(Copy, Clone, Debug)]
1004pub struct MidenMultiAir;
1005
1006impl MidenMultiAir {
1007 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_KERNEL_DIGESTS + KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE
1031 }
1032
1033 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 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 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
1171pub 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
1204struct 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#[cfg(test)]
1260mod tests {
1261 use alloc::string::ToString;
1262
1263 use miden_core::field::{PrimeCharacteristicRing, QuadFelt};
1264
1265 use super::*;
1266
1267 #[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 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 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 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}