Skip to main content

miden_air/
lib.rs

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
12#[cfg(feature = "arbitrary")]
13use miden_core::program::Kernel;
14use miden_core::{
15    WORD_SIZE, Word,
16    field::ExtensionField,
17    precompile::PrecompileTranscriptState,
18    program::{MIN_STACK_DEPTH, ProgramInfo, StackInputs, StackOutputs},
19};
20use miden_crypto::stark::air::{
21    ReducedAuxValues, ReductionError, VarLenPublicInputs, WindowAccess,
22};
23#[cfg(feature = "arbitrary")]
24use proptest::prelude::*;
25
26pub mod ace;
27pub mod config;
28mod constraints;
29pub mod lookup;
30pub mod trace;
31
32/// Miden VM-specific LogUp lookup argument: bus identifiers and bus message types.
33///
34/// The `LookupAir` and `AuxBuilder` trait impls live directly on [`crate::ProcessorAir`].
35/// The generic LogUp framework this builds on lives in [`crate::lookup`] and is free of
36/// Miden-specific types so it can be extracted into its own crate.
37pub mod logup {
38    pub use crate::constraints::lookup::{
39        BusId, MIDEN_MAX_MESSAGE_WIDTH, messages::*, miden_air::NUM_LOGUP_COMMITTED_FINALS,
40    };
41}
42
43use constraints::{
44    columns::MainCols,
45    lookup::{
46        chiplet_air::{ChipletLookupAir, ChipletLookupBuilder},
47        main_air::{MainLookupAir, MainLookupBuilder},
48        miden_air::{MIDEN_COLUMN_SHAPE, emit_miden_boundary},
49    },
50};
51use logup::{BusId, MIDEN_MAX_MESSAGE_WIDTH, NUM_LOGUP_COMMITTED_FINALS};
52use lookup::{
53    BoundaryBuilder, Challenges, ConstraintLookupBuilder, LookupAir, LookupMessage,
54    build_logup_aux_trace,
55};
56use miden_core::utils::RowMajorMatrix;
57use trace::TRACE_WIDTH;
58
59// RE-EXPORTS
60// ================================================================================================
61mod export {
62    pub use miden_core::{
63        Felt,
64        serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
65        utils::ToElements,
66    };
67    pub use miden_crypto::stark::{
68        air::{
69            AirBuilder, AuxBuilder, BaseAir, ExtensionBuilder, LiftedAir, LiftedAirBuilder,
70            PermutationAirBuilder,
71        },
72        debug,
73    };
74    pub use miden_lifted_stark::AirWitness;
75}
76
77pub use export::*;
78
79// MIDEN AIR BUILDER
80// ================================================================================================
81
82/// Convenience super-trait that pins `LiftedAirBuilder` to our field.
83///
84/// All constraint functions in this crate should be generic over `AB: MidenAirBuilder`
85/// instead of spelling out the full `LiftedAirBuilder<F = Felt>` bound.
86pub trait MidenAirBuilder: LiftedAirBuilder<F = Felt> {}
87impl<T: LiftedAirBuilder<F = Felt>> MidenAirBuilder for T {}
88
89// PUBLIC INPUTS
90// ================================================================================================
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[cfg_attr(
94    all(feature = "arbitrary", test),
95    miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false))
96)]
97pub struct PublicInputs {
98    program_info: ProgramInfo,
99    stack_inputs: StackInputs,
100    stack_outputs: StackOutputs,
101    pc_transcript_state: PrecompileTranscriptState,
102}
103
104impl PublicInputs {
105    /// Creates a new instance of `PublicInputs` from program information, stack inputs and outputs,
106    /// and the precompile transcript state (capacity of an internal sponge).
107    pub fn new(
108        program_info: ProgramInfo,
109        stack_inputs: StackInputs,
110        stack_outputs: StackOutputs,
111        pc_transcript_state: PrecompileTranscriptState,
112    ) -> Self {
113        Self {
114            program_info,
115            stack_inputs,
116            stack_outputs,
117            pc_transcript_state,
118        }
119    }
120
121    pub fn stack_inputs(&self) -> StackInputs {
122        self.stack_inputs
123    }
124
125    pub fn stack_outputs(&self) -> StackOutputs {
126        self.stack_outputs
127    }
128
129    pub fn program_info(&self) -> ProgramInfo {
130        self.program_info.clone()
131    }
132
133    /// Returns the precompile transcript state.
134    pub fn pc_transcript_state(&self) -> PrecompileTranscriptState {
135        self.pc_transcript_state
136    }
137
138    /// Returns the fixed-length public values and the variable-length kernel procedure digests
139    /// as a flat slice of `Felt`s.
140    ///
141    /// The fixed-length public values layout is:
142    ///   [0..4]   program hash
143    ///   [4..20]  stack inputs
144    ///   [20..36] stack outputs
145    ///   [36..40] precompile transcript state
146    ///
147    /// The kernel procedure digests are returned as a single flat `Vec<Felt>` (concatenated
148    /// words), to be passed as a single variable-length public input slice to the verifier.
149    pub fn to_air_inputs(&self) -> (Vec<Felt>, Vec<Felt>) {
150        let mut public_values = Vec::with_capacity(NUM_PUBLIC_VALUES);
151        public_values.extend_from_slice(self.program_info.program_hash().as_elements());
152        public_values.extend_from_slice(self.stack_inputs.as_ref());
153        public_values.extend_from_slice(self.stack_outputs.as_ref());
154        public_values.extend_from_slice(self.pc_transcript_state.as_ref());
155
156        let kernel_felts: Vec<Felt> =
157            Word::words_as_elements(self.program_info.kernel_procedures()).to_vec();
158
159        (public_values, kernel_felts)
160    }
161
162    /// Converts public inputs into a vector of field elements (Felt) in the canonical order:
163    /// - program info elements (including kernel procedure hashes)
164    /// - stack inputs
165    /// - stack outputs
166    /// - precompile transcript state
167    pub fn to_elements(&self) -> Vec<Felt> {
168        let mut result = self.program_info.to_elements();
169        result.extend_from_slice(self.stack_inputs.as_ref());
170        result.extend_from_slice(self.stack_outputs.as_ref());
171        result.extend_from_slice(self.pc_transcript_state.as_ref());
172        result
173    }
174}
175
176#[cfg(feature = "arbitrary")]
177impl Arbitrary for PublicInputs {
178    type Parameters = ();
179    type Strategy = BoxedStrategy<Self>;
180
181    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
182        fn felt_strategy() -> impl Strategy<Value = Felt> {
183            any::<u32>().prop_map(Felt::from)
184        }
185
186        fn word_strategy() -> impl Strategy<Value = Word> {
187            any::<[u32; WORD_SIZE]>().prop_map(|values| Word::new(values.map(Felt::from)))
188        }
189
190        let program_info = word_strategy()
191            .prop_map(|program_hash| ProgramInfo::new(program_hash, Kernel::default()));
192        let stack_inputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
193            .prop_map(|values| StackInputs::new(&values).expect("generated stack inputs fit"));
194        let stack_outputs = proptest::collection::vec(felt_strategy(), 0..=MIN_STACK_DEPTH)
195            .prop_map(|values| StackOutputs::new(&values).expect("generated stack outputs fit"));
196
197        (program_info, stack_inputs, stack_outputs, word_strategy())
198            .prop_map(|(program_info, stack_inputs, stack_outputs, pc_transcript_state)| {
199                Self::new(program_info, stack_inputs, stack_outputs, pc_transcript_state)
200            })
201            .boxed()
202    }
203}
204
205// SERIALIZATION
206// ================================================================================================
207
208impl Serializable for PublicInputs {
209    fn write_into<W: ByteWriter>(&self, target: &mut W) {
210        self.program_info.write_into(target);
211        self.stack_inputs.write_into(target);
212        self.stack_outputs.write_into(target);
213        self.pc_transcript_state.write_into(target);
214    }
215}
216
217impl Deserializable for PublicInputs {
218    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
219        let program_info = ProgramInfo::read_from(source)?;
220        let stack_inputs = StackInputs::read_from(source)?;
221        let stack_outputs = StackOutputs::read_from(source)?;
222        let pc_transcript_state = PrecompileTranscriptState::read_from(source)?;
223
224        Ok(PublicInputs {
225            program_info,
226            stack_inputs,
227            stack_outputs,
228            pc_transcript_state,
229        })
230    }
231}
232
233// PROCESSOR AIR
234// ================================================================================================
235
236/// Number of fixed-length public values for the Miden VM AIR.
237///
238/// Layout (40 Felts total):
239///   [0..4]   program hash
240///   [4..20]  stack inputs
241///   [20..36] stack outputs
242///   [36..40] precompile transcript state
243pub const NUM_PUBLIC_VALUES: usize = WORD_SIZE + MIN_STACK_DEPTH + MIN_STACK_DEPTH + WORD_SIZE;
244
245/// LogUp aux trace width: 4 main-trace columns + 3 chiplet-trace columns.
246pub const LOGUP_AUX_TRACE_WIDTH: usize = 7;
247
248// Public values layout offsets.
249const PV_PROGRAM_HASH: usize = 0;
250const PV_TRANSCRIPT_STATE: usize = NUM_PUBLIC_VALUES - WORD_SIZE;
251
252/// Miden VM Processor AIR implementation.
253///
254/// Auxiliary trace building is handled separately via [`AuxBuilder`].
255///
256/// Public-input-dependent boundary checks are performed in [`LiftedAir::reduced_aux_values`].
257/// Aux columns are NOT initialized with boundary terms -- they start at identity. The verifier
258/// independently computes expected boundary messages from variable length public values and checks
259/// them against the final column values.
260#[derive(Copy, Clone, Debug, Default)]
261pub struct ProcessorAir;
262
263// --- Upstream trait impls for ProcessorAir ---
264
265impl BaseAir<Felt> for ProcessorAir {
266    fn width(&self) -> usize {
267        TRACE_WIDTH
268    }
269
270    fn num_public_values(&self) -> usize {
271        NUM_PUBLIC_VALUES
272    }
273}
274
275// --- LiftedAir impl ---
276
277impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for ProcessorAir {
278    fn periodic_columns(&self) -> Vec<Vec<Felt>> {
279        constraints::chiplets::columns::PeriodicCols::periodic_columns()
280    }
281
282    fn num_randomness(&self) -> usize {
283        trace::AUX_TRACE_RAND_CHALLENGES
284    }
285
286    fn aux_width(&self) -> usize {
287        // 4 main-trace + 3 chiplet-trace = 7 LogUp columns. Matches
288        // `ProcessorAir::num_columns()` (LookupAir impl) and the per-row shape returned by
289        // `ProcessorAir::build_aux_trace` (AuxBuilder impl).
290        LOGUP_AUX_TRACE_WIDTH
291    }
292
293    fn num_aux_values(&self) -> usize {
294        NUM_LOGUP_COMMITTED_FINALS
295    }
296
297    /// Returns the number of variable-length public input slices.
298    ///
299    /// The Miden VM AIR uses a single variable-length slice that contains all kernel
300    /// procedure digests as concatenated field elements (each digest is `WORD_SIZE`
301    /// elements). The verifier framework uses this count to validate that the correct
302    /// number of slices is provided.
303    fn num_var_len_public_inputs(&self) -> usize {
304        1
305    }
306
307    fn reduced_aux_values(
308        &self,
309        aux_values: &[EF],
310        challenges: &[EF],
311        public_values: &[Felt],
312        var_len_public_inputs: VarLenPublicInputs<'_, Felt>,
313    ) -> Result<ReducedAuxValues<EF>, ReductionError>
314    where
315        EF: ExtensionField<Felt>,
316    {
317        // LogUp boundary identity. The verifier checks `is_identity()` — i.e.
318        // `prod == ONE && sum == ZERO` — on the accumulated `ReducedAuxValues`
319        // across every AIR. There are no multiplicative bus checks under LogUp, so
320        // `prod = ONE`. The boundary equation lives in `sum`:
321        //
322        //   sum = Σ aux_finals[col]  +  total_correction
323        //
324        // `total_correction` cancels the unmatched-fraction contributions from the
325        // three open buses (block hash, log precompile, kernel ROM init). Rather
326        // than spelling those out here, we drive the shared
327        // `emit_miden_boundary` emitter — the same source `LookupAir::eval_boundary`
328        // uses for the debug walker — through a reducer that accumulates
329        // `Σ multiplicity · encode(msg)⁻¹` into an `EF`.
330        if public_values.len() != NUM_PUBLIC_VALUES {
331            return Err(format!(
332                "expected {} public values, got {}",
333                NUM_PUBLIC_VALUES,
334                public_values.len()
335            )
336            .into());
337        }
338        if var_len_public_inputs.len() != 1 {
339            return Err(format!(
340                "expected 1 var-len public input slice, got {}",
341                var_len_public_inputs.len()
342            )
343            .into());
344        }
345        if !var_len_public_inputs[0].len().is_multiple_of(WORD_SIZE) {
346            return Err(format!(
347                "kernel digest felts length {} is not a multiple of {}",
348                var_len_public_inputs[0].len(),
349                WORD_SIZE
350            )
351            .into());
352        }
353
354        let challenges = Challenges::<EF>::new(
355            challenges[0],
356            challenges[1],
357            MIDEN_MAX_MESSAGE_WIDTH,
358            BusId::COUNT,
359        );
360
361        let mut reducer = ReduceBoundaryBuilder {
362            challenges: &challenges,
363            public_values,
364            var_len_public_inputs,
365            sum: EF::ZERO,
366            error: None,
367        };
368        emit_miden_boundary(&mut reducer);
369        let total_correction = reducer.finalize()?;
370
371        // TODO(#3032): aux_values[1..] are the placeholder slots from
372        // NUM_LOGUP_COMMITTED_FINALS (see `constraints::lookup::miden_air`); enforce the
373        // zero invariant until trace splitting lands. The recursive verifier gets the
374        // matching `AuxBusBoundary(col) = 0` identity via `LogUpBoundaryConfig::zero_columns`
375        // (see `ace.rs`), so both paths reject a nonzero padding slot.
376        for unused_aux in aux_values.iter().skip(1) {
377            if !unused_aux.is_zero() {
378                return Err("padding aux value is non-zero".into());
379            }
380        }
381        let aux_sum: EF = aux_values.iter().copied().sum();
382
383        Ok(ReducedAuxValues {
384            prod: EF::ONE,
385            sum: aux_sum + total_correction,
386        })
387    }
388
389    fn eval<AB: MidenAirBuilder>(&self, builder: &mut AB) {
390        let main = builder.main();
391
392        // Access the two rows: current (local) and next
393        let local = main.current_slice();
394        let next = main.next_slice();
395
396        // Use structured column access via MainTraceCols
397        let local: &MainCols<AB::Var> = (*local).borrow();
398        let next: &MainCols<AB::Var> = (*next).borrow();
399
400        // Build chiplet selectors and op flags once, shared by main and bus constraints.
401        let selectors =
402            constraints::chiplets::selectors::build_chiplet_selectors(builder, local, next);
403        let op_flags =
404            constraints::op_flags::OpFlags::new(&local.decoder, &local.stack, &next.decoder);
405
406        // Main trace constraints.
407        constraints::enforce_main(builder, local, next, &selectors, &op_flags);
408
409        {
410            let mut lb = ConstraintLookupBuilder::new(builder, self);
411            <Self as LookupAir<_>>::eval(self, &mut lb);
412        }
413
414        // Public inputs boundary constraints.
415        constraints::public_inputs::enforce_main(builder, local);
416    }
417
418    fn log_quotient_degree(&self) -> usize
419    where
420        Self: Sized,
421    {
422        // override to avoid recomputing through the SymbolicAir
423        3
424    }
425}
426
427// --- LookupAir impl (7-column aggregator over main + chiplet sub-AIRs) ---
428
429impl<LB> LookupAir<LB> for ProcessorAir
430where
431    LB: MainLookupBuilder + ChipletLookupBuilder,
432{
433    fn num_columns(&self) -> usize {
434        MIDEN_COLUMN_SHAPE.len()
435    }
436
437    fn column_shape(&self) -> &[usize] {
438        &MIDEN_COLUMN_SHAPE
439    }
440
441    fn max_message_width(&self) -> usize {
442        // Width of the `beta_powers` table precomputed by `Challenges::new`, also equal
443        // to the exponent of `gamma = beta^MIDEN_MAX_MESSAGE_WIDTH` used in the per-bus
444        // prefix. Must match the MASM recursive verifier's Poseidon2 absorption loop.
445        // `HasherMsg::State` is the widest live payload at 15 slots (label@β⁰, addr@β¹,
446        // node_index@β², state[0..12]@β³..β¹⁴); the 16th slot is unused slack kept for
447        // MASM transcript alignment.
448        MIDEN_MAX_MESSAGE_WIDTH
449    }
450
451    fn num_bus_ids(&self) -> usize {
452        BusId::COUNT
453    }
454
455    fn eval(&self, builder: &mut LB) {
456        MainLookupAir.eval(builder);
457        ChipletLookupAir.eval(builder);
458    }
459
460    fn eval_boundary<B>(&self, boundary: &mut B)
461    where
462        B: BoundaryBuilder<F = LB::F, EF = LB::EF>,
463    {
464        emit_miden_boundary(boundary);
465    }
466}
467
468// --- AuxBuilder impl (stateless LogUp aux-trace construction) ---
469
470impl<EF> AuxBuilder<Felt, EF> for ProcessorAir
471where
472    EF: ExtensionField<Felt>,
473{
474    fn build_aux_trace(
475        &self,
476        main: &RowMajorMatrix<Felt>,
477        challenges: &[EF],
478    ) -> (RowMajorMatrix<EF>, Vec<EF>) {
479        let (aux_trace, mut committed) = build_logup_aux_trace(self, main, challenges);
480        // TODO(#3032): pad the placeholder slot — see `NUM_LOGUP_COMMITTED_FINALS`. Remove
481        // the pad once trace splitting lands.
482        debug_assert_eq!(
483            committed.len(),
484            1,
485            "build_logup_aux_trace should return exactly one real committed final"
486        );
487        committed.push(EF::ZERO);
488        (aux_trace, committed)
489    }
490}
491
492// REDUCED-AUX BOUNDARY BUILDER
493// ================================================================================================
494
495/// `BoundaryBuilder` impl that reduces each emitted interaction to its LogUp
496/// denominator contribution `multiplicity · encode(msg)⁻¹` and sums them into a
497/// running `EF` accumulator.
498///
499/// Lets `reduced_aux_values` reuse the structured boundary emissions from
500/// [`emit_miden_boundary`] — the same source consumed by the debug walker —
501/// instead of open-coding the three corrections a second time.
502///
503/// Denominators are `α + Σ βⁱ · field_i` with random `α, β`; on any legitimate proof they
504/// are non-zero with overwhelming probability. A malformed/adversarial proof can still
505/// drive a denominator to zero, so the reducer captures the first failure and surfaces it
506/// to `reduced_aux_values`, which bubbles a [`ReductionError`] to the verifier rather than
507/// panicking.
508struct ReduceBoundaryBuilder<'a, EF: ExtensionField<Felt>> {
509    challenges: &'a Challenges<EF>,
510    public_values: &'a [Felt],
511    var_len_public_inputs: VarLenPublicInputs<'a, Felt>,
512    sum: EF,
513    error: Option<ReductionError>,
514}
515
516impl<'a, EF: ExtensionField<Felt>> ReduceBoundaryBuilder<'a, EF> {
517    fn finalize(self) -> Result<EF, ReductionError> {
518        match self.error {
519            Some(err) => Err(err),
520            None => Ok(self.sum),
521        }
522    }
523}
524
525impl<'a, EF: ExtensionField<Felt>> BoundaryBuilder for ReduceBoundaryBuilder<'a, EF> {
526    type F = Felt;
527    type EF = EF;
528
529    fn public_values(&self) -> &[Felt] {
530        self.public_values
531    }
532
533    fn var_len_public_inputs(&self) -> &[&[Felt]] {
534        self.var_len_public_inputs
535    }
536
537    fn insert<M>(&mut self, _name: &'static str, multiplicity: Felt, msg: M)
538    where
539        M: LookupMessage<Felt, EF>,
540    {
541        if self.error.is_some() {
542            return;
543        }
544        match msg.encode(self.challenges).try_inverse() {
545            Some(inv) => self.sum += inv * multiplicity,
546            None => {
547                self.error = Some("LogUp boundary denominator was zero".into());
548            },
549        }
550    }
551}