Skip to main content

miden_processor/trace/
mod.rs

1use alloc::{format, sync::Arc, vec::Vec};
2#[cfg(any(test, feature = "testing"))]
3use core::ops::Range;
4
5use miden_air::{
6    MidenMultiAir, ProverStatement, PublicInputs, StarkConfig, Statement, config, debug,
7    trace::{MainTrace, decoder::NUM_USER_OP_HELPERS},
8};
9use miden_core::{
10    deferred::{DeferredState, DeferredStateWire, Digest, TRUE_DIGEST},
11    program::ExecutionClaim,
12    serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
13};
14
15use crate::{
16    Felt, MIN_STACK_DEPTH, ProgramInfo, StackInputs, StackOutputs, Word, ZERO,
17    fast::ExecutionOutput, field::QuadFelt, utils::RowMajorMatrix,
18};
19
20pub(crate) mod utils;
21use utils::ChipletTraceFragment;
22
23pub mod chiplets;
24pub(crate) mod execution_tracer;
25
26mod block_stack;
27mod parallel;
28mod range;
29mod stack;
30mod trace_state;
31
32#[cfg(test)]
33mod tests;
34
35// RE-EXPORTS
36// ================================================================================================
37
38pub(crate) use execution_tracer::TraceReplay;
39pub use miden_air::trace::RowIndex;
40pub use miden_core::deferred::PrecompileWitness;
41pub use parallel::{
42    CORE_TRACE_WIDTH, DEFAULT_MAX_PROVER_MEMORY_BYTES, build_trace, build_trace_with_budget,
43};
44// Re-exported for the streaming trace-build path
45// (`FastProcessor::execute_and_build_trace_sync`), which is std-only; the buffered path
46// uses `build_hasher_chiplet` and `MAX_TRACE_LEN` directly within `parallel`.
47#[cfg(feature = "std")]
48pub(crate) use parallel::{MAX_TRACE_LEN, build_hasher_chiplet, build_trace_with_prebuilt_hasher};
49#[cfg(feature = "std")]
50pub(crate) use trace_state::ResolvedHasherOp;
51pub use utils::{ChipletsLengths, TraceLenSummary};
52
53/// Complete in-memory witness produced by a traced program execution.
54///
55/// The processor constructs its VM witness and optional singleton precompile witness from the same
56/// execution output, so they retain the same deferred root. The aggregate may contain private and
57/// potentially large prover data. Its binary form is trusted replay data: sparse MAST node and
58/// digest maps inside the trace replay are not checked against a source `MastForest` commitment;
59/// see <https://github.com/0xMiden/miden-vm/issues/3303>.
60#[derive(Debug)]
61pub struct ExecutionWitness {
62    vm: VmWitness,
63    precompile: Option<PrecompileWitness>,
64}
65
66impl ExecutionWitness {
67    pub(crate) fn from_execution(
68        program_info: ProgramInfo,
69        stack_inputs: StackInputs,
70        execution_output: ExecutionOutput,
71        trace: TraceReplay,
72    ) -> Self {
73        let ExecutionOutput {
74            stack: stack_outputs,
75            advice: _,
76            memory: _,
77            deferred_state: precompiles,
78        } = execution_output;
79        let precompile_root = precompiles.root();
80        let vm = VmWitness {
81            program_info,
82            stack_inputs,
83            stack_outputs,
84            trace,
85            precompile_root,
86        };
87        let precompile = (precompile_root != TRUE_DIGEST).then(|| {
88            PrecompileWitness::new(precompiles)
89                .expect("a non-TRUE execution root must produce a singleton precompile witness")
90        });
91
92        Self { vm, precompile }
93    }
94
95    /// Returns the public claim associated with this witness.
96    pub fn claim(&self) -> ExecutionClaim {
97        self.vm.claim()
98    }
99
100    /// Consumes this witness and returns its supported low-level proving components.
101    ///
102    /// The [`VmWitness`] can be passed to [`build_trace`]. The optional [`PrecompileWitness`] is
103    /// present only when execution authenticated deferred precompile work.
104    pub fn into_parts(self) -> (VmWitness, Option<PrecompileWitness>) {
105        (self.vm, self.precompile)
106    }
107}
108
109/// Current wire format version for [`ExecutionWitness`] serialization.
110///
111/// The version is written as the first byte of every serialized witness. Deserialization only
112/// accepts this exact value, so a future format change only needs to add a new accepted version
113/// and keep the old readers where compatibility matters.
114const EXECUTION_WITNESS_WIRE_VERSION: u8 = 1;
115
116impl Serializable for ExecutionWitness {
117    fn write_into<W: ByteWriter>(&self, target: &mut W) {
118        EXECUTION_WITNESS_WIRE_VERSION.write_into(target);
119        self.vm.write_into(target);
120        match &self.precompile {
121            Some(precompile) => {
122                target.write_u8(1);
123                write_precompile_witness(precompile, target);
124            },
125            None => target.write_u8(0),
126        }
127    }
128}
129
130impl Deserializable for ExecutionWitness {
131    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
132        let version = u8::read_from(source)?;
133        if version != EXECUTION_WITNESS_WIRE_VERSION {
134            return Err(DeserializationError::InvalidValue(format!(
135                "unsupported execution witness wire version {version} (expected \
136                 {EXECUTION_WITNESS_WIRE_VERSION})"
137            )));
138        }
139        let vm = VmWitness::read_from(source)?;
140        let precompile = match source.read_u8()? {
141            0 => {
142                if vm.precompile_root != TRUE_DIGEST {
143                    return Err(DeserializationError::InvalidValue(
144                        "VM witness claims deferred work but no precompile witness is present"
145                            .into(),
146                    ));
147                }
148                None
149            },
150            1 => {
151                let witness = read_precompile_witness(source)?;
152                // `read_precompile_witness` only produces singleton witnesses, but do not index
153                // blindly: keep deserialization panic-free even if that invariant changes.
154                let [witness_root] = witness.roots() else {
155                    return Err(DeserializationError::InvalidValue(
156                        "expected a singleton precompile witness".into(),
157                    ));
158                };
159                if *witness_root != vm.precompile_root {
160                    return Err(DeserializationError::InvalidValue(
161                        "precompile witness root does not match the VM witness precompile root"
162                            .into(),
163                    ));
164                }
165                Some(witness)
166            },
167            tag => {
168                return Err(DeserializationError::InvalidValue(format!(
169                    "invalid precompile witness option tag {tag}"
170                )));
171            },
172        };
173        Ok(Self { vm, precompile })
174    }
175}
176
177/// Witness required to materialize and prove a VM execution trace.
178///
179/// This potentially large value contains private replay data and is consumed by trace-building and
180/// proving operations. Its binary form is trusted replay data: sparse MAST node and digest maps
181/// inside the trace replay are not checked against a source `MastForest` commitment; see
182/// <https://github.com/0xMiden/miden-vm/issues/3303>.
183#[derive(Debug)]
184pub struct VmWitness {
185    program_info: ProgramInfo,
186    stack_inputs: StackInputs,
187    stack_outputs: StackOutputs,
188    trace: TraceReplay,
189    precompile_root: Digest,
190}
191
192impl VmWitness {
193    /// Returns the public claim associated with this witness.
194    pub fn claim(&self) -> ExecutionClaim {
195        ExecutionClaim::from_program_info(
196            self.program_info.clone(),
197            self.stack_inputs,
198            self.stack_outputs,
199        )
200    }
201
202    /// Takes the hasher replay out, leaving an empty buffered one.
203    ///
204    /// The streaming path uses this to drop the replay's channel sender once execution has
205    /// finished, so the concurrently running hasher builder sees its input stream end.
206    #[cfg(feature = "std")]
207    pub(crate) fn take_hasher_replay(&mut self) -> trace_state::HasherRequestReplay {
208        core::mem::take(&mut self.trace.hasher_for_chiplet)
209    }
210
211    /// Returns the replay data captured during execution.
212    #[cfg(any(test, feature = "testing"))]
213    #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))]
214    pub(crate) fn trace_replay(&self) -> &TraceReplay {
215        &self.trace
216    }
217
218    // Kept for tests that force invalid replay data without widening the public API.
219    #[cfg(any(test, feature = "testing"))]
220    #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))]
221    pub(crate) fn trace_replay_mut(&mut self) -> &mut TraceReplay {
222        &mut self.trace
223    }
224
225    /// Returns the number of sparse MAST forests captured in the replay.
226    ///
227    /// Available under the `testing` feature for external tests that check which replay data a
228    /// serialized witness preserves.
229    #[cfg(any(test, feature = "testing"))]
230    #[allow(dead_code)]
231    pub fn mast_forest_count(&self) -> usize {
232        self.trace.mast_forest_store.len()
233    }
234}
235
236impl Serializable for VmWitness {
237    fn write_into<W: ByteWriter>(&self, target: &mut W) {
238        self.program_info.write_into(target);
239        self.stack_inputs.write_into(target);
240        self.stack_outputs.write_into(target);
241        self.trace.write_into(target);
242        self.precompile_root.write_into(target);
243    }
244}
245
246impl Deserializable for VmWitness {
247    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
248        Ok(Self {
249            program_info: ProgramInfo::read_from(source)?,
250            stack_inputs: StackInputs::read_from(source)?,
251            stack_outputs: StackOutputs::read_from(source)?,
252            trace: TraceReplay::read_from(source)?,
253            precompile_root: Digest::read_from(source)?,
254        })
255    }
256}
257
258/// Writes a singleton precompile witness as its ordered roots followed by its canonical deferred
259/// wire.
260fn write_precompile_witness<W: ByteWriter>(witness: &PrecompileWitness, target: &mut W) {
261    let roots = witness.roots();
262    debug_assert_eq!(roots.len(), 1, "only singleton precompile witnesses are serializable");
263    target.write_usize(roots.len());
264    for root in roots {
265        root.write_into(target);
266    }
267    let deferred_wire = witness
268        .state()
269        .to_wire()
270        .expect("deferred state must serialize to canonical wire");
271    deferred_wire.write_into(target);
272}
273
274/// Reads a singleton precompile witness written by [`write_precompile_witness`].
275fn read_precompile_witness<R: ByteReader>(
276    source: &mut R,
277) -> Result<PrecompileWitness, DeserializationError> {
278    let roots = Vec::<Digest>::read_from(source)?;
279    if roots.len() != 1 {
280        return Err(DeserializationError::InvalidValue(
281            "expected a singleton precompile witness".into(),
282        ));
283    }
284    let deferred_wire = DeferredStateWire::read_from(source)?;
285    let deferred_state =
286        DeferredState::from_wire(Arc::new(miden_precompiles::registry()), &deferred_wire).map_err(
287            |err| DeserializationError::InvalidValue(format!("invalid deferred state: {err}")),
288        )?;
289
290    let witness = PrecompileWitness::new(deferred_state).map_err(|err| {
291        DeserializationError::InvalidValue(format!("invalid precompile witness: {err}"))
292    })?;
293    if witness.roots() != roots.as_slice() {
294        return Err(DeserializationError::InvalidValue(
295            "precompile witness roots do not match its deferred state".into(),
296        ));
297    }
298    Ok(witness)
299}
300
301// VM EXECUTION TRACE
302// ================================================================================================
303
304/// Execution trace which is generated when a program is executed on the VM.
305///
306/// The trace consists of the following components:
307/// - Per-AIR trace matrices for Core, Chiplets, and Poseidon2Permutation.
308/// - Information about the program (program hash and the kernel).
309/// - Information about the initial and final stack states and authenticated precompile root.
310/// - Summary of trace lengths of the main trace components.
311#[derive(Debug)]
312pub struct VmTrace {
313    main_trace: MainTrace,
314    program_info: ProgramInfo,
315    stack_inputs: StackInputs,
316    stack_outputs: StackOutputs,
317    precompile_root: Digest,
318    trace_len_summary: TraceLenSummary,
319}
320
321impl VmTrace {
322    // CONSTRUCTOR
323    // --------------------------------------------------------------------------------------------
324
325    pub(crate) fn new_from_parts(
326        program_info: ProgramInfo,
327        stack_inputs: StackInputs,
328        stack_outputs: StackOutputs,
329        precompile_root: Digest,
330        main_trace: MainTrace,
331        trace_len_summary: TraceLenSummary,
332    ) -> Self {
333        Self {
334            main_trace,
335            program_info,
336            stack_inputs,
337            stack_outputs,
338            precompile_root,
339            trace_len_summary,
340        }
341    }
342
343    // PUBLIC ACCESSORS
344    // --------------------------------------------------------------------------------------------
345
346    /// Returns the program info of this execution trace.
347    pub fn program_info(&self) -> &ProgramInfo {
348        &self.program_info
349    }
350
351    /// Returns hash of the program execution of which resulted in this execution trace.
352    pub fn program_hash(&self) -> &Word {
353        self.program_info.program_hash()
354    }
355
356    /// Returns outputs of the program execution which resulted in this execution trace.
357    pub fn stack_outputs(&self) -> &StackOutputs {
358        &self.stack_outputs
359    }
360
361    /// Returns the public inputs for this execution trace.
362    pub fn public_inputs(&self) -> PublicInputs {
363        PublicInputs::new(
364            self.program_info.clone(),
365            self.stack_inputs,
366            self.stack_outputs,
367            self.precompile_root,
368        )
369    }
370
371    /// Returns the public values for this execution trace.
372    pub fn to_public_values(&self) -> Vec<Felt> {
373        self.public_inputs().to_elements()
374    }
375
376    /// Returns a reference to the main trace.
377    pub fn main_trace(&self) -> &MainTrace {
378        &self.main_trace
379    }
380
381    /// Returns a mutable reference to the main trace.
382    pub fn main_trace_mut(&mut self) -> &mut MainTrace {
383        &mut self.main_trace
384    }
385
386    /// Returns the authenticated root of the deferred precompile state.
387    pub fn precompile_root(&self) -> Digest {
388        self.precompile_root
389    }
390
391    /// Returns the owned stack outputs required for proof packaging.
392    pub fn into_outputs(self) -> StackOutputs {
393        self.stack_outputs
394    }
395
396    /// Returns the initial state of the top 16 stack registers.
397    pub fn init_stack_state(&self) -> StackInputs {
398        self.stack_inputs
399    }
400
401    /// Returns the final state of the top 16 stack registers.
402    pub fn last_stack_state(&self) -> StackOutputs {
403        let last_step = RowIndex::from(self.last_step());
404        let mut result = [ZERO; MIN_STACK_DEPTH];
405        for (i, result) in result.iter_mut().enumerate() {
406            *result = self.main_trace.stack_element(i, last_step);
407        }
408        result.into()
409    }
410
411    /// Returns helper registers state at the specified `clk` of the VM
412    pub fn get_user_op_helpers_at(&self, clk: u32) -> [Felt; NUM_USER_OP_HELPERS] {
413        let mut result = [ZERO; NUM_USER_OP_HELPERS];
414        let row = RowIndex::from(clk);
415        for (i, result) in result.iter_mut().enumerate() {
416            *result = self.main_trace.helper_register(i, row);
417        }
418        result
419    }
420
421    /// Returns the trace length.
422    pub fn get_trace_len(&self) -> usize {
423        self.main_trace.num_rows()
424    }
425
426    /// Returns the length of the trace (number of rows in the main trace).
427    pub fn length(&self) -> usize {
428        self.get_trace_len()
429    }
430
431    /// Returns a summary of the per-component trace lengths.
432    pub fn trace_len_summary(&self) -> &TraceLenSummary {
433        &self.trace_len_summary
434    }
435
436    // DEBUG CONSTRAINT CHECKING
437    // --------------------------------------------------------------------------------------------
438
439    /// Validates this execution trace against all AIR constraints without generating a STARK
440    /// proof.
441    ///
442    /// This is the recommended way to test trace correctness. It is much faster than full STARK
443    /// proving and provides better error diagnostics (panics on the first constraint violation
444    /// with the instance and row index).
445    ///
446    /// # Panics
447    ///
448    /// Panics if any AIR constraint evaluates to nonzero.
449    pub fn check_constraints(&self) {
450        let public_inputs = self.public_inputs();
451        let (core_matrix, chiplets_matrix, poseidon2_matrix) = self.main_trace.to_air_matrices();
452
453        let (public_values, aux_inputs) = public_inputs.to_air_inputs();
454
455        let statement =
456            Statement::<Felt, QuadFelt, _>::new(MidenMultiAir::new(), public_values, aux_inputs)
457                .expect("valid statement inputs");
458        let prover_statement =
459            ProverStatement::new(statement, vec![core_matrix, chiplets_matrix, poseidon2_matrix])
460                .expect("valid trace shapes");
461
462        // A deterministic challenger seeds the debug constraint check; this is a local
463        // constraint debugger, not a full proof transcript, so any fixed challenge set works.
464        let config = config::poseidon2_config(config::pcs_params(), config::RELATION_DIGEST);
465        debug::check_constraints(&prover_statement, config.challenger());
466    }
467
468    /// Splits the trace into the per-AIR matrices consumed by the multi-AIR proving path.
469    pub fn to_air_matrices(
470        &self,
471    ) -> (RowMajorMatrix<Felt>, RowMajorMatrix<Felt>, RowMajorMatrix<Felt>) {
472        self.main_trace.to_air_matrices()
473    }
474
475    /// Consuming variant for the proving hot path.
476    pub fn into_air_matrices(
477        self,
478    ) -> (RowMajorMatrix<Felt>, RowMajorMatrix<Felt>, RowMajorMatrix<Felt>) {
479        self.main_trace.into_air_matrices()
480    }
481
482    // HELPER METHODS
483    // --------------------------------------------------------------------------------------------
484
485    /// Returns the index of the last row in the Core trace.
486    fn last_step(&self) -> usize {
487        self.main_trace.core_height() - 1
488    }
489
490    #[cfg(any(test, feature = "testing"))]
491    pub fn get_column_range(&self, range: Range<usize>) -> Vec<Vec<Felt>> {
492        self.main_trace.get_column_range(range)
493    }
494}
495
496#[cfg(test)]
497mod wire_tests {
498    use miden_assembly::Assembler;
499    use miden_core::deferred::TRUE_DIGEST;
500
501    use super::{Deserializable, ExecutionWitness, Serializable};
502    use crate::{DefaultHost, FastProcessor, StackInputs};
503
504    fn deferred_witness_bytes() -> alloc::vec::Vec<u8> {
505        let program = Assembler::default()
506            .assemble_program("program", "begin log_deferred end")
507            .expect("program should compile")
508            .unwrap_program();
509        let mut host = DefaultHost::default();
510        let witness = FastProcessor::new(StackInputs::default())
511            .execute_for_proving_sync(&program, &mut host)
512            .expect("execution should produce a witness");
513        witness.to_bytes()
514    }
515
516    #[test]
517    fn witness_wire_rejects_unsupported_version() {
518        let mut bytes = deferred_witness_bytes();
519        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
520
521        // The first byte of the wire is the format version; any other value must be rejected
522        // before any payload is parsed.
523        bytes[0] = bytes[0].wrapping_add(1);
524        let err = ExecutionWitness::read_from_bytes(&bytes)
525            .expect_err("witness with an unknown wire version should be rejected");
526        assert!(
527            format!("{err:?}").contains("unsupported execution witness wire version"),
528            "unexpected error: {err:?}"
529        );
530    }
531
532    #[test]
533    fn witness_wire_rejects_mismatched_precompile_root() {
534        let bytes = deferred_witness_bytes();
535        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
536        let (vm, precompile) = restored.into_parts();
537        let precompile = precompile.expect("deferred execution should carry a precompile witness");
538        assert_ne!(vm.precompile_root, TRUE_DIGEST);
539
540        // Tamper only the VM-side precompile root and re-serialize: the two halves of the wire
541        // no longer describe the same execution, so deserialization must reject them.
542        let tampered = ExecutionWitness {
543            vm: super::VmWitness { precompile_root: TRUE_DIGEST, ..vm },
544            precompile: Some(precompile),
545        };
546        let err = ExecutionWitness::read_from_bytes(&tampered.to_bytes())
547            .expect_err("tampered witness should be rejected");
548        assert!(
549            format!("{err:?}")
550                .contains("precompile witness root does not match the VM witness precompile root"),
551            "unexpected error: {err:?}"
552        );
553
554        // Sanity: the untampered wire still round-trips.
555        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
556    }
557
558    #[test]
559    fn witness_wire_rejects_missing_precompile_witness() {
560        let bytes = deferred_witness_bytes();
561        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
562        let (vm, precompile) = restored.into_parts();
563        assert!(precompile.is_some(), "deferred execution should carry a precompile witness");
564
565        // Drop only the precompile witness while the VM side still claims deferred work: the
566        // wire must not validate as a complete execution.
567        let stripped = ExecutionWitness { vm, precompile: None };
568        let err = ExecutionWitness::read_from_bytes(&stripped.to_bytes())
569            .expect_err("witness without its precompile half should be rejected");
570        assert!(
571            format!("{err:?}")
572                .contains("VM witness claims deferred work but no precompile witness is present"),
573            "unexpected error: {err:?}"
574        );
575    }
576}