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