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