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 field::ExtensionField,
15 precompile::PrecompileTranscriptState,
16 program::{Kernel, MIN_STACK_DEPTH, ProgramInfo, StackInputs, StackOutputs},
17};
18use miden_crypto::stark::{
19 air::{ReductionError, WindowAccess},
20 challenger::CanObserve,
21};
22#[cfg(feature = "arbitrary")]
23use proptest::prelude::*;
24
25pub mod ace;
26pub mod config;
27mod constraints;
28pub mod lookup;
29pub mod trace;
30
31pub mod logup {
39 pub use crate::constraints::lookup::{
40 BusId, MIDEN_MAX_MESSAGE_WIDTH, messages::*, miden_air::NUM_LOGUP_COMMITTED_FINALS,
41 };
42}
43
44use constraints::lookup::{
45 chiplet_air::ChipletLookupBuilder,
46 main_air::{MainLookupAir, MainLookupBuilder},
47};
48pub use constraints::{
49 chiplets::columns::{
50 AceCols, AceEvalCols, AceReadCols, BitwiseCols, ControllerCols, KernelRomCols, MemoryCols,
51 PermutationCols,
52 },
53 columns::{ChipletCols, CoreCols},
54 decoder::columns::DecoderCols,
55 ext_field::QuadFeltExpr,
56 range::columns::RangeCols,
57 stack::columns::StackCols,
58 system::columns::SystemCols,
59};
60use logup::{BusId, MIDEN_MAX_MESSAGE_WIDTH};
61use lookup::{
62 BoundaryBuilder, Challenges, ConstraintLookupBuilder, LookupAir, LookupMessage,
63 build_logup_aux_trace,
64};
65use miden_core::utils::RowMajorMatrix;
66
67mod export {
70 pub use miden_core::{
71 Felt,
72 serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
73 utils::ToElements,
74 };
75 pub use miden_crypto::stark::{
76 StarkConfig,
77 air::{
78 AirBuilder, BaseAir, ConstraintDegrees, ExtensionBuilder, LiftedAir, LiftedAirBuilder,
79 MultiAir, PermutationAirBuilder, ProverStatement, Statement,
80 },
81 debug,
82 };
83}
84
85pub use export::*;
86
87pub trait MidenAirBuilder: LiftedAirBuilder<F = Felt> {}
95impl<T: LiftedAirBuilder<F = Felt>> MidenAirBuilder for T {}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101#[cfg_attr(
102 all(feature = "arbitrary", test),
103 miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false))
104)]
105pub struct PublicInputs {
106 program_info: ProgramInfo,
107 stack_inputs: StackInputs,
108 stack_outputs: StackOutputs,
109 pc_transcript_state: PrecompileTranscriptState,
110}
111
112impl PublicInputs {
113 pub fn new(
116 program_info: ProgramInfo,
117 stack_inputs: StackInputs,
118 stack_outputs: StackOutputs,
119 pc_transcript_state: PrecompileTranscriptState,
120 ) -> Self {
121 Self {
122 program_info,
123 stack_inputs,
124 stack_outputs,
125 pc_transcript_state,
126 }
127 }
128
129 pub fn stack_inputs(&self) -> StackInputs {
130 self.stack_inputs
131 }
132
133 pub fn stack_outputs(&self) -> StackOutputs {
134 self.stack_outputs
135 }
136
137 pub fn program_info(&self) -> ProgramInfo {
138 self.program_info.clone()
139 }
140
141 pub fn pc_transcript_state(&self) -> PrecompileTranscriptState {
143 self.pc_transcript_state
144 }
145
146 pub fn kernel_commitment(&self) -> Word {
150 self.program_info.kernel_commitment()
151 }
152
153 pub fn to_air_inputs(&self) -> (Vec<Felt>, Vec<Felt>) {
166 let mut air_inputs = Vec::with_capacity(NUM_PUBLIC_VALUES);
167 air_inputs.extend_from_slice(self.stack_inputs.as_ref());
168 air_inputs.extend_from_slice(self.stack_outputs.as_ref());
169
170 let kernel_felts = Word::words_as_elements(self.program_info.kernel_procedures());
171 let mut aux_inputs = Vec::with_capacity(AUX_KERNEL_DIGESTS + kernel_felts.len());
172 aux_inputs.extend_from_slice(self.program_info.program_hash().as_elements());
173 aux_inputs.extend_from_slice(self.pc_transcript_state.as_ref());
174 aux_inputs.extend_from_slice(kernel_felts);
175
176 (air_inputs, aux_inputs)
177 }
178
179 pub fn to_elements(&self) -> Vec<Felt> {
185 let mut result = self.program_info.to_elements();
186 result.extend_from_slice(self.stack_inputs.as_ref());
187 result.extend_from_slice(self.stack_outputs.as_ref());
188 result.extend_from_slice(self.pc_transcript_state.as_ref());
189 result
190 }
191}
192
193#[cfg(feature = "arbitrary")]
194impl Arbitrary for PublicInputs {
195 type Parameters = ();
196 type Strategy = BoxedStrategy<Self>;
197
198 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
199 fn felt_strategy() -> impl Strategy<Value = Felt> {
200 any::<u32>().prop_map(Felt::from)
201 }
202
203 fn word_strategy() -> impl Strategy<Value = Word> {
204 any::<[u32; WORD_SIZE]>().prop_map(|values| Word::new(values.map(Felt::from)))
205 }
206
207 let program_info = word_strategy()
208 .prop_map(|program_hash| ProgramInfo::new(program_hash, Kernel::default()));
209 let stack_inputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
210 .prop_map(|values| StackInputs::new(&values).expect("generated stack inputs fit"));
211 let stack_outputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
212 .prop_map(|values| StackOutputs::new(&values).expect("generated stack outputs fit"));
213
214 (program_info, stack_inputs, stack_outputs, word_strategy())
215 .prop_map(|(program_info, stack_inputs, stack_outputs, pc_transcript_state)| {
216 Self::new(program_info, stack_inputs, stack_outputs, pc_transcript_state)
217 })
218 .boxed()
219 }
220}
221
222impl Serializable for PublicInputs {
226 fn write_into<W: ByteWriter>(&self, target: &mut W) {
227 self.program_info.write_into(target);
228 self.stack_inputs.write_into(target);
229 self.stack_outputs.write_into(target);
230 self.pc_transcript_state.write_into(target);
231 }
232}
233
234impl Deserializable for PublicInputs {
235 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
236 let program_info = ProgramInfo::read_from(source)?;
237 let stack_inputs = StackInputs::read_from(source)?;
238 let stack_outputs = StackOutputs::read_from(source)?;
239 let pc_transcript_state = PrecompileTranscriptState::read_from(source)?;
240
241 Ok(PublicInputs {
242 program_info,
243 stack_inputs,
244 stack_outputs,
245 pc_transcript_state,
246 })
247 }
248}
249
250pub const NUM_PUBLIC_VALUES: usize = MIN_STACK_DEPTH + MIN_STACK_DEPTH;
263
264pub const LOGUP_AUX_TRACE_WIDTH: usize = 7;
266
267const AUX_PROGRAM_HASH: usize = 0;
271const AUX_TRANSCRIPT_STATE: usize = WORD_SIZE;
272const AUX_KERNEL_DIGESTS: usize = 2 * WORD_SIZE;
273
274#[derive(Copy, Clone, Debug, Default)]
282pub struct CoreAir;
283
284impl CoreAir {
285 fn width(self) -> usize {
286 constraints::columns::NUM_CORE_COLS
287 }
288
289 fn periodic_columns(self) -> Vec<Vec<Felt>> {
290 Vec::new()
292 }
293
294 fn aux_width(self) -> usize {
295 constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
296 }
297
298 fn boundary_correction<EF: ExtensionField<Felt>>(
303 self,
304 challenges: &Challenges<EF>,
305 public_values: &[Felt],
306 var_len_public_inputs: &[&[Felt]],
307 ) -> Result<EF, ReductionError> {
308 if var_len_public_inputs.len() != 1 {
309 return Err(format!(
310 "CoreAir expects 1 var-len public input slice, got {}",
311 var_len_public_inputs.len()
312 )
313 .into());
314 }
315 if var_len_public_inputs[0].len() != 2 * WORD_SIZE {
316 return Err(format!(
317 "CoreAir expects {} boundary felts (program hash + transcript state), got {}",
318 2 * WORD_SIZE,
319 var_len_public_inputs[0].len()
320 )
321 .into());
322 }
323
324 let mut reducer = ReduceBoundaryBuilder {
325 challenges,
326 public_values,
327 var_len_public_inputs,
328 sum: EF::ZERO,
329 error: None,
330 };
331 constraints::lookup::miden_air::emit_core_boundary(&mut reducer);
332 reducer.finalize()
333 }
334
335 fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
336 let main = builder.main();
337 let local: &CoreCols<AB::Var> = (*main.current_slice()).borrow();
338 let next: &CoreCols<AB::Var> = (*main.next_slice()).borrow();
339
340 let op_flags =
341 constraints::op_flags::OpFlags::new(&local.decoder, &local.stack, &next.decoder);
342
343 constraints::enforce_core(builder, local, next, &op_flags);
344 constraints::public_inputs::enforce_main(builder, local);
345
346 let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::CORE);
347 self.lookup_eval(&mut lb);
348 }
349
350 fn lookup_num_columns(self) -> usize {
351 constraints::lookup::main_air::MAIN_COLUMN_SHAPE.len()
352 }
353
354 fn lookup_column_shape(self) -> &'static [usize] {
355 &constraints::lookup::main_air::MAIN_COLUMN_SHAPE
356 }
357
358 fn lookup_max_message_width(self) -> usize {
359 MIDEN_MAX_MESSAGE_WIDTH
360 }
361
362 fn lookup_num_bus_ids(self) -> usize {
363 BusId::COUNT
364 }
365
366 fn lookup_eval<LB: MainLookupBuilder>(self, builder: &mut LB) {
367 MainLookupAir.eval(builder);
368 }
369
370 fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
371 constraints::lookup::miden_air::emit_core_boundary(boundary);
372 }
373}
374
375#[derive(Copy, Clone, Debug, Default)]
382pub struct ChipletsAir;
383
384impl ChipletsAir {
387 fn width(self) -> usize {
388 constraints::columns::NUM_CHIPLETS_COLS
389 }
390
391 fn periodic_columns(self) -> Vec<Vec<Felt>> {
392 constraints::chiplets::columns::PeriodicCols::periodic_columns()
395 }
396
397 fn aux_width(self) -> usize {
398 constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
399 }
400
401 fn boundary_correction<EF: ExtensionField<Felt>>(
406 self,
407 challenges: &Challenges<EF>,
408 public_values: &[Felt],
409 var_len_public_inputs: &[&[Felt]],
410 ) -> Result<EF, ReductionError> {
411 if var_len_public_inputs.len() != 1 {
412 return Err(format!(
413 "ChipletsAir expects 1 var-len public input slice, got {}",
414 var_len_public_inputs.len()
415 )
416 .into());
417 }
418 if !var_len_public_inputs[0].len().is_multiple_of(WORD_SIZE) {
419 return Err(format!(
420 "kernel digest felts length {} is not a multiple of {}",
421 var_len_public_inputs[0].len(),
422 WORD_SIZE
423 )
424 .into());
425 }
426
427 let mut reducer = ReduceBoundaryBuilder {
428 challenges,
429 public_values,
430 var_len_public_inputs,
431 sum: EF::ZERO,
432 error: None,
433 };
434 constraints::lookup::miden_air::emit_chiplets_boundary(&mut reducer);
435 reducer.finalize()
436 }
437
438 fn eval<AB: MidenAirBuilder>(self, builder: &mut AB) {
439 let main = builder.main();
440 let local: &ChipletCols<AB::Var> = (*main.current_slice()).borrow();
441 let next: &ChipletCols<AB::Var> = (*main.next_slice()).borrow();
442
443 let selectors =
444 constraints::chiplets::selectors::build_chiplet_selectors(builder, local, next);
445
446 constraints::enforce_chiplets(builder, local, next, &selectors);
447
448 let mut lb = ConstraintLookupBuilder::new(builder, &MidenAir::CHIPLETS);
449 self.lookup_eval(&mut lb);
450 }
451
452 fn lookup_num_columns(self) -> usize {
453 constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE.len()
454 }
455
456 fn lookup_column_shape(self) -> &'static [usize] {
457 &constraints::lookup::chiplet_air::CHIPLET_COLUMN_SHAPE
458 }
459
460 fn lookup_max_message_width(self) -> usize {
461 MIDEN_MAX_MESSAGE_WIDTH
462 }
463
464 fn lookup_num_bus_ids(self) -> usize {
465 BusId::COUNT
466 }
467
468 fn lookup_eval<LB: ChipletLookupBuilder>(self, builder: &mut LB) {
469 let main = builder.main();
470 let local: &ChipletCols<_> = main.current_slice().borrow();
471 let next: &ChipletCols<_> = main.next_slice().borrow();
472
473 constraints::lookup::chiplet_air::emit_chiplet_lookup_columns(builder, local, next);
474 }
475
476 fn lookup_eval_boundary<B: BoundaryBuilder>(self, boundary: &mut B) {
477 constraints::lookup::miden_air::emit_chiplets_boundary(boundary);
478 }
479}
480
481#[derive(Copy, Clone, Debug)]
489pub enum MidenAir {
490 Core(CoreAir),
491 Chiplets(ChipletsAir),
492}
493
494impl MidenAir {
495 pub const CORE: Self = Self::Core(CoreAir);
496 pub const CHIPLETS: Self = Self::Chiplets(ChipletsAir);
497}
498
499impl BaseAir<Felt> for MidenAir {
500 fn width(&self) -> usize {
501 match self {
502 Self::Core(a) => a.width(),
503 Self::Chiplets(a) => a.width(),
504 }
505 }
506
507 fn num_public_values(&self) -> usize {
508 NUM_PUBLIC_VALUES
509 }
510
511 fn periodic_columns(&self) -> Vec<Vec<Felt>> {
512 match self {
513 Self::Core(a) => a.periodic_columns(),
514 Self::Chiplets(a) => a.periodic_columns(),
515 }
516 }
517}
518
519impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for MidenAir {
520 fn num_randomness(&self) -> usize {
521 trace::AUX_TRACE_RAND_CHALLENGES
523 }
524
525 fn aux_width(&self) -> usize {
526 match self {
527 Self::Core(a) => a.aux_width(),
528 Self::Chiplets(a) => a.aux_width(),
529 }
530 }
531
532 fn num_aux_values(&self) -> usize {
533 1
535 }
536
537 fn build_aux_trace(
538 &self,
539 main: &RowMajorMatrix<Felt>,
540 _air_inputs: &[Felt],
541 _aux_inputs: &[Felt],
542 challenges: &[EF],
543 ) -> (RowMajorMatrix<EF>, Vec<EF>) {
544 let (aux_trace, committed) = build_logup_aux_trace(self, main, challenges);
545 debug_assert_eq!(
546 committed.len(),
547 1,
548 "build_logup_aux_trace returns one committed final per AIR (col 0's terminal sum)"
549 );
550 (aux_trace, committed)
551 }
552
553 fn constraint_degree(&self) -> ConstraintDegrees {
554 ConstraintDegrees { base: 9, ext: 9 }
556 }
557
558 fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
559 match self {
560 Self::Core(a) => a.eval(builder),
561 Self::Chiplets(a) => a.eval(builder),
562 }
563 }
564}
565
566impl<LB> LookupAir<LB> for MidenAir
567where
568 LB: MainLookupBuilder + ChipletLookupBuilder,
569{
570 fn num_columns(&self) -> usize {
571 match self {
572 Self::Core(a) => a.lookup_num_columns(),
573 Self::Chiplets(a) => a.lookup_num_columns(),
574 }
575 }
576
577 fn column_shape(&self) -> &[usize] {
578 match self {
579 Self::Core(a) => a.lookup_column_shape(),
580 Self::Chiplets(a) => a.lookup_column_shape(),
581 }
582 }
583
584 fn max_message_width(&self) -> usize {
585 match self {
586 Self::Core(a) => a.lookup_max_message_width(),
587 Self::Chiplets(a) => a.lookup_max_message_width(),
588 }
589 }
590
591 fn num_bus_ids(&self) -> usize {
592 match self {
593 Self::Core(a) => a.lookup_num_bus_ids(),
594 Self::Chiplets(a) => a.lookup_num_bus_ids(),
595 }
596 }
597
598 fn eval(&self, builder: &mut LB) {
599 match self {
600 Self::Core(a) => a.lookup_eval(builder),
601 Self::Chiplets(a) => a.lookup_eval(builder),
602 }
603 }
604
605 fn eval_boundary<B>(&self, boundary: &mut B)
606 where
607 B: BoundaryBuilder<F = LB::F, EF = LB::EF>,
608 {
609 match self {
610 Self::Core(a) => a.lookup_eval_boundary(boundary),
611 Self::Chiplets(a) => a.lookup_eval_boundary(boundary),
612 }
613 }
614}
615
616#[derive(Copy, Clone, Debug)]
626pub struct MidenMultiAir {
627 airs: [MidenAir; 2],
628}
629
630impl MidenMultiAir {
631 pub const fn new() -> Self {
633 Self {
634 airs: [MidenAir::CORE, MidenAir::CHIPLETS],
635 }
636 }
637}
638
639impl Default for MidenMultiAir {
640 fn default() -> Self {
641 Self::new()
642 }
643}
644
645impl<EF: ExtensionField<Felt>> MultiAir<Felt, EF> for MidenMultiAir {
646 type Air = MidenAir;
647
648 fn airs(&self) -> &[MidenAir] {
649 &self.airs
650 }
651
652 fn num_air_inputs(&self) -> usize {
653 NUM_PUBLIC_VALUES
654 }
655
656 fn max_aux_inputs(&self) -> usize {
657 AUX_KERNEL_DIGESTS + Kernel::MAX_NUM_PROCEDURES * WORD_SIZE
661 }
662
663 fn observe<C: CanObserve<Felt>>(
679 &self,
680 challenger: &mut C,
681 air_inputs: &[Felt],
682 aux_inputs: &[Felt],
683 _log_trace_heights: &[u8],
684 ) {
685 assert_eq!(air_inputs.len(), NUM_PUBLIC_VALUES, "unexpected public-value count");
686 assert!(
687 aux_inputs.len() >= AUX_KERNEL_DIGESTS,
688 "aux inputs shorter than the fixed program-hash + transcript-state prefix"
689 );
690
691 let kernel_h = hash_kernel_digests(&aux_inputs[AUX_KERNEL_DIGESTS..]);
692 let program_hash = &aux_inputs[AUX_PROGRAM_HASH..AUX_PROGRAM_HASH + WORD_SIZE];
693 let transcript_state = &aux_inputs[AUX_TRANSCRIPT_STATE..AUX_TRANSCRIPT_STATE + WORD_SIZE];
694 let stack_io = air_inputs;
695
696 for &v in kernel_h.iter().chain(program_hash) {
698 challenger.observe(v);
699 }
700 for &v in transcript_state {
701 challenger.observe(v);
702 }
703 for _ in 0..WORD_SIZE {
704 challenger.observe(Felt::ZERO);
705 }
706 for &v in stack_io {
707 challenger.observe(v);
708 }
709 }
710
711 fn eval_external(
716 &self,
717 challenges: &[EF],
718 air_inputs: &[Felt],
719 aux_inputs: &[Felt],
720 aux_values: &[&[EF]],
721 _log_trace_heights: &[u8],
722 ) -> Result<Vec<EF>, ReductionError> {
723 if aux_inputs.len() < AUX_KERNEL_DIGESTS {
724 return Err(format!(
725 "aux_inputs length {} is shorter than the fixed prefix {AUX_KERNEL_DIGESTS}",
726 aux_inputs.len()
727 )
728 .into());
729 }
730 let challenges = Challenges::<EF>::new(
731 challenges[0],
732 challenges[1],
733 MIDEN_MAX_MESSAGE_WIDTH,
734 BusId::COUNT,
735 );
736
737 let core_correction = CoreAir.boundary_correction(
738 &challenges,
739 air_inputs,
740 &[&aux_inputs[..AUX_KERNEL_DIGESTS]],
741 )?;
742 let chiplets_correction = ChipletsAir.boundary_correction(
743 &challenges,
744 air_inputs,
745 &[&aux_inputs[AUX_KERNEL_DIGESTS..]],
746 )?;
747
748 let aux_sum: EF = aux_values.iter().flat_map(|vals| vals.iter().copied()).sum();
749 Ok(vec![aux_sum + core_correction + chiplets_correction])
750 }
751}
752
753pub fn hash_kernel_digests(kernel_felts: &[Felt]) -> [Felt; WORD_SIZE] {
765 assert!(
766 kernel_felts.len().is_multiple_of(WORD_SIZE),
767 "kernel digest felts must be whole words"
768 );
769
770 miden_core::chiplets::hasher::hash_elements(kernel_felts).into()
771}
772
773struct ReduceBoundaryBuilder<'a, EF: ExtensionField<Felt>> {
789 challenges: &'a Challenges<EF>,
790 public_values: &'a [Felt],
791 var_len_public_inputs: &'a [&'a [Felt]],
792 sum: EF,
793 error: Option<ReductionError>,
794}
795
796impl<'a, EF: ExtensionField<Felt>> ReduceBoundaryBuilder<'a, EF> {
797 fn finalize(self) -> Result<EF, ReductionError> {
798 match self.error {
799 Some(err) => Err(err),
800 None => Ok(self.sum),
801 }
802 }
803}
804
805impl<'a, EF: ExtensionField<Felt>> BoundaryBuilder for ReduceBoundaryBuilder<'a, EF> {
806 type F = Felt;
807 type EF = EF;
808
809 fn public_values(&self) -> &[Felt] {
810 self.public_values
811 }
812
813 fn var_len_public_inputs(&self) -> &[&[Felt]] {
814 self.var_len_public_inputs
815 }
816
817 fn insert<M>(&mut self, _name: &'static str, multiplicity: Felt, msg: M)
818 where
819 M: LookupMessage<Felt, EF>,
820 {
821 if self.error.is_some() {
822 return;
823 }
824 match msg.encode(self.challenges).try_inverse() {
825 Some(inv) => self.sum += inv * multiplicity,
826 None => {
827 self.error = Some("LogUp boundary denominator was zero".into());
828 },
829 }
830 }
831}
832
833#[cfg(test)]
837mod tests {
838 use miden_core::field::QuadFelt;
839
840 use super::*;
841
842 #[test]
845 fn constraint_degree_override_matches_symbolic() {
846 for air in [MidenAir::CORE, MidenAir::CHIPLETS] {
847 let symbolic = ConstraintDegrees::from_air::<Felt, QuadFelt, _>(&air);
848 let declared = <MidenAir as LiftedAir<Felt, QuadFelt>>::constraint_degree(&air);
849 assert_eq!(declared, symbolic, "static constraint_degree override is stale");
850 }
851 }
852}