Skip to main content

miden_processor/trace/
mod.rs

1use alloc::{format, 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::{Digest, TRUE_DIGEST},
11    program::ExecutionClaim,
12    serde::{
13        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
14        SliceReader,
15    },
16};
17
18use crate::{
19    Felt, MIN_STACK_DEPTH, ProgramInfo, StackInputs, StackOutputs, Word, ZERO,
20    fast::ExecutionOutput, field::QuadFelt, utils::RowMajorMatrix,
21};
22
23pub(crate) mod utils;
24use utils::ChipletTraceFragment;
25
26pub mod chiplets;
27pub(crate) mod execution_tracer;
28
29mod block_stack;
30mod parallel;
31mod range;
32mod stack;
33mod trace_state;
34
35#[cfg(test)]
36mod tests;
37
38// RE-EXPORTS
39// ================================================================================================
40
41pub(crate) use execution_tracer::TraceReplay;
42pub use miden_air::trace::RowIndex;
43pub use miden_core::deferred::PrecompileWitness;
44pub use parallel::{
45    CORE_TRACE_WIDTH, DEFAULT_MAX_PROVER_MEMORY_BYTES, build_trace, build_trace_with_budget,
46};
47// Re-exported for the streaming trace-build path
48// (`FastProcessor::execute_and_build_trace_sync`), which is std-only; the buffered path
49// uses `build_hasher_chiplet` and `MAX_TRACE_LEN` directly within `parallel`.
50#[cfg(feature = "std")]
51pub(crate) use parallel::{MAX_TRACE_LEN, build_hasher_chiplet, build_trace_with_prebuilt_hasher};
52#[cfg(feature = "std")]
53pub(crate) use trace_state::ResolvedHasherOp;
54pub use utils::{ChipletsLengths, TraceLenSummary};
55
56/// Complete in-memory witness produced by a traced program execution.
57///
58/// The processor constructs its VM witness and optional singleton precompile witness from the same
59/// execution output, so they retain the same deferred root. The aggregate may contain private and
60/// potentially large prover data. Its binary form is trusted replay data: sparse MAST node and
61/// digest maps inside the trace replay are not checked against a source `MastForest` commitment;
62/// see <https://github.com/0xMiden/miden-vm/issues/3303>.
63#[derive(Debug)]
64pub struct ExecutionWitness {
65    vm: VmWitness,
66    precompile: Option<PrecompileWitness>,
67}
68
69impl ExecutionWitness {
70    pub(crate) fn from_execution(
71        program_info: ProgramInfo,
72        stack_inputs: StackInputs,
73        execution_output: ExecutionOutput,
74        trace: TraceReplay,
75    ) -> Self {
76        let ExecutionOutput {
77            stack: stack_outputs,
78            advice: _,
79            memory: _,
80            precompile_witness: precompile,
81        } = execution_output;
82        let precompile_root =
83            precompile.as_ref().map_or(TRUE_DIGEST, PrecompileWitness::root_unchecked);
84        let vm = VmWitness {
85            program_info,
86            stack_inputs,
87            stack_outputs,
88            trace,
89            precompile_root,
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    /// Returns whether this witness contains precompile work.
101    pub const fn has_precompiles(&self) -> bool {
102        self.precompile.is_some()
103    }
104
105    /// Consumes this witness and returns its supported low-level proving components.
106    ///
107    /// The [`VmWitness`] can be passed to [`build_trace`]. The optional [`PrecompileWitness`] is
108    /// present only when execution authenticated deferred precompile work.
109    pub fn into_parts(self) -> (VmWitness, Option<PrecompileWitness>) {
110        (self.vm, self.precompile)
111    }
112
113    /// Decodes one execution witness from a potentially adversarial byte slice.
114    ///
115    /// The reader applies an input-proportional limit to byte consumption and collection
116    /// preallocation, then rejects trailing bytes. These checks establish safe transport syntax.
117    /// They do not prove that the sparse MAST replay is a subset of a particular source forest
118    /// because the witness wire does not carry such a proof.
119    ///
120    /// Use [`Self::read_from_bytes_trusted`] for bytes retained inside a trusted prover system.
121    #[track_caller]
122    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
123        let budget = bytes.len().saturating_mul(EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER);
124        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
125        let witness = <Self as Deserializable>::read_from(&mut reader)?;
126
127        if reader.has_more_bytes() {
128            return Err(DeserializationError::InvalidValue(
129                "extra bytes after execution witness payload".into(),
130            ));
131        }
132        Ok(witness)
133    }
134
135    /// Decodes execution witness bytes retained inside a trusted prover system.
136    ///
137    /// This preserves the original permissive byte-slice behavior. It trusts sparse MAST replay
138    /// hashes, accepts trailing bytes, and uses a replay-sized byte budget. Use
139    /// [`Self::read_from_bytes`] for bytes received across a trust boundary.
140    #[track_caller]
141    pub fn read_from_bytes_trusted(bytes: &[u8]) -> Result<Self, DeserializationError> {
142        let budget = bytes.len().saturating_mul(EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER);
143        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
144        <Self as Deserializable>::read_from(&mut reader)
145    }
146}
147
148/// Covers nested replay length checks while keeping untrusted work proportional to input size.
149const EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER: usize = 4;
150
151/// Current wire format version for [`ExecutionWitness`] serialization.
152///
153/// The version is written as the first byte of every serialized witness. Deserialization only
154/// accepts this exact value; older formats are intentionally unsupported.
155const EXECUTION_WITNESS_WIRE_VERSION: u8 = 2;
156
157impl Serializable for ExecutionWitness {
158    fn write_into<W: ByteWriter>(&self, target: &mut W) {
159        EXECUTION_WITNESS_WIRE_VERSION.write_into(target);
160        self.vm.write_into(target);
161        match &self.precompile {
162            Some(precompile) => {
163                target.write_u8(1);
164                precompile.write_into(target);
165            },
166            None => target.write_u8(0),
167        }
168    }
169}
170
171impl Deserializable for ExecutionWitness {
172    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
173        let version = u8::read_from(source)?;
174        if version != EXECUTION_WITNESS_WIRE_VERSION {
175            return Err(DeserializationError::InvalidValue(format!(
176                "unsupported execution witness wire version {version} (expected \
177                 {EXECUTION_WITNESS_WIRE_VERSION})"
178            )));
179        }
180        let vm = VmWitness::read_from(source)?;
181        let precompile = match source.read_u8()? {
182            0 => {
183                if vm.precompile_root != TRUE_DIGEST {
184                    return Err(DeserializationError::InvalidValue(
185                        "VM witness claims deferred work but no precompile witness is present"
186                            .into(),
187                    ));
188                }
189                None
190            },
191            1 => {
192                let witness = PrecompileWitness::read_from(source)?;
193                if witness.root_unchecked() != vm.precompile_root {
194                    return Err(DeserializationError::InvalidValue(
195                        "precompile witness root does not match the VM witness precompile root"
196                            .into(),
197                    ));
198                }
199                Some(witness)
200            },
201            tag => {
202                return Err(DeserializationError::InvalidValue(format!(
203                    "invalid precompile witness option tag {tag}"
204                )));
205            },
206        };
207        Ok(Self { vm, precompile })
208    }
209
210    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
211        ExecutionWitness::read_from_bytes(bytes)
212    }
213}
214
215/// Witness required to materialize and prove a VM execution trace.
216///
217/// This potentially large value contains private replay data and is consumed by trace-building and
218/// proving operations. Its binary form is trusted replay data: sparse MAST node and digest maps
219/// inside the trace replay are not checked against a source `MastForest` commitment; see
220/// <https://github.com/0xMiden/miden-vm/issues/3303>.
221#[derive(Debug)]
222pub struct VmWitness {
223    program_info: ProgramInfo,
224    stack_inputs: StackInputs,
225    stack_outputs: StackOutputs,
226    trace: TraceReplay,
227    precompile_root: Digest,
228}
229
230impl VmWitness {
231    /// Returns the public claim associated with this witness.
232    pub fn claim(&self) -> ExecutionClaim {
233        ExecutionClaim::from_program_info(
234            self.program_info.clone(),
235            self.stack_inputs,
236            self.stack_outputs,
237        )
238    }
239
240    /// Returns whether this witness authenticates deferred precompile work.
241    pub fn has_precompiles(&self) -> bool {
242        self.precompile_root != TRUE_DIGEST
243    }
244
245    /// Takes the hasher replay out, leaving an empty buffered one.
246    ///
247    /// The streaming path uses this to drop the replay's channel sender once execution has
248    /// finished, so the concurrently running hasher builder sees its input stream end.
249    #[cfg(feature = "std")]
250    pub(crate) fn take_hasher_replay(&mut self) -> trace_state::HasherRequestReplay {
251        core::mem::take(&mut self.trace.hasher_for_chiplet)
252    }
253
254    /// Returns the replay data captured during execution.
255    #[cfg(any(test, feature = "testing"))]
256    #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))]
257    pub(crate) fn trace_replay(&self) -> &TraceReplay {
258        &self.trace
259    }
260
261    // Kept for tests that force invalid replay data without widening the public API.
262    #[cfg(any(test, feature = "testing"))]
263    #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))]
264    pub(crate) fn trace_replay_mut(&mut self) -> &mut TraceReplay {
265        &mut self.trace
266    }
267
268    /// Returns the number of sparse MAST forests captured in the replay.
269    ///
270    /// Available under the `testing` feature for external tests that check which replay data a
271    /// serialized witness preserves.
272    #[cfg(any(test, feature = "testing"))]
273    #[allow(dead_code)]
274    pub fn mast_forest_count(&self) -> usize {
275        self.trace.mast_forest_store.len()
276    }
277}
278
279impl Serializable for VmWitness {
280    fn write_into<W: ByteWriter>(&self, target: &mut W) {
281        self.program_info.write_into(target);
282        self.stack_inputs.write_into(target);
283        self.stack_outputs.write_into(target);
284        self.trace.write_into(target);
285        self.precompile_root.write_into(target);
286    }
287}
288
289impl Deserializable for VmWitness {
290    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
291        Ok(Self {
292            program_info: ProgramInfo::read_from(source)?,
293            stack_inputs: StackInputs::read_from(source)?,
294            stack_outputs: StackOutputs::read_from(source)?,
295            trace: TraceReplay::read_from(source)?,
296            precompile_root: Digest::read_from(source)?,
297        })
298    }
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::{ExecutionWitness, Serializable};
502    use crate::{DefaultHost, FastProcessor, StackInputs, mast::MastNodeId};
503
504    fn execution_witness(source: &str) -> ExecutionWitness {
505        let program = Assembler::default()
506            .assemble_program("program", source)
507            .expect("program should compile")
508            .unwrap_program();
509        let mut host = DefaultHost::default();
510        FastProcessor::new(StackInputs::default())
511            .execute_for_proving_sync(&program, &mut host)
512            .expect("execution should produce a witness")
513    }
514
515    fn deferred_witness() -> ExecutionWitness {
516        execution_witness("begin log_deferred end")
517    }
518
519    fn deferred_witness_bytes() -> alloc::vec::Vec<u8> {
520        deferred_witness().to_bytes()
521    }
522
523    #[test]
524    fn witness_reports_precompile_state() {
525        let plain = execution_witness("begin push.1 drop end");
526        assert!(!plain.has_precompiles());
527
528        let deferred = execution_witness("begin log_deferred end");
529        assert!(deferred.has_precompiles());
530    }
531
532    #[test]
533    fn witness_wire_rejects_unsupported_version() {
534        let mut bytes = deferred_witness_bytes();
535        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
536
537        // Rejected versions are checked before decoding replay or witness payloads.
538        for version in [0, 1, super::EXECUTION_WITNESS_WIRE_VERSION + 1] {
539            bytes[0] = version;
540            let err = ExecutionWitness::read_from_bytes(&bytes)
541                .expect_err("unsupported witness format must be rejected");
542            assert!(format!("{err:?}").contains("unsupported execution witness wire version"));
543        }
544    }
545
546    #[test]
547    fn witness_wire_rejects_trailing_bytes() {
548        let mut bytes = deferred_witness_bytes();
549        bytes.push(0);
550
551        let err = ExecutionWitness::read_from_bytes(&bytes)
552            .expect_err("witness payload with trailing bytes should be rejected");
553        assert!(
554            format!("{err:?}").contains("extra bytes after execution witness payload"),
555            "unexpected error: {err:?}"
556        );
557
558        assert!(
559            ExecutionWitness::read_from_bytes_trusted(&bytes).is_ok(),
560            "the explicit trusted reader should preserve the old permissive behavior"
561        );
562    }
563
564    #[test]
565    fn witness_wire_accepts_large_minimally_encoded_continuation_stack() {
566        let mut witness = deferred_witness();
567        let continuation = &mut witness
568            .vm
569            .trace_replay_mut()
570            .core_trace_contexts
571            .first_mut()
572            .expect("witness should contain a trace fragment")
573            .continuation;
574        for _ in 0..4096 {
575            continuation.push_start_node(MastNodeId::from(0));
576        }
577
578        let bytes = witness.to_bytes();
579        let restored = ExecutionWitness::read_from_bytes(&bytes)
580            .expect("valid witness should fit its input-proportional allocation budget");
581
582        assert_eq!(restored.to_bytes(), bytes);
583    }
584
585    #[test]
586    fn witness_wire_rejects_mismatched_precompile_root() {
587        let bytes = deferred_witness_bytes();
588        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
589        let (vm, precompile) = restored.into_parts();
590        let precompile = precompile.expect("deferred execution should carry a precompile witness");
591        assert_ne!(vm.precompile_root, TRUE_DIGEST);
592
593        // Tamper only the VM-side precompile root and re-serialize: the two halves of the wire
594        // no longer describe the same execution, so deserialization must reject them.
595        let tampered = ExecutionWitness {
596            vm: super::VmWitness { precompile_root: TRUE_DIGEST, ..vm },
597            precompile: Some(precompile),
598        };
599        let err = ExecutionWitness::read_from_bytes(&tampered.to_bytes())
600            .expect_err("tampered witness should be rejected");
601        assert!(
602            format!("{err:?}")
603                .contains("precompile witness root does not match the VM witness precompile root"),
604            "unexpected error: {err:?}"
605        );
606
607        // Sanity: the untampered wire still round-trips.
608        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
609    }
610
611    #[test]
612    fn witness_wire_rejects_missing_precompile_witness() {
613        let bytes = deferred_witness_bytes();
614        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
615        let (vm, precompile) = restored.into_parts();
616        assert!(precompile.is_some(), "deferred execution should carry a precompile witness");
617
618        // Drop only the precompile witness while the VM side still claims deferred work: the
619        // wire must not validate as a complete execution.
620        let stripped = ExecutionWitness { vm, precompile: None };
621        let err = ExecutionWitness::read_from_bytes(&stripped.to_bytes())
622            .expect_err("witness without its precompile half should be rejected");
623        assert!(
624            format!("{err:?}")
625                .contains("VM witness claims deferred work but no precompile witness is present"),
626            "unexpected error: {err:?}"
627        );
628    }
629}