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
12use miden_core::{
13    WORD_SIZE, Word,
14    field::ExtensionField,
15    precompile::PrecompileTranscriptState,
16    program::{MIN_STACK_DEPTH, ProgramInfo, StackInputs, StackOutputs},
17};
18use miden_crypto::stark::air::{
19    ReducedAuxValues, ReductionError, VarLenPublicInputs, WindowAccess,
20};
21
22pub mod ace;
23pub mod config;
24mod constraints;
25
26pub mod trace;
27use trace::{AUX_TRACE_WIDTH, MainTraceRow, TRACE_WIDTH};
28
29// RE-EXPORTS
30// ================================================================================================
31mod export {
32    pub use miden_core::{
33        Felt,
34        serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
35        utils::ToElements,
36    };
37    pub use miden_crypto::stark::{
38        air::{
39            AirBuilder, AirWitness, AuxBuilder, BaseAir, ExtensionBuilder, LiftedAir,
40            LiftedAirBuilder, PermutationAirBuilder,
41        },
42        debug,
43    };
44}
45
46pub use export::*;
47
48// PUBLIC INPUTS
49// ================================================================================================
50
51#[derive(Debug, Clone)]
52pub struct PublicInputs {
53    program_info: ProgramInfo,
54    stack_inputs: StackInputs,
55    stack_outputs: StackOutputs,
56    pc_transcript_state: PrecompileTranscriptState,
57}
58
59impl PublicInputs {
60    /// Creates a new instance of `PublicInputs` from program information, stack inputs and outputs,
61    /// and the precompile transcript state (capacity of an internal sponge).
62    pub fn new(
63        program_info: ProgramInfo,
64        stack_inputs: StackInputs,
65        stack_outputs: StackOutputs,
66        pc_transcript_state: PrecompileTranscriptState,
67    ) -> Self {
68        Self {
69            program_info,
70            stack_inputs,
71            stack_outputs,
72            pc_transcript_state,
73        }
74    }
75
76    pub fn stack_inputs(&self) -> StackInputs {
77        self.stack_inputs
78    }
79
80    pub fn stack_outputs(&self) -> StackOutputs {
81        self.stack_outputs
82    }
83
84    pub fn program_info(&self) -> ProgramInfo {
85        self.program_info.clone()
86    }
87
88    /// Returns the precompile transcript state.
89    pub fn pc_transcript_state(&self) -> PrecompileTranscriptState {
90        self.pc_transcript_state
91    }
92
93    /// Returns the fixed-length public values and the variable-length kernel procedure digests
94    /// as a flat slice of `Felt`s.
95    ///
96    /// The fixed-length public values layout is:
97    ///   [0..4]   program hash
98    ///   [4..20]  stack inputs
99    ///   [20..36] stack outputs
100    ///   [36..40] precompile transcript state
101    ///
102    /// The kernel procedure digests are returned as a single flat `Vec<Felt>` (concatenated
103    /// words), to be passed as a single variable-length public input slice to the verifier.
104    pub fn to_air_inputs(&self) -> (Vec<Felt>, Vec<Felt>) {
105        let mut public_values = Vec::with_capacity(NUM_PUBLIC_VALUES);
106        public_values.extend_from_slice(self.program_info.program_hash().as_elements());
107        public_values.extend_from_slice(self.stack_inputs.as_ref());
108        public_values.extend_from_slice(self.stack_outputs.as_ref());
109        public_values.extend_from_slice(self.pc_transcript_state.as_ref());
110
111        let kernel_felts: Vec<Felt> =
112            Word::words_as_elements(self.program_info.kernel_procedures()).to_vec();
113
114        (public_values, kernel_felts)
115    }
116
117    /// Converts public inputs into a vector of field elements (Felt) in the canonical order:
118    /// - program info elements (including kernel procedure hashes)
119    /// - stack inputs
120    /// - stack outputs
121    /// - precompile transcript state
122    pub fn to_elements(&self) -> Vec<Felt> {
123        let mut result = self.program_info.to_elements();
124        result.extend_from_slice(self.stack_inputs.as_ref());
125        result.extend_from_slice(self.stack_outputs.as_ref());
126        result.extend_from_slice(self.pc_transcript_state.as_ref());
127        result
128    }
129}
130
131// SERIALIZATION
132// ================================================================================================
133
134impl Serializable for PublicInputs {
135    fn write_into<W: ByteWriter>(&self, target: &mut W) {
136        self.program_info.write_into(target);
137        self.stack_inputs.write_into(target);
138        self.stack_outputs.write_into(target);
139        self.pc_transcript_state.write_into(target);
140    }
141}
142
143impl Deserializable for PublicInputs {
144    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
145        let program_info = ProgramInfo::read_from(source)?;
146        let stack_inputs = StackInputs::read_from(source)?;
147        let stack_outputs = StackOutputs::read_from(source)?;
148        let pc_transcript_state = PrecompileTranscriptState::read_from(source)?;
149
150        Ok(PublicInputs {
151            program_info,
152            stack_inputs,
153            stack_outputs,
154            pc_transcript_state,
155        })
156    }
157}
158
159// PROCESSOR AIR
160// ================================================================================================
161
162/// Number of fixed-length public values for the Miden VM AIR.
163///
164/// Layout (40 Felts total):
165///   [0..4]   program hash
166///   [4..20]  stack inputs
167///   [20..36] stack outputs
168///   [36..40] precompile transcript state
169pub const NUM_PUBLIC_VALUES: usize = WORD_SIZE + MIN_STACK_DEPTH + MIN_STACK_DEPTH + WORD_SIZE;
170
171// Public values layout offsets.
172const PV_PROGRAM_HASH: usize = 0;
173const PV_TRANSCRIPT_STATE: usize = NUM_PUBLIC_VALUES - WORD_SIZE;
174
175/// Miden VM Processor AIR implementation.
176///
177/// Auxiliary trace building is handled separately via [`AuxBuilder`].
178///
179/// Public-input-dependent boundary checks are performed in [`LiftedAir::reduced_aux_values`].
180/// Aux columns are NOT initialized with boundary terms -- they start at identity. The verifier
181/// independently computes expected boundary messages from variable length public values and checks
182/// them against the final column values.
183#[derive(Copy, Clone, Debug, Default)]
184pub struct ProcessorAir;
185
186// --- Upstream trait impls for ProcessorAir ---
187
188impl BaseAir<Felt> for ProcessorAir {
189    fn width(&self) -> usize {
190        TRACE_WIDTH
191    }
192
193    fn num_public_values(&self) -> usize {
194        NUM_PUBLIC_VALUES
195    }
196}
197
198// --- LiftedAir impl ---
199
200impl<EF: ExtensionField<Felt>> LiftedAir<Felt, EF> for ProcessorAir {
201    fn periodic_columns(&self) -> Vec<Vec<Felt>> {
202        let mut cols = constraints::chiplets::hasher::periodic_columns();
203        cols.extend(constraints::chiplets::bitwise::periodic_columns());
204        cols
205    }
206
207    fn num_randomness(&self) -> usize {
208        trace::AUX_TRACE_RAND_CHALLENGES
209    }
210
211    fn aux_width(&self) -> usize {
212        AUX_TRACE_WIDTH
213    }
214
215    fn num_aux_values(&self) -> usize {
216        AUX_TRACE_WIDTH
217    }
218
219    /// Returns the number of variable-length public input slices.
220    ///
221    /// The Miden VM AIR uses a single variable-length slice that contains all kernel
222    /// procedure digests as concatenated field elements (each digest is `WORD_SIZE`
223    /// elements). The verifier framework uses this count to validate that the correct
224    /// number of slices is provided.
225    fn num_var_len_public_inputs(&self) -> usize {
226        1
227    }
228
229    fn reduced_aux_values(
230        &self,
231        aux_values: &[EF],
232        challenges: &[EF],
233        public_values: &[Felt],
234        var_len_public_inputs: VarLenPublicInputs<'_, Felt>,
235    ) -> Result<ReducedAuxValues<EF>, ReductionError>
236    where
237        EF: ExtensionField<Felt>,
238    {
239        // Extract final aux column values.
240        let p1 = aux_values[trace::DECODER_AUX_TRACE_OFFSET];
241        let p2 = aux_values[trace::DECODER_AUX_TRACE_OFFSET + 1];
242        let p3 = aux_values[trace::DECODER_AUX_TRACE_OFFSET + 2];
243        let s_aux = aux_values[trace::STACK_AUX_TRACE_OFFSET];
244        let b_range = aux_values[trace::RANGE_CHECK_AUX_TRACE_OFFSET];
245        let b_hash_kernel = aux_values[trace::HASH_KERNEL_VTABLE_AUX_TRACE_OFFSET];
246        let b_chiplets = aux_values[trace::CHIPLETS_BUS_AUX_TRACE_OFFSET];
247        let v_wiring = aux_values[trace::ACE_CHIPLET_WIRING_BUS_OFFSET];
248
249        // Parse fixed-length public values (see `NUM_PUBLIC_VALUES` for layout).
250        if public_values.len() != NUM_PUBLIC_VALUES {
251            return Err(format!(
252                "expected {} public values, got {}",
253                NUM_PUBLIC_VALUES,
254                public_values.len()
255            )
256            .into());
257        }
258        let program_hash: Word = public_values[PV_PROGRAM_HASH..PV_PROGRAM_HASH + WORD_SIZE]
259            .try_into()
260            .map_err(|_| -> ReductionError { "invalid program hash slice".into() })?;
261        let pc_transcript_state: PrecompileTranscriptState = public_values
262            [PV_TRANSCRIPT_STATE..PV_TRANSCRIPT_STATE + WORD_SIZE]
263            .try_into()
264            .map_err(|_| -> ReductionError { "invalid transcript state slice".into() })?;
265
266        // Precompute challenge powers once for all bus message encodings.
267        let challenges = trace::Challenges::<EF>::new(challenges[0], challenges[1]);
268
269        // Compute expected bus messages from public inputs and derived challenges.
270        let ph_msg = program_hash_message(&challenges, &program_hash);
271
272        let (default_transcript_msg, final_transcript_msg) =
273            transcript_messages(&challenges, pc_transcript_state);
274
275        let kernel_reduced = kernel_reduced_from_var_len(&challenges, var_len_public_inputs)?;
276
277        // Combine all multiset column finals with reduced variable length public-inputs.
278        //
279        // Running-product columns accumulate `responses / requests` at each row, so
280        // their final value is product(responses) / product(requests) over the entire trace.
281        //
282        // Columns whose requests and responses fully cancel end at 1:
283        //   p1  (block stack table) -- every block pushed is later popped
284        //   p3  (op group table)    -- every op group pushed is later consumed
285        //   s_aux (stack overflow)  -- every overflow push has a matching pop
286        //
287        // Columns with public-input-dependent boundary terms end at non-unity values:
288        //
289        //   p2 (block hash table):
290        //     The root block's hash is removed from the table at END, but was never
291        //     added (the root has no parent that would add it). This leaves one
292        //     unmatched removal: p2_final = 1 / ph_msg.
293        //
294        //   b_hash_kernel (chiplets virtual table: sibling table + transcript state):
295        //     The log_precompile transcript tracking chain starts by removing
296        //     default_transcript_msg (initial capacity state) and ends by inserting
297        //     final_transcript_msg (final capacity state). On the other hand, sibling table
298        //     entries cancel out. Net: b_hk_final = final_transcript_msg / default_transcript_msg.
299        //
300        //   b_chiplets (chiplets bus):
301        //     Each unique kernel procedure produces a KernelRomInitMessage response
302        //     from the kernel ROM chiplet These init messages are matched by the verifier
303        //     via public inputs. Net: b_ch_final = product(kernel_init_msgs) = kernel_reduced.
304        //
305        // Multiplying all finals with correction terms:
306        //   prod = (p1 * p3 * s_aux)                               -- each is 1
307        //        * (p2 * ph_msg)                                   -- (1/ph_msg) * ph_msg = 1
308        //        * (b_hk * default_msg / final_msg)                -- cancels to 1
309        //        * (b_ch / kernel_reduced)                         -- cancels to 1
310        //
311        // Rearranged: prod = all_finals * ph_msg * default_msg / (final_msg * kernel_reduced) (= 1)
312        let expected_denom = final_transcript_msg * kernel_reduced;
313        let expected_denom_inv = expected_denom
314            .try_inverse()
315            .ok_or_else(|| -> ReductionError { "zero denominator in reduced_aux_values".into() })?;
316
317        let prod = p1
318            * p2
319            * p3
320            * s_aux
321            * b_hash_kernel
322            * b_chiplets
323            * ph_msg
324            * default_transcript_msg
325            * expected_denom_inv;
326
327        // LogUp: all columns should end at 0.
328        let sum = b_range + v_wiring;
329
330        Ok(ReducedAuxValues { prod, sum })
331    }
332
333    fn eval<AB: LiftedAirBuilder<F = Felt>>(&self, builder: &mut AB) {
334        use crate::constraints;
335
336        let main = builder.main();
337
338        // Access the two rows: current (local) and next
339        let local = main.current_slice();
340        let next = main.next_slice();
341
342        // Use structured column access via MainTraceCols
343        let local: &MainTraceRow<AB::Var> = (*local).borrow();
344        let next: &MainTraceRow<AB::Var> = (*next).borrow();
345
346        // Main trace constraints.
347        constraints::enforce_main(builder, local, next);
348
349        // Auxiliary (bus) constraints.
350        constraints::enforce_bus(builder, local, next);
351
352        // Public inputs boundary constraints.
353        constraints::public_inputs::enforce_main(builder, local);
354    }
355}
356
357// REDUCED AUX VALUES HELPERS
358// ================================================================================================
359
360/// Builds the program-hash bus message for the block-hash table boundary term.
361///
362/// Must match `BlockHashTableRow::from_end().collapse()` on the prover side for the
363/// root block, which encodes `[parent_id=0, hash[0..4], is_first_child=0, is_loop_body=0]`.
364fn program_hash_message<EF: ExtensionField<Felt>>(
365    challenges: &trace::Challenges<EF>,
366    program_hash: &Word,
367) -> EF {
368    challenges.encode([
369        Felt::ZERO, // parent_id = 0 (root block)
370        program_hash[0],
371        program_hash[1],
372        program_hash[2],
373        program_hash[3],
374        Felt::ZERO, // is_first_child = false
375        Felt::ZERO, // is_loop_body = false
376    ])
377}
378
379/// Returns the pair of (initial, final) log-precompile transcript messages for the
380/// virtual-table bus boundary term.
381///
382/// The initial message uses the default (zero) capacity state; the final message uses
383/// the public-input transcript state.
384fn transcript_messages<EF: ExtensionField<Felt>>(
385    challenges: &trace::Challenges<EF>,
386    final_state: PrecompileTranscriptState,
387) -> (EF, EF) {
388    let encode = |state: PrecompileTranscriptState| {
389        let cap: &[Felt] = state.as_ref();
390        challenges.encode([
391            Felt::from_u8(trace::LOG_PRECOMPILE_LABEL),
392            cap[0],
393            cap[1],
394            cap[2],
395            cap[3],
396        ])
397    };
398    (encode(PrecompileTranscriptState::default()), encode(final_state))
399}
400
401/// Builds the kernel procedure init message for the kernel ROM bus.
402///
403/// Must match `KernelRomInitMessage::value()` on the prover side, which encodes
404/// `[KERNEL_PROC_INIT_LABEL, digest[0..4]]`.
405fn kernel_proc_message<EF: ExtensionField<Felt>>(
406    challenges: &trace::Challenges<EF>,
407    digest: &Word,
408) -> EF {
409    challenges.encode([
410        trace::chiplets::kernel_rom::KERNEL_PROC_INIT_LABEL,
411        digest[0],
412        digest[1],
413        digest[2],
414        digest[3],
415    ])
416}
417
418/// Reduces kernel procedure digests from var-len public inputs into a multiset product.
419///
420/// Expects exactly one variable-length public input slice containing all kernel digests
421/// as concatenated `Felt`s (i.e. `len % WORD_SIZE == 0`).
422fn kernel_reduced_from_var_len<EF: ExtensionField<Felt>>(
423    challenges: &trace::Challenges<EF>,
424    var_len_public_inputs: VarLenPublicInputs<'_, Felt>,
425) -> Result<EF, ReductionError> {
426    if var_len_public_inputs.len() != 1 {
427        return Err(format!(
428            "expected 1 var-len public input slice, got {}",
429            var_len_public_inputs.len()
430        )
431        .into());
432    }
433    let kernel_felts = var_len_public_inputs[0];
434    if !kernel_felts.len().is_multiple_of(WORD_SIZE) {
435        return Err(format!(
436            "kernel digest felts length {} is not a multiple of {}",
437            kernel_felts.len(),
438            WORD_SIZE
439        )
440        .into());
441    }
442    let mut acc = EF::ONE;
443    for digest in kernel_felts.chunks_exact(WORD_SIZE) {
444        let word: Word = [digest[0], digest[1], digest[2], digest[3]].into();
445        acc *= kernel_proc_message(challenges, &word);
446    }
447    Ok(acc)
448}