1#![no_std]
2
3#[macro_use]
4extern crate alloc;
5
6#[cfg(feature = "std")]
7extern crate std;
8
9use alloc::vec::Vec;
10use core::borrow::Borrow;
11
12use miden_core::{
13 WORD_SIZE, Word,
14 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 trace;
35
36pub mod logup {
43 pub use crate::constraints::lookup::{
44 BusId, MIDEN_MAX_MESSAGE_WIDTH, messages::*, miden_air::NUM_LOGUP_COMMITTED_FINALS,
45 };
46}
47
48use constraints::lookup::{
49 chiplet_air::ChipletLookupBuilder,
50 main_air::{MainLookupAir, MainLookupBuilder},
51 poseidon2_permutation_air::Poseidon2PermutationLookupBuilder,
52};
53pub use constraints::{
54 chiplets::columns::{
55 AceCols, AceEvalCols, AceReadCols, BitwiseCols, ControllerCols, KernelRomCols, MemoryCols,
56 },
57 columns::{ChipletCols, CoreCols},
58 decoder::columns::DecoderCols,
59 ext_field::QuadFeltExpr,
60 poseidon2_permutation::columns::{
61 CYCLE_INPUT_ROW, CYCLE_OUTPUT_ROW, INITIAL_EXTERNAL_ROUND_END,
62 INITIAL_EXTERNAL_ROUND_START, INTERNAL_PLUS_EXTERNAL_ROW, LAST_INTERNAL_ROUND_ARK_IDX,
63 NUM_PACKED_INTERNAL_ROUND_ROWS, NUM_SBOX_WITNESSES, NUM_TRAILING_EXTERNAL_ROUND_ROWS,
64 PACKED_INTERNAL_ROUND_START, Poseidon2PermutationCols, Poseidon2PermutationPeriodicCols,
65 },
66 range::columns::RangeCols,
67 stack::columns::StackCols,
68 system::columns::SystemCols,
69};
70use logup::{BusId, MIDEN_MAX_MESSAGE_WIDTH};
71use lookup::{
72 BoundaryBuilder, Challenges, ConstraintLookupBuilder, LookupAir, LookupMessage,
73 build_logup_aux_trace,
74};
75use miden_core::utils::RowMajorMatrix;
76
77mod export {
80 pub use miden_core::{
81 Felt,
82 serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
83 utils::ToElements,
84 };
85 pub use miden_crypto::stark::{
86 StarkConfig,
87 air::{
88 AirBuilder, BaseAir, ConstraintDegrees, ExtensionBuilder, LiftedAir, LiftedAirBuilder,
89 MultiAir, PermutationAirBuilder, ProverStatement, Statement,
90 },
91 debug,
92 pcs::PcsParams,
93 };
94}
95
96pub use export::*;
97pub use proof_order::{
98 AIRS, MIDEN_AIR_COUNT, PROOF_ORDER_COUNT, PROOF_ORDER_REGISTRY_DEPTH, ProofOrder,
99};
100
101pub trait MidenAirBuilder: LiftedAirBuilder<F = Felt> {}
109impl<T: LiftedAirBuilder<F = Felt>> MidenAirBuilder for T {}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
115#[cfg_attr(
116 all(feature = "arbitrary", test),
117 miden_test_serialization_macros::serialization_test
118)]
119pub struct PublicInputs {
120 program_info: ProgramInfo,
121 stack_inputs: StackInputs,
122 stack_outputs: StackOutputs,
123 deferred_root: DeferredRoot,
124}
125
126impl PublicInputs {
127 pub fn new(
130 program_info: ProgramInfo,
131 stack_inputs: StackInputs,
132 stack_outputs: StackOutputs,
133 deferred_root: DeferredRoot,
134 ) -> Self {
135 Self {
136 program_info,
137 stack_inputs,
138 stack_outputs,
139 deferred_root,
140 }
141 }
142
143 pub fn stack_inputs(&self) -> StackInputs {
144 self.stack_inputs
145 }
146
147 pub fn stack_outputs(&self) -> StackOutputs {
148 self.stack_outputs
149 }
150
151 pub fn program_info(&self) -> ProgramInfo {
152 self.program_info.clone()
153 }
154
155 pub fn deferred_root(&self) -> DeferredRoot {
157 self.deferred_root
158 }
159
160 pub fn kernel_commitment(&self) -> Word {
165 self.program_info.kernel_commitment()
166 }
167
168 pub fn to_air_inputs(&self) -> (Vec<Felt>, Vec<Felt>) {
180 let mut air_inputs = Vec::with_capacity(NUM_PUBLIC_VALUES);
181 air_inputs.extend_from_slice(self.stack_inputs.as_ref());
182 air_inputs.extend_from_slice(self.stack_outputs.as_ref());
183
184 let kernel_felts = Word::words_as_elements(self.program_info.kernel_procedures());
185 let mut aux_inputs = Vec::with_capacity(AUX_KERNEL_DIGESTS + kernel_felts.len());
186 aux_inputs.extend_from_slice(self.program_info.program_hash().as_elements());
187 aux_inputs.extend_from_slice(self.deferred_root.as_ref());
188 aux_inputs.extend_from_slice(kernel_felts);
189
190 (air_inputs, aux_inputs)
191 }
192
193 pub fn to_elements(&self) -> Vec<Felt> {
199 let mut result = self.program_info.to_elements();
200 result.extend_from_slice(self.stack_inputs.as_ref());
201 result.extend_from_slice(self.stack_outputs.as_ref());
202 result.extend_from_slice(self.deferred_root.as_ref());
203 result
204 }
205}
206
207#[cfg(feature = "arbitrary")]
208impl Arbitrary for PublicInputs {
209 type Parameters = ();
210 type Strategy = BoxedStrategy<Self>;
211
212 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
213 fn felt_strategy() -> impl Strategy<Value = Felt> {
214 any::<u32>().prop_map(Felt::from)
215 }
216
217 fn word_strategy() -> impl Strategy<Value = Word> {
218 any::<[u32; WORD_SIZE]>().prop_map(|values| Word::new(values.map(Felt::from)))
219 }
220
221 let program_info = word_strategy()
222 .prop_map(|program_hash| ProgramInfo::new(program_hash, KernelDescriptor::default()));
223 let stack_inputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
224 .prop_map(|values| StackInputs::new(&values).expect("generated stack inputs fit"));
225 let stack_outputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
226 .prop_map(|values| StackOutputs::new(&values).expect("generated stack outputs fit"));
227
228 (program_info, stack_inputs, stack_outputs, word_strategy())
229 .prop_map(|(program_info, stack_inputs, stack_outputs, deferred_root)| {
230 Self::new(program_info, stack_inputs, stack_outputs, deferred_root)
231 })
232 .boxed()
233 }
234}
235
236impl Serializable for PublicInputs {
240 fn write_into<W: ByteWriter>(&self, target: &mut W) {
241 self.program_info.write_into(target);
242 self.stack_inputs.write_into(target);
243 self.stack_outputs.write_into(target);
244 self.deferred_root.write_into(target);
245 }
246}
247
248impl Deserializable for PublicInputs {
249 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
250 let program_info = ProgramInfo::read_from(source)?;
251 let stack_inputs = StackInputs::read_from(source)?;
252 let stack_outputs = StackOutputs::read_from(source)?;
253 let deferred_root = DeferredRoot::read_from(source)?;
254
255 Ok(PublicInputs {
256 program_info,
257 stack_inputs,
258 stack_outputs,
259 deferred_root,
260 })
261 }
262}
263
264pub const NUM_PUBLIC_VALUES: usize = MIN_STACK_DEPTH + MIN_STACK_DEPTH;
277
278pub const LOGUP_AUX_TRACE_WIDTH: usize = 8;
280
281const AUX_PROGRAM_HASH: usize = 0;
285const AUX_DEFERRED_ROOT: usize = WORD_SIZE;
286const AUX_KERNEL_DIGESTS: usize = 2 * WORD_SIZE;
287
288#[derive(Copy, Clone, Debug, Default)]
295pub struct CoreAir;
296
297impl CoreAir {
298 fn width(self) -> usize {
299 constraints::columns::NUM_CORE_COLS
300 }
301
302 fn periodic_columns(self) -> Vec<Vec<Felt>> {
303 Vec::new()
304 }
305
306 fn aux_width(self) -> usize {
307 constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
308 }
309
310 fn boundary_correction<EF: ExtensionField<Felt>>(
315 self,
316 challenges: &Challenges<EF>,
317 public_values: &[Felt],
318 boundary_inputs: &[&[Felt]],
319 ) -> Result<EF, ReductionError> {
320 if boundary_inputs.len() != 1 {
321 return Err(format!(
322 "CoreAir expects 1 boundary input slice, got {}",
323 boundary_inputs.len()
324 )
325 .into());
326 }
327 if boundary_inputs[0].len() != 2 * WORD_SIZE {
328 return Err(format!(
329 "CoreAir expects {} boundary felts (program hash + deferred root), got {}",
330 2 * WORD_SIZE,
331 boundary_inputs[0].len()
332 )
333 .into());
334 }
335
336 let mut reducer = ReduceBoundaryBuilder {
337 challenges,
338 public_values,
339 var_len_public_inputs: boundary_inputs,
340 sum: EF::ZERO,
341 error: None,
342 };
343 constraints::lookup::miden_air::emit_core_boundary(&mut reducer);
344 reducer.finalize()
345 }
346
347 fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
348 let main = builder.main();
349 let local: &CoreCols<AB::Var> = (*main.current_slice()).borrow();
350 let next: &CoreCols<AB::Var> = (*main.next_slice()).borrow();
351
352 let op_flags =
353 constraints::op_flags::OpFlags::new(&local.decoder, &local.stack, &next.decoder);
354
355 constraints::enforce_core(builder, local, next, &op_flags);
356 constraints::public_inputs::enforce_main(builder, local);
357
358 let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::Core);
359 self.lookup_eval(&mut lb);
360 }
361
362 fn lookup_num_columns(self) -> usize {
363 constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
364 }
365
366 fn lookup_column_shape(self) -> &'static [usize] {
367 &constraints::lookup::main_air::MAIN_COLUMN_SHAPE
368 }
369
370 fn lookup_max_message_width(self) -> usize {
371 MIDEN_MAX_MESSAGE_WIDTH
372 }
373
374 fn lookup_num_bus_ids(self) -> usize {
375 BusId::COUNT
376 }
377
378 fn lookup_eval<LB: MainLookupBuilder>(self, builder: &mut LB) {
379 MainLookupAir.eval(builder);
380 }
381
382 fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
383 constraints::lookup::miden_air::emit_core_boundary(boundary);
384 }
385}
386
387#[derive(Copy, Clone, Debug, Default)]
395pub struct ChipletsAir;
396
397impl ChipletsAir {
398 fn width(self) -> usize {
399 constraints::columns::NUM_CHIPLETS_COLS
400 }
401
402 fn periodic_columns(self) -> Vec<Vec<Felt>> {
403 constraints::chiplets::columns::PeriodicCols::periodic_columns()
404 }
405
406 fn aux_width(self) -> usize {
407 constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
408 }
409
410 fn boundary_correction<EF: ExtensionField<Felt>>(
415 self,
416 challenges: &Challenges<EF>,
417 public_values: &[Felt],
418 boundary_inputs: &[&[Felt]],
419 ) -> Result<EF, ReductionError> {
420 if boundary_inputs.len() != 1 {
421 return Err(format!(
422 "ChipletsAir expects 1 boundary input slice, got {}",
423 boundary_inputs.len()
424 )
425 .into());
426 }
427 if !boundary_inputs[0].len().is_multiple_of(WORD_SIZE) {
428 return Err(format!(
429 "kernel digest felts length {} is not a multiple of {}",
430 boundary_inputs[0].len(),
431 WORD_SIZE
432 )
433 .into());
434 }
435
436 let mut reducer = ReduceBoundaryBuilder {
437 challenges,
438 public_values,
439 var_len_public_inputs: boundary_inputs,
440 sum: EF::ZERO,
441 error: None,
442 };
443 constraints::lookup::miden_air::emit_chiplets_boundary(&mut reducer);
444 reducer.finalize()
445 }
446
447 fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
448 let main = builder.main();
449 let local: &ChipletCols<AB::Var> = (*main.current_slice()).borrow();
450 let next: &ChipletCols<AB::Var> = (*main.next_slice()).borrow();
451
452 let selectors =
453 constraints::chiplets::selectors::build_chiplet_selectors(builder, local, next);
454
455 constraints::enforce_chiplets(builder, local, next, &selectors);
456
457 let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::Chiplets);
458 self.lookup_eval(&mut lb);
459 }
460
461 fn lookup_num_columns(self) -> usize {
462 constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
463 }
464
465 fn lookup_column_shape(self) -> &'static [usize] {
466 &constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE
467 }
468
469 fn lookup_max_message_width(self) -> usize {
470 MIDEN_MAX_MESSAGE_WIDTH
471 }
472
473 fn lookup_num_bus_ids(self) -> usize {
474 BusId::COUNT
475 }
476
477 fn lookup_eval<LB: ChipletLookupBuilder>(self, builder: &mut LB) {
478 let main = builder.main();
479 let local: &ChipletCols<_> = main.current_slice().borrow();
480 let next: &ChipletCols<_> = main.next_slice().borrow();
481
482 constraints::lookup::chiplet_air::emit_chiplet_lookup_columns(builder, local, next);
483 }
484
485 fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
486 constraints::lookup::miden_air::emit_chiplets_boundary(boundary);
487 }
488}
489
490#[derive(Copy, Clone, Debug, Default)]
497pub struct Poseidon2PermutationAir;
498
499impl Poseidon2PermutationAir {
500 fn width(self) -> usize {
501 constraints::poseidon2_permutation::columns::NUM_POSEIDON2_PERMUTATION_COLS
502 }
503
504 fn periodic_columns(self) -> Vec<Vec<Felt>> {
505 Poseidon2PermutationPeriodicCols::periodic_columns()
506 }
507
508 fn aux_width(self) -> usize {
509 constraints::lookup::poseidon2_permutation_air::POSEIDON2_PERMUTATION_COLUMN_SHAPE.len()
510 }
511
512 fn boundary_correction<EF: ExtensionField<Felt>>(
513 self,
514 _challenges: &Challenges<EF>,
515 _public_values: &[Felt],
516 boundary_inputs: &[&[Felt]],
517 ) -> Result<EF, ReductionError> {
518 if !boundary_inputs.is_empty() {
519 return Err(format!(
520 "Poseidon2PermutationAir expects 0 boundary input slices, got {}",
521 boundary_inputs.len()
522 )
523 .into());
524 }
525 Ok(EF::ZERO)
526 }
527
528 fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
529 constraints::enforce_poseidon2_permutation(builder);
530
531 let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::Poseidon2Permutation);
532 self.lookup_eval(&mut lb);
533 }
534
535 fn lookup_num_columns(self) -> usize {
536 constraints::lookup::poseidon2_permutation_air::POSEIDON2_PERMUTATION_COLUMN_SHAPE.len()
537 }
538
539 fn lookup_column_shape(self) -> &'static [usize] {
540 &constraints::lookup::poseidon2_permutation_air::POSEIDON2_PERMUTATION_COLUMN_SHAPE
541 }
542
543 fn lookup_max_message_width(self) -> usize {
544 MIDEN_MAX_MESSAGE_WIDTH
545 }
546
547 fn lookup_num_bus_ids(self) -> usize {
548 BusId::COUNT
549 }
550
551 fn lookup_eval<LB: Poseidon2PermutationLookupBuilder>(self, builder: &mut LB) {
552 let main = builder.main();
553 let local: &Poseidon2PermutationCols<_> = main.current_slice().borrow();
554
555 constraints::lookup::poseidon2_permutation_air::emit_poseidon2_permutation_lookup_columns(
556 builder, local,
557 );
558 }
559
560 fn lookup_eval_boundary<B: BoundaryBuilder>(self, _boundary: &mut B) {}
561}
562
563#[derive(Copy, Clone, Debug, Eq, PartialEq)]
572pub enum MidenAir {
573 Core,
574 Chiplets,
575 Poseidon2Permutation,
576}
577
578impl MidenAir {
579 pub const fn instance_index(self) -> usize {
580 match self {
581 Self::Core => 0,
582 Self::Chiplets => 1,
583 Self::Poseidon2Permutation => 2,
584 }
585 }
586
587 pub const fn name(self) -> &'static str {
588 match self {
589 Self::Core => "Core",
590 Self::Chiplets => "Chiplets",
591 Self::Poseidon2Permutation => "Poseidon2Permutation",
592 }
593 }
594
595 pub const fn file_token(self) -> &'static str {
596 match self {
597 Self::Core => "core",
598 Self::Chiplets => "chiplets",
599 Self::Poseidon2Permutation => "poseidon2_permutation",
600 }
601 }
602
603 fn boundary_correction<EF: ExtensionField<Felt>>(
604 self,
605 challenges: &Challenges<EF>,
606 public_values: &[Felt],
607 aux_inputs: &[Felt],
608 ) -> Result<EF, ReductionError> {
609 if aux_inputs.len() < AUX_KERNEL_DIGESTS {
610 return Err(format!(
611 "aux_inputs length {} is shorter than the fixed prefix {AUX_KERNEL_DIGESTS}",
612 aux_inputs.len()
613 )
614 .into());
615 }
616
617 match self {
618 Self::Core => CoreAir.boundary_correction(
619 challenges,
620 public_values,
621 &[&aux_inputs[..AUX_KERNEL_DIGESTS]],
622 ),
623 Self::Chiplets => ChipletsAir.boundary_correction(
624 challenges,
625 public_values,
626 &[&aux_inputs[AUX_KERNEL_DIGESTS..]],
627 ),
628 Self::Poseidon2Permutation => {
629 Poseidon2PermutationAir.boundary_correction(challenges, public_values, &[])
630 },
631 }
632 }
633
634 pub fn eval_handwritten<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
641 match self {
642 Self::Core => CoreAir.eval(builder),
643 Self::Chiplets => ChipletsAir.eval(builder),
644 Self::Poseidon2Permutation => Poseidon2PermutationAir.eval(builder),
645 }
646 }
647}
648
649#[derive(Copy, Clone, Debug)]
656pub struct HandwrittenMidenAir(pub MidenAir);
657
658impl BaseAir<Felt> for HandwrittenMidenAir {
659 fn width(&self) -> usize {
660 self.0.width()
661 }
662
663 fn num_public_values(&self) -> usize {
664 BaseAir::<Felt>::num_public_values(&self.0)
665 }
666
667 fn periodic_columns(&self) -> Vec<Vec<Felt>> {
668 self.0.periodic_columns()
669 }
670}
671
672impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for HandwrittenMidenAir {
673 fn num_randomness(&self) -> usize {
674 LiftedAir::<Felt, EF>::num_randomness(&self.0)
675 }
676
677 fn aux_width(&self) -> usize {
678 LiftedAir::<Felt, EF>::aux_width(&self.0)
679 }
680
681 fn num_aux_values(&self) -> usize {
682 LiftedAir::<Felt, EF>::num_aux_values(&self.0)
683 }
684
685 fn build_aux_trace(
686 &self,
687 main: &RowMajorMatrix<Felt>,
688 air_inputs: &[Felt],
689 aux_inputs: &[Felt],
690 challenges: &[EF],
691 ) -> (RowMajorMatrix<EF>, Vec<EF>) {
692 self.0.build_aux_trace(main, air_inputs, aux_inputs, challenges)
693 }
694
695 fn constraint_degree(&self) -> ConstraintDegrees {
696 LiftedAir::<Felt, EF>::constraint_degree(&self.0)
697 }
698
699 fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
700 self.0.eval_handwritten(builder)
701 }
702}
703
704impl BaseAir<Felt> for MidenAir {
705 fn width(&self) -> usize {
706 match self {
707 Self::Core => CoreAir.width(),
708 Self::Chiplets => ChipletsAir.width(),
709 Self::Poseidon2Permutation => Poseidon2PermutationAir.width(),
710 }
711 }
712
713 fn num_public_values(&self) -> usize {
714 NUM_PUBLIC_VALUES
715 }
716
717 fn periodic_columns(&self) -> Vec<Vec<Felt>> {
718 match self {
719 Self::Core => CoreAir.periodic_columns(),
720 Self::Chiplets => ChipletsAir.periodic_columns(),
721 Self::Poseidon2Permutation => Poseidon2PermutationAir.periodic_columns(),
722 }
723 }
724}
725
726impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for MidenAir {
727 fn num_randomness(&self) -> usize {
728 trace::AUX_TRACE_RAND_CHALLENGES
730 }
731
732 fn aux_width(&self) -> usize {
733 match self {
734 Self::Core => CoreAir.aux_width(),
735 Self::Chiplets => ChipletsAir.aux_width(),
736 Self::Poseidon2Permutation => Poseidon2PermutationAir.aux_width(),
737 }
738 }
739
740 fn num_aux_values(&self) -> usize {
741 1
743 }
744
745 fn build_aux_trace(
746 &self,
747 main: &RowMajorMatrix<Felt>,
748 _air_inputs: &[Felt],
749 _aux_inputs: &[Felt],
750 challenges: &[EF],
751 ) -> (RowMajorMatrix<EF>, Vec<EF>) {
752 let (aux_trace, committed) = build_logup_aux_trace(self, main, challenges);
753 debug_assert_eq!(
754 committed.len(),
755 1,
756 "build_logup_aux_trace returns one normalized LogUp sum per AIR"
757 );
758 (aux_trace, committed)
759 }
760
761 fn constraint_degree(&self) -> ConstraintDegrees {
762 match self {
763 Self::Core | Self::Chiplets => ConstraintDegrees { base: 9, ext: 9 },
764 Self::Poseidon2Permutation => ConstraintDegrees { base: 8, ext: 3 },
765 }
766 }
767
768 fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
769 match self {
774 Self::Core => constraints::generated::eval_core(builder),
775 Self::Chiplets => constraints::generated::eval_chiplets(builder),
776 Self::Poseidon2Permutation => {
777 constraints::generated::eval_poseidon2_permutation(builder)
778 },
779 }
780 }
781}
782
783impl<LB> LookupAir<LB> for MidenAir
784where
785 LB: MainLookupBuilder + ChipletLookupBuilder + Poseidon2PermutationLookupBuilder,
786{
787 fn num_columns(&self) -> usize {
788 match self {
789 Self::Core => CoreAir.lookup_num_columns(),
790 Self::Chiplets => ChipletsAir.lookup_num_columns(),
791 Self::Poseidon2Permutation => Poseidon2PermutationAir.lookup_num_columns(),
792 }
793 }
794
795 fn column_shape(&self) -> &[usize] {
796 match self {
797 Self::Core => CoreAir.lookup_column_shape(),
798 Self::Chiplets => ChipletsAir.lookup_column_shape(),
799 Self::Poseidon2Permutation => Poseidon2PermutationAir.lookup_column_shape(),
800 }
801 }
802
803 fn max_message_width(&self) -> usize {
804 match self {
805 Self::Core => CoreAir.lookup_max_message_width(),
806 Self::Chiplets => ChipletsAir.lookup_max_message_width(),
807 Self::Poseidon2Permutation => Poseidon2PermutationAir.lookup_max_message_width(),
808 }
809 }
810
811 fn num_bus_ids(&self) -> usize {
812 match self {
813 Self::Core => CoreAir.lookup_num_bus_ids(),
814 Self::Chiplets => ChipletsAir.lookup_num_bus_ids(),
815 Self::Poseidon2Permutation => Poseidon2PermutationAir.lookup_num_bus_ids(),
816 }
817 }
818
819 fn eval(&self, builder: &mut LB) {
820 match self {
821 Self::Core => CoreAir.lookup_eval(builder),
822 Self::Chiplets => ChipletsAir.lookup_eval(builder),
823 Self::Poseidon2Permutation => Poseidon2PermutationAir.lookup_eval(builder),
824 }
825 }
826
827 fn eval_boundary<B>(&self, boundary: &mut B)
828 where
829 B: BoundaryBuilder<F = LB::F, EF = LB::EF>,
830 {
831 match self {
832 Self::Core => CoreAir.lookup_eval_boundary(boundary),
833 Self::Chiplets => ChipletsAir.lookup_eval_boundary(boundary),
834 Self::Poseidon2Permutation => Poseidon2PermutationAir.lookup_eval_boundary(boundary),
835 }
836 }
837}
838
839#[derive(Copy, Clone, Debug)]
850pub struct MidenMultiAir;
851
852impl MidenMultiAir {
853 pub const fn new() -> Self {
855 Self
856 }
857}
858
859impl Default for MidenMultiAir {
860 fn default() -> Self {
861 Self::new()
862 }
863}
864
865impl<EF: ExtensionField<Felt>> MultiAir<Felt, EF> for MidenMultiAir {
866 type Air = MidenAir;
867
868 fn airs(&self) -> &[MidenAir] {
869 &AIRS
870 }
871
872 fn num_air_inputs(&self) -> usize {
873 NUM_PUBLIC_VALUES
874 }
875
876 fn max_aux_inputs(&self) -> usize {
877 AUX_KERNEL_DIGESTS + KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE
881 }
882
883 fn observe<C: CanObserve<Felt>>(
895 &self,
896 challenger: &mut C,
897 air_inputs: &[Felt],
898 aux_inputs: &[Felt],
899 _log_trace_heights: &[u8],
900 ) {
901 assert_eq!(air_inputs.len(), NUM_PUBLIC_VALUES, "unexpected public-value count");
902 assert!(
903 aux_inputs.len() >= AUX_KERNEL_DIGESTS,
904 "aux inputs shorter than the fixed program-hash + deferred-root prefix"
905 );
906
907 let kernel_h = hash_kernel_digests(&aux_inputs[AUX_KERNEL_DIGESTS..]);
908 let program_hash = &aux_inputs[AUX_PROGRAM_HASH..AUX_PROGRAM_HASH + WORD_SIZE];
909 let deferred_root = &aux_inputs[AUX_DEFERRED_ROOT..AUX_DEFERRED_ROOT + WORD_SIZE];
910
911 let mut claim = [Felt::ZERO; NUM_CLAIM_ELEMENTS];
914 claim[0..WORD_SIZE].copy_from_slice(program_hash);
915 claim[WORD_SIZE..2 * WORD_SIZE].copy_from_slice(&kernel_h);
916 claim[2 * WORD_SIZE..].copy_from_slice(air_inputs);
917 let claim_hash = miden_core::program::claim_commitment(&claim);
918
919 for &v in claim_hash.as_elements().iter().chain(deferred_root) {
920 challenger.observe(v);
921 }
922 }
923
924 fn eval_external(
930 &self,
931 challenges: &[EF],
932 air_inputs: &[Felt],
933 aux_inputs: &[Felt],
934 aux_values: &[&[EF]],
935 log_trace_heights: &[u8],
936 ) -> Result<Vec<EF>, ReductionError> {
937 if aux_values.len() != AIRS.len() {
938 return Err(format!(
939 "expected aux values for {} AIRs, got {}",
940 AIRS.len(),
941 aux_values.len()
942 )
943 .into());
944 }
945 if log_trace_heights.len() != AIRS.len() {
946 return Err(format!(
947 "expected log heights for {} AIRs, got {}",
948 AIRS.len(),
949 log_trace_heights.len()
950 )
951 .into());
952 }
953 if challenges.len() != trace::AUX_TRACE_RAND_CHALLENGES {
954 return Err(format!(
955 "expected {} aux trace challenges, got {}",
956 trace::AUX_TRACE_RAND_CHALLENGES,
957 challenges.len()
958 )
959 .into());
960 }
961 if air_inputs.len() != NUM_PUBLIC_VALUES {
962 return Err(format!(
963 "expected {NUM_PUBLIC_VALUES} public values, got {}",
964 air_inputs.len()
965 )
966 .into());
967 }
968 if aux_inputs.len() < AUX_KERNEL_DIGESTS {
969 return Err(format!(
970 "aux_inputs length {} is shorter than the fixed prefix {AUX_KERNEL_DIGESTS}",
971 aux_inputs.len()
972 )
973 .into());
974 }
975 let max_aux_inputs = <MidenMultiAir as MultiAir<Felt, EF>>::max_aux_inputs(self);
976 if aux_inputs.len() > max_aux_inputs {
977 return Err(format!(
978 "aux_inputs length {} exceeds maximum {max_aux_inputs}",
979 aux_inputs.len()
980 )
981 .into());
982 }
983 let challenges = Challenges::<EF>::new(
984 challenges[0],
985 challenges[1],
986 MIDEN_MAX_MESSAGE_WIDTH,
987 BusId::COUNT,
988 );
989
990 let mut weighted_aux_sum = EF::ZERO;
991 let mut boundary_correction = EF::ZERO;
992 for ((air, values), &log_height) in
993 AIRS.iter().copied().zip(aux_values.iter()).zip(log_trace_heights)
994 {
995 boundary_correction += air.boundary_correction(&challenges, air_inputs, aux_inputs)?;
996 let expected = <MidenAir as LiftedAir<Felt, EF>>::num_aux_values(&air);
997 if values.len() != expected {
998 return Err(format!(
999 "{} expects {expected} aux boundary values, got {}",
1000 air.name(),
1001 values.len()
1002 )
1003 .into());
1004 }
1005
1006 let trace_length = 1_u64.checked_shl(u32::from(log_height)).ok_or_else(|| {
1007 ReductionError::from(format!(
1008 "{} log trace height {log_height} does not fit in u64",
1009 air.name()
1010 ))
1011 })?;
1012 weighted_aux_sum +=
1013 values.iter().copied().sum::<EF>() * Felt::new_unchecked(trace_length);
1014 }
1015
1016 Ok(vec![weighted_aux_sum + boundary_correction])
1017 }
1018}
1019
1020pub fn hash_kernel_digests(kernel_felts: &[Felt]) -> [Felt; WORD_SIZE] {
1033 assert!(
1034 kernel_felts.len().is_multiple_of(WORD_SIZE),
1035 "kernel digest felts must be whole words"
1036 );
1037 assert!(
1038 kernel_felts.len() <= KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE,
1039 "kernel digest felts exceed KernelDescriptor::MAX_NUM_PROCEDURES"
1040 );
1041
1042 hash_kernel_input_felts(kernel_felts)
1043}
1044
1045fn hash_kernel_input_felts(kernel_felts: &[Felt]) -> [Felt; WORD_SIZE] {
1046 miden_core::chiplets::hasher::hash_elements_in_domain(
1047 kernel_felts,
1048 miden_core::program::KERNEL_DOMAIN_TAG,
1049 )
1050 .into()
1051}
1052
1053struct ReduceBoundaryBuilder<'a, EF: ExtensionField<Felt>> {
1066 challenges: &'a Challenges<EF>,
1067 public_values: &'a [Felt],
1068 var_len_public_inputs: &'a [&'a [Felt]],
1069 sum: EF,
1070 error: Option<ReductionError>,
1071}
1072
1073impl<'a, EF: ExtensionField<Felt>> ReduceBoundaryBuilder<'a, EF> {
1074 fn finalize(self) -> Result<EF, ReductionError> {
1075 match self.error {
1076 Some(err) => Err(err),
1077 None => Ok(self.sum),
1078 }
1079 }
1080}
1081
1082impl<'a, EF: ExtensionField<Felt>> BoundaryBuilder for ReduceBoundaryBuilder<'a, EF> {
1083 type F = Felt;
1084 type EF = EF;
1085
1086 fn public_values(&self) -> &[Felt] {
1087 self.public_values
1088 }
1089
1090 fn var_len_public_inputs(&self) -> &[&[Felt]] {
1091 self.var_len_public_inputs
1092 }
1093
1094 fn insert<M>(&mut self, _name: &'static str, multiplicity: Felt, msg: M)
1095 where
1096 M: LookupMessage<Felt, EF>,
1097 {
1098 if self.error.is_some() {
1099 return;
1100 }
1101 match msg.encode(self.challenges).try_inverse() {
1102 Some(inv) => self.sum += inv * multiplicity,
1103 None => {
1104 self.error = Some("LogUp boundary denominator was zero".into());
1105 },
1106 }
1107 }
1108}
1109
1110#[cfg(test)]
1114mod tests {
1115 use alloc::string::ToString;
1116
1117 use miden_core::field::{PrimeCharacteristicRing, QuadFelt};
1118
1119 use super::*;
1120
1121 #[test]
1124 fn constraint_degree_override_matches_symbolic() {
1125 for air in AIRS {
1126 let symbolic = ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&air);
1127 let declared = <MidenAir as LiftedAir<Felt, QuadFelt>>::constraint_degree(&air);
1128 assert_eq!(declared, symbolic, "static constraint_degree override is stale");
1129 }
1130 }
1131
1132 #[test]
1133 fn eval_external_weights_normalized_sums_by_trace_length() {
1134 let multi_air = MidenMultiAir::new();
1135 let raw_challenges = [QuadFelt::from_u32(7), QuadFelt::from_u32(11)];
1136 let air_inputs = vec![Felt::ZERO; NUM_PUBLIC_VALUES];
1137 let aux_inputs = vec![Felt::ZERO; AUX_KERNEL_DIGESTS];
1138 let normalized_sums =
1139 [QuadFelt::from_u32(13), QuadFelt::from_u32(17), QuadFelt::from_u32(19)];
1140 let core_values = [normalized_sums[0]];
1141 let chiplets_values = [normalized_sums[1]];
1142 let poseidon2_values = [normalized_sums[2]];
1143 let aux_values: [&[QuadFelt]; MIDEN_AIR_COUNT] =
1144 [&core_values, &chiplets_values, &poseidon2_values];
1145 let log_trace_heights = [6, 9, 7];
1146
1147 let lookup_challenges = Challenges::new(
1148 raw_challenges[0],
1149 raw_challenges[1],
1150 MIDEN_MAX_MESSAGE_WIDTH,
1151 BusId::COUNT,
1152 );
1153 let mut boundary_correction = QuadFelt::ZERO;
1154 for air in AIRS {
1155 boundary_correction +=
1156 air.boundary_correction(&lookup_challenges, &air_inputs, &aux_inputs).unwrap();
1157 }
1158
1159 let result = <MidenMultiAir as MultiAir<Felt, QuadFelt>>::eval_external(
1160 &multi_air,
1161 &raw_challenges,
1162 &air_inputs,
1163 &aux_inputs,
1164 &aux_values,
1165 &log_trace_heights,
1166 )
1167 .unwrap();
1168
1169 let weighted_sum = normalized_sums
1170 .iter()
1171 .zip(log_trace_heights)
1172 .map(|(&value, log_height)| value * Felt::new_unchecked(1_u64 << u32::from(log_height)))
1173 .sum::<QuadFelt>();
1174 assert_eq!(result, vec![weighted_sum + boundary_correction]);
1175 }
1176
1177 #[test]
1178 fn eval_external_rejects_partial_kernel_digest() {
1179 let challenges =
1180 [QuadFelt::from(Felt::new_unchecked(3)), QuadFelt::from(Felt::new_unchecked(5))];
1181 let air_inputs = vec![Felt::ZERO; NUM_PUBLIC_VALUES];
1182 let mut aux_inputs = vec![Felt::ZERO; AUX_KERNEL_DIGESTS];
1183 aux_inputs.push(Felt::ONE);
1184 let zero = QuadFelt::from(Felt::ZERO);
1185 let core_aux = [zero];
1186 let chiplets_aux = [zero];
1187 let poseidon2_aux = [zero];
1188 let aux_values = [core_aux.as_slice(), chiplets_aux.as_slice(), poseidon2_aux.as_slice()];
1189
1190 let err = MidenMultiAir::new()
1191 .eval_external(&challenges, &air_inputs, &aux_inputs, &aux_values, &[8, 8, 8])
1192 .unwrap_err();
1193
1194 assert!(err.to_string().contains("kernel digest felts length 1 is not a multiple of 4"));
1195 }
1196
1197 #[test]
1198 fn eval_external_rejects_too_many_kernel_digests() {
1199 let challenges =
1200 [QuadFelt::from(Felt::new_unchecked(3)), QuadFelt::from(Felt::new_unchecked(5))];
1201 let air_inputs = vec![Felt::ZERO; NUM_PUBLIC_VALUES];
1202 let max_aux_inputs = AUX_KERNEL_DIGESTS + KernelDescriptor::MAX_NUM_PROCEDURES * WORD_SIZE;
1203 let actual_aux_inputs = max_aux_inputs + WORD_SIZE;
1204 let aux_inputs = vec![Felt::ZERO; actual_aux_inputs];
1205 let zero = QuadFelt::from(Felt::ZERO);
1206 let core_aux = [zero];
1207 let chiplets_aux = [zero];
1208 let poseidon2_aux = [zero];
1209 let aux_values = [core_aux.as_slice(), chiplets_aux.as_slice(), poseidon2_aux.as_slice()];
1210
1211 let err = MidenMultiAir::new()
1212 .eval_external(&challenges, &air_inputs, &aux_inputs, &aux_values, &[8, 8, 8])
1213 .unwrap_err();
1214
1215 assert!(err.to_string().contains(&format!(
1216 "aux_inputs length {actual_aux_inputs} exceeds maximum {max_aux_inputs}"
1217 )));
1218 }
1219
1220 #[test]
1221 fn hash_kernel_digests_matches_kernel_descriptor_commitment() {
1222 use miden_core::Word;
1225
1226 let word = |a: u64| -> Word {
1227 [Felt::new_unchecked(a), Felt::new_unchecked(a + 1), Felt::ZERO, Felt::ONE].into()
1228 };
1229 for procs in [vec![], vec![word(10)], vec![word(10), word(20), word(30)]] {
1230 let descriptor = KernelDescriptor::from_hashes(procs).unwrap();
1231 let flattened: Vec<Felt> =
1232 descriptor.proc_hashes().iter().flat_map(|w| w.as_elements().to_vec()).collect();
1233 assert_eq!(
1234 Word::new(hash_kernel_digests(&flattened)),
1235 descriptor.commitment(),
1236 "hash_kernel_digests diverged from KernelDescriptor::commitment"
1237 );
1238 }
1239 }
1240
1241 #[test]
1242 #[should_panic(expected = "kernel digest felts exceed KernelDescriptor::MAX_NUM_PROCEDURES")]
1243 fn hash_kernel_digests_rejects_too_many_digest_felts() {
1244 let kernel_felts = vec![Felt::ZERO; (KernelDescriptor::MAX_NUM_PROCEDURES + 1) * WORD_SIZE];
1245
1246 let _ = hash_kernel_digests(&kernel_felts);
1247 }
1248
1249 #[test]
1250 fn observe_matches_execution_claim_commitment() {
1251 use miden_core::{field::QuadFelt, program::ExecutionClaim};
1255
1256 #[derive(Default)]
1257 struct FeltSink {
1258 observed: Vec<Felt>,
1259 }
1260 impl CanObserve<Felt> for FeltSink {
1261 fn observe(&mut self, value: Felt) {
1262 self.observed.push(value);
1263 }
1264 }
1265
1266 let word = |a: u64| -> Word {
1267 [
1268 Felt::new_unchecked(a),
1269 Felt::new_unchecked(a + 1),
1270 Felt::new_unchecked(a + 2),
1271 Felt::new_unchecked(a + 3),
1272 ]
1273 .into()
1274 };
1275 let kernel = KernelDescriptor::from_hashes(vec![word(50), word(60)]).unwrap();
1276 let program_hash = word(1);
1277 let stack_inputs =
1278 StackInputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(6)]).unwrap();
1279 let stack_outputs = StackOutputs::new(&[Felt::new_unchecked(7)]).unwrap();
1280 let deferred_root = word(90);
1281
1282 let claim = ExecutionClaim::from_program_info(
1283 ProgramInfo::new(program_hash, kernel.clone()),
1284 stack_inputs,
1285 stack_outputs,
1286 );
1287
1288 let mut air_inputs = [Felt::ZERO; NUM_PUBLIC_VALUES];
1290 air_inputs[0..MIN_STACK_DEPTH].copy_from_slice(&stack_inputs[..]);
1291 air_inputs[MIN_STACK_DEPTH..].copy_from_slice(&stack_outputs[..]);
1292 let mut aux_inputs: Vec<Felt> = Vec::new();
1293 aux_inputs.extend(program_hash.as_elements());
1294 aux_inputs.extend(deferred_root.as_elements());
1295 aux_inputs.extend(Word::words_as_elements(kernel.proc_hashes()));
1296
1297 let mut sink = FeltSink::default();
1298 <MidenMultiAir as MultiAir<Felt, QuadFelt>>::observe(
1299 &MidenMultiAir::new(),
1300 &mut sink,
1301 &air_inputs,
1302 &aux_inputs,
1303 &[10, 10, 10],
1304 );
1305
1306 let mut expected: Vec<Felt> = claim.commitment().as_elements().to_vec();
1307 expected.extend(deferred_root.as_elements());
1308 assert_eq!(sink.observed, expected, "observe must emit [CLAIM_HASH | D]");
1309 }
1310
1311 #[test]
1312 #[should_panic(
1313 expected = "aux inputs shorter than the fixed program-hash + deferred-root prefix"
1314 )]
1315 fn observe_rejects_short_aux_inputs() {
1316 #[derive(Default)]
1317 struct FeltSink {
1318 observed: Vec<Felt>,
1319 }
1320
1321 impl CanObserve<Felt> for FeltSink {
1322 fn observe(&mut self, value: Felt) {
1323 self.observed.push(value);
1324 }
1325 }
1326
1327 let mut challenger = FeltSink::default();
1328 let air_inputs = vec![Felt::ZERO; NUM_PUBLIC_VALUES];
1329 let multi_air = MidenMultiAir::new();
1330
1331 <MidenMultiAir as MultiAir<Felt, QuadFelt>>::observe(
1332 &multi_air,
1333 &mut challenger,
1334 &air_inputs,
1335 &[],
1336 &[8, 8, 8],
1337 );
1338 }
1339}