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_root,
81            precompile_witness: precompile,
82        } = execution_output;
83        let vm = VmWitness {
84            program_info,
85            stack_inputs,
86            stack_outputs,
87            trace,
88            precompile_root,
89        };
90
91        Self { vm, precompile }
92    }
93
94    /// Returns the public claim associated with this witness.
95    pub fn claim(&self) -> ExecutionClaim {
96        self.vm.claim()
97    }
98
99    /// Returns whether this witness contains precompile work.
100    pub const fn has_precompiles(&self) -> bool {
101        self.precompile.is_some()
102    }
103
104    /// Consumes this witness and returns its supported low-level proving components.
105    ///
106    /// The [`VmWitness`] can be passed to [`build_trace`]. The optional [`PrecompileWitness`] is
107    /// present only when execution authenticated deferred precompile work.
108    pub fn into_parts(self) -> (VmWitness, Option<PrecompileWitness>) {
109        (self.vm, self.precompile)
110    }
111
112    /// Decodes one execution witness from a potentially adversarial byte slice.
113    ///
114    /// The reader applies an input-proportional limit to byte consumption and collection
115    /// preallocation, then rejects trailing bytes. These checks establish safe transport syntax.
116    /// They do not prove that the sparse MAST replay is a subset of a particular source forest
117    /// because the witness wire does not carry such a proof.
118    ///
119    /// Use [`Self::read_from_bytes_trusted`] for bytes retained inside a trusted prover system.
120    #[track_caller]
121    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
122        let budget = bytes.len().saturating_mul(EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER);
123        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
124        let witness = <Self as Deserializable>::read_from(&mut reader)?;
125
126        if reader.has_more_bytes() {
127            return Err(DeserializationError::InvalidValue(
128                "extra bytes after execution witness payload".into(),
129            ));
130        }
131        Ok(witness)
132    }
133
134    /// Decodes execution witness bytes retained inside a trusted prover system.
135    ///
136    /// This preserves the original permissive byte-slice behavior. It trusts sparse MAST replay
137    /// hashes, accepts trailing bytes, and uses a replay-sized byte budget. Use
138    /// [`Self::read_from_bytes`] for bytes received across a trust boundary.
139    #[track_caller]
140    pub fn read_from_bytes_trusted(bytes: &[u8]) -> Result<Self, DeserializationError> {
141        let budget = bytes.len().saturating_mul(EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER);
142        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
143        <Self as Deserializable>::read_from(&mut reader)
144    }
145}
146
147/// Covers nested replay length checks while keeping untrusted work proportional to input size.
148const EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER: usize = 4;
149
150/// Current wire format version for [`ExecutionWitness`] serialization.
151///
152/// The version is written as the first byte of every serialized witness. Deserialization only
153/// accepts this exact value; older formats are intentionally unsupported.
154const EXECUTION_WITNESS_WIRE_VERSION: u8 = 2;
155
156impl Serializable for ExecutionWitness {
157    fn write_into<W: ByteWriter>(&self, target: &mut W) {
158        EXECUTION_WITNESS_WIRE_VERSION.write_into(target);
159        self.vm.write_into(target);
160        match &self.precompile {
161            Some(precompile) => {
162                target.write_u8(1);
163                precompile.write_into(target);
164            },
165            None => target.write_u8(0),
166        }
167    }
168}
169
170impl Deserializable for ExecutionWitness {
171    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
172        let version = u8::read_from(source)?;
173        if version != EXECUTION_WITNESS_WIRE_VERSION {
174            return Err(DeserializationError::InvalidValue(format!(
175                "unsupported execution witness wire version {version} (expected \
176                 {EXECUTION_WITNESS_WIRE_VERSION})"
177            )));
178        }
179        let vm = VmWitness::read_from(source)?;
180        let precompile = match source.read_u8()? {
181            0 => {
182                if vm.precompile_root != TRUE_DIGEST {
183                    return Err(DeserializationError::InvalidValue(
184                        "VM witness claims deferred work but no precompile witness is present"
185                            .into(),
186                    ));
187                }
188                None
189            },
190            1 => {
191                let witness = PrecompileWitness::read_from(source)?;
192                if vm.precompile_root == TRUE_DIGEST {
193                    return Err(DeserializationError::InvalidValue(
194                        "VM witness carries a precompile witness for an empty deferred root".into(),
195                    ));
196                }
197                Some(witness)
198            },
199            tag => {
200                return Err(DeserializationError::InvalidValue(format!(
201                    "invalid precompile witness option tag {tag}"
202                )));
203            },
204        };
205        Ok(Self { vm, precompile })
206    }
207
208    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
209        ExecutionWitness::read_from_bytes(bytes)
210    }
211}
212
213/// Witness required to materialize and prove a VM execution trace.
214///
215/// This potentially large value contains private replay data and is consumed by trace-building and
216/// proving operations. Its binary form is trusted replay data: sparse MAST node and digest maps
217/// inside the trace replay are not checked against a source `MastForest` commitment; see
218/// <https://github.com/0xMiden/miden-vm/issues/3303>.
219///
220/// Its direct [`Serializable`] encoding is an unversioned implementation detail. Use the
221/// versioned [`ExecutionWitness`] envelope for transport or persistence.
222#[derive(Debug)]
223pub struct VmWitness {
224    program_info: ProgramInfo,
225    stack_inputs: StackInputs,
226    stack_outputs: StackOutputs,
227    trace: TraceReplay,
228    precompile_root: Digest,
229}
230
231impl VmWitness {
232    /// Returns the public claim associated with this witness.
233    pub fn claim(&self) -> ExecutionClaim {
234        ExecutionClaim::from_program_info(
235            self.program_info.clone(),
236            self.stack_inputs,
237            self.stack_outputs,
238        )
239    }
240
241    /// Returns whether this witness authenticates deferred precompile work.
242    pub fn has_precompiles(&self) -> bool {
243        self.precompile_root != TRUE_DIGEST
244    }
245
246    /// Returns the deferred root authenticated by the replayed VM execution.
247    pub fn precompile_root(&self) -> Digest {
248        self.precompile_root
249    }
250
251    /// Takes the hasher replay out, leaving an empty buffered one.
252    ///
253    /// The streaming path uses this to drop the replay's channel sender once execution has
254    /// finished, so the concurrently running hasher builder sees its input stream end.
255    #[cfg(feature = "std")]
256    pub(crate) fn take_hasher_replay(&mut self) -> trace_state::HasherRequestReplay {
257        core::mem::take(&mut self.trace.hasher_for_chiplet)
258    }
259
260    /// Returns the replay data captured during execution.
261    #[cfg(any(test, feature = "testing"))]
262    #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))]
263    pub(crate) fn trace_replay(&self) -> &TraceReplay {
264        &self.trace
265    }
266
267    // Kept for tests that force invalid replay data without widening the public API.
268    #[cfg(any(test, feature = "testing"))]
269    #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))]
270    pub(crate) fn trace_replay_mut(&mut self) -> &mut TraceReplay {
271        &mut self.trace
272    }
273
274    /// Returns the number of sparse MAST forests captured in the replay.
275    ///
276    /// Available under the `testing` feature for external tests that check which replay data a
277    /// serialized witness preserves.
278    #[cfg(any(test, feature = "testing"))]
279    #[allow(dead_code)]
280    pub fn mast_forest_count(&self) -> usize {
281        self.trace.mast_forest_store.len()
282    }
283}
284
285impl Serializable for VmWitness {
286    fn write_into<W: ByteWriter>(&self, target: &mut W) {
287        self.program_info.write_into(target);
288        self.stack_inputs.write_into(target);
289        self.stack_outputs.write_into(target);
290        self.trace.write_into(target);
291        self.precompile_root.write_into(target);
292    }
293}
294
295impl Deserializable for VmWitness {
296    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
297        Ok(Self {
298            program_info: ProgramInfo::read_from(source)?,
299            stack_inputs: StackInputs::read_from(source)?,
300            stack_outputs: StackOutputs::read_from(source)?,
301            trace: TraceReplay::read_from(source)?,
302            precompile_root: Digest::read_from(source)?,
303        })
304    }
305}
306
307// VM EXECUTION TRACE
308// ================================================================================================
309
310/// Execution trace which is generated when a program is executed on the VM.
311///
312/// The trace consists of the following components:
313/// - Per-AIR main traces for Core, Chiplets, Eidos compression, and byte-pair lookup.
314/// - Information about the program (program hash and the kernel).
315/// - Information about the initial and final stack states and authenticated precompile root.
316/// - Summary of trace lengths of the main trace components.
317#[derive(Debug)]
318pub struct VmTrace {
319    main_trace: MainTrace,
320    program_info: ProgramInfo,
321    stack_inputs: StackInputs,
322    stack_outputs: StackOutputs,
323    precompile_root: Digest,
324    trace_len_summary: TraceLenSummary,
325}
326
327impl VmTrace {
328    // CONSTRUCTOR
329    // --------------------------------------------------------------------------------------------
330
331    pub(crate) fn new_from_parts(
332        program_info: ProgramInfo,
333        stack_inputs: StackInputs,
334        stack_outputs: StackOutputs,
335        precompile_root: Digest,
336        main_trace: MainTrace,
337        trace_len_summary: TraceLenSummary,
338    ) -> Self {
339        Self {
340            main_trace,
341            program_info,
342            stack_inputs,
343            stack_outputs,
344            precompile_root,
345            trace_len_summary,
346        }
347    }
348
349    // PUBLIC ACCESSORS
350    // --------------------------------------------------------------------------------------------
351
352    /// Returns the program info of this execution trace.
353    pub fn program_info(&self) -> &ProgramInfo {
354        &self.program_info
355    }
356
357    /// Returns hash of the program execution of which resulted in this execution trace.
358    pub fn program_hash(&self) -> &Word {
359        self.program_info.program_hash()
360    }
361
362    /// Returns outputs of the program execution which resulted in this execution trace.
363    pub fn stack_outputs(&self) -> &StackOutputs {
364        &self.stack_outputs
365    }
366
367    /// Returns the public inputs for this execution trace.
368    pub fn public_inputs(&self) -> PublicInputs {
369        PublicInputs::new(
370            self.program_info.clone(),
371            self.stack_inputs,
372            self.stack_outputs,
373            self.precompile_root,
374        )
375    }
376
377    /// Returns the public values for this execution trace.
378    pub fn to_public_values(&self) -> Vec<Felt> {
379        self.public_inputs().to_elements()
380    }
381
382    /// Returns a reference to the main trace.
383    pub fn main_trace(&self) -> &MainTrace {
384        &self.main_trace
385    }
386
387    /// Returns a mutable reference to the main trace.
388    pub fn main_trace_mut(&mut self) -> &mut MainTrace {
389        &mut self.main_trace
390    }
391
392    /// Returns the authenticated root of the deferred precompile state.
393    pub fn precompile_root(&self) -> Digest {
394        self.precompile_root
395    }
396
397    /// Returns the owned stack outputs required for proof packaging.
398    pub fn into_outputs(self) -> StackOutputs {
399        self.stack_outputs
400    }
401
402    /// Returns the initial state of the top 16 stack registers.
403    pub fn init_stack_state(&self) -> StackInputs {
404        self.stack_inputs
405    }
406
407    /// Returns the final state of the top 16 stack registers.
408    pub fn last_stack_state(&self) -> StackOutputs {
409        let last_step = RowIndex::from(self.last_step());
410        let mut result = [ZERO; MIN_STACK_DEPTH];
411        for (i, result) in result.iter_mut().enumerate() {
412            *result = self.main_trace.stack_element(i, last_step);
413        }
414        result.into()
415    }
416
417    /// Returns helper registers state at the specified `clk` of the VM
418    pub fn get_user_op_helpers_at(&self, clk: u32) -> [Felt; NUM_USER_OP_HELPERS] {
419        let mut result = [ZERO; NUM_USER_OP_HELPERS];
420        let row = RowIndex::from(clk);
421        for (i, result) in result.iter_mut().enumerate() {
422            *result = self.main_trace.helper_register(i, row);
423        }
424        result
425    }
426
427    /// Returns the trace length.
428    pub fn get_trace_len(&self) -> usize {
429        self.main_trace.num_rows()
430    }
431
432    /// Returns the length of the trace (number of rows in the main trace).
433    pub fn length(&self) -> usize {
434        self.get_trace_len()
435    }
436
437    /// Returns a summary of the per-AIR trace lengths.
438    pub fn trace_len_summary(&self) -> &TraceLenSummary {
439        &self.trace_len_summary
440    }
441
442    // DEBUG CONSTRAINT CHECKING
443    // --------------------------------------------------------------------------------------------
444
445    /// Validates this execution trace against all AIR constraints without generating a STARK
446    /// proof.
447    ///
448    /// This is the recommended way to test trace correctness. It is much faster than full STARK
449    /// proving and provides better error diagnostics (panics on the first constraint violation
450    /// with the instance and row index).
451    ///
452    /// # Panics
453    ///
454    /// Panics if any AIR constraint evaluates to nonzero.
455    pub fn check_constraints(&self) {
456        let (core_matrix, chiplets_matrix, eidos_compression_matrix, and8_lookup_matrix) =
457            self.main_trace.clone_air_matrices();
458        let (public_values, kernel_felts) = self.public_inputs().to_air_inputs();
459
460        let statement: Statement<Felt, QuadFelt, MidenMultiAir> =
461            Statement::new(MidenMultiAir::new(), vec![(0, public_values)], kernel_felts)
462                .expect("statement construction failed");
463        let prover_statement = ProverStatement::new(
464            statement,
465            vec![core_matrix, chiplets_matrix, eidos_compression_matrix, and8_lookup_matrix],
466        )
467        .expect("prover statement construction failed");
468
469        let config = config::eidos_config(config::pcs_params(), config::RELATION_DIGEST);
470        debug::check_constraints(&prover_statement, config.challenger());
471    }
472
473    /// Clones the trace buffers into the per-AIR matrices consumed by the multi-AIR prover.
474    pub fn clone_air_matrices(
475        &self,
476    ) -> (
477        RowMajorMatrix<Felt>,
478        RowMajorMatrix<Felt>,
479        RowMajorMatrix<Felt>,
480        RowMajorMatrix<Felt>,
481    ) {
482        self.main_trace.clone_air_matrices()
483    }
484
485    /// Consuming variant for the proving hot path: moves the row-major buffers.
486    pub fn into_air_matrices(
487        self,
488    ) -> (
489        RowMajorMatrix<Felt>,
490        RowMajorMatrix<Felt>,
491        RowMajorMatrix<Felt>,
492        RowMajorMatrix<Felt>,
493    ) {
494        self.main_trace.into_air_matrices()
495    }
496
497    // HELPER METHODS
498    // --------------------------------------------------------------------------------------------
499
500    /// Returns the index of the last row in the Core trace.
501    fn last_step(&self) -> usize {
502        self.main_trace.core_height() - 1
503    }
504
505    #[cfg(any(test, feature = "testing"))]
506    pub fn get_column_range(&self, range: Range<usize>) -> Vec<Vec<Felt>> {
507        self.main_trace.get_column_range(range)
508    }
509}
510
511#[cfg(test)]
512mod wire_tests {
513    use miden_assembly::Assembler;
514    use miden_core::{
515        deferred::TRUE_DIGEST,
516        mast::{BasicBlockNodeBuilder, MastForest},
517        operations::Operation,
518        program::Program,
519    };
520    use rstest::rstest;
521
522    use super::{
523        ExecutionWitness, Serializable, build_trace,
524        trace_state::{AeadStreamReplayEntry, BitwiseOp, BitwiseReplayEntry},
525    };
526    use crate::{DefaultHost, ExecutionError, FastProcessor, Felt, StackInputs, mast::MastNodeId};
527
528    fn execution_witness(source: &str) -> ExecutionWitness {
529        let program = Assembler::default()
530            .assemble_program("program", source)
531            .expect("program should compile")
532            .unwrap_program();
533        let mut host = DefaultHost::default();
534        FastProcessor::new(StackInputs::default())
535            .execute_for_proving_sync(&program, &mut host)
536            .expect("execution should produce a witness")
537    }
538
539    fn deferred_witness() -> ExecutionWitness {
540        execution_witness("begin log_deferred end")
541    }
542
543    fn deferred_witness_bytes() -> alloc::vec::Vec<u8> {
544        deferred_witness().to_bytes()
545    }
546
547    #[test]
548    fn witness_reports_precompile_state() {
549        let plain = execution_witness("begin push.1 drop end");
550        assert!(!plain.has_precompiles());
551
552        let deferred = execution_witness("begin log_deferred end");
553        assert!(deferred.has_precompiles());
554    }
555
556    fn aead_stream_witness() -> ExecutionWitness {
557        let mut mast_forest = MastForest::new();
558        let basic_block_id = BasicBlockNodeBuilder::new(vec![Operation::CryptoStream])
559            .add_to_forest(&mut mast_forest)
560            .unwrap();
561        mast_forest.make_root(basic_block_id);
562        let program = Program::new(mast_forest.into(), basic_block_id);
563
564        let stack = [
565            1, 2, 3, 4, // K_CTR
566            0, // counter
567            0, // src_ptr
568            8, // dst_ptr
569            1, // remaining
570            0, 0, 0, 0, 0, 0, 0, 0, // tail
571        ]
572        .map(Felt::new_unchecked);
573        let mut host = DefaultHost::default();
574        FastProcessor::new(StackInputs::new(&stack).unwrap())
575            .execute_for_proving_sync(&program, &mut host)
576            .expect("AEAD stream execution should produce a witness")
577    }
578
579    #[test]
580    fn witness_wire_rejects_unsupported_version() {
581        let mut bytes = deferred_witness_bytes();
582        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
583        assert_eq!(bytes[0], 2, "Eidos witnesses must use wire version 2");
584
585        // Rejected versions are checked before decoding replay or witness payloads.
586        for version in [0, 1, super::EXECUTION_WITNESS_WIRE_VERSION + 1] {
587            bytes[0] = version;
588            let err = ExecutionWitness::read_from_bytes(&bytes)
589                .expect_err("unsupported witness format must be rejected");
590            assert!(format!("{err:?}").contains("unsupported execution witness wire version"));
591        }
592    }
593
594    #[test]
595    fn witness_wire_rejects_trailing_bytes() {
596        let mut bytes = deferred_witness_bytes();
597        bytes.push(0);
598
599        let err = ExecutionWitness::read_from_bytes(&bytes)
600            .expect_err("witness payload with trailing bytes should be rejected");
601        assert!(
602            format!("{err:?}").contains("extra bytes after execution witness payload"),
603            "unexpected error: {err:?}"
604        );
605
606        assert!(
607            ExecutionWitness::read_from_bytes_trusted(&bytes).is_ok(),
608            "the explicit trusted reader should preserve the old permissive behavior"
609        );
610    }
611
612    #[test]
613    fn witness_wire_accepts_large_minimally_encoded_continuation_stack() {
614        let mut witness = deferred_witness();
615        let continuation = &mut witness
616            .vm
617            .trace_replay_mut()
618            .core_trace_contexts
619            .first_mut()
620            .expect("witness should contain a trace fragment")
621            .continuation;
622        for _ in 0..4096 {
623            continuation.push_start_node(MastNodeId::from(0));
624        }
625
626        let bytes = witness.to_bytes();
627        let restored = ExecutionWitness::read_from_bytes(&bytes)
628            .expect("valid witness should fit its input-proportional allocation budget");
629
630        assert_eq!(restored.to_bytes(), bytes);
631    }
632
633    #[test]
634    fn aead_stream_witness_round_trip_rebuilds_the_same_trace() {
635        let witness = aead_stream_witness();
636        let bytes = witness.to_bytes();
637        let claim = witness.claim();
638        let (original_vm, original_precompile) = witness.into_parts();
639        assert!(original_precompile.is_none());
640        let original_trace = build_trace(original_vm).expect("original trace should build");
641
642        let restored =
643            ExecutionWitness::read_from_bytes(&bytes).expect("witness should round trip");
644        assert_eq!(restored.claim(), claim);
645        let (restored_vm, restored_precompile) = restored.into_parts();
646        assert!(restored_precompile.is_none());
647        let restored_trace = build_trace(restored_vm).expect("restored trace should build");
648
649        assert_eq!(restored_trace.public_inputs(), original_trace.public_inputs());
650        assert_eq!(restored_trace.trace_len_summary(), original_trace.trace_len_summary());
651        assert_eq!(restored_trace.clone_air_matrices(), original_trace.clone_air_matrices());
652    }
653
654    #[rstest]
655    #[case::keystream_not_u32(
656        |entry: &mut AeadStreamReplayEntry, lane: usize| {
657            entry.keystream[lane] = Felt::new_unchecked(1 << 32);
658        },
659        "operation expected u32 values"
660    )]
661    #[case::ciphertext_not_u32(
662        |entry: &mut AeadStreamReplayEntry, lane: usize| {
663            entry.ciphertext[lane] = Felt::new_unchecked(1 << 32);
664        },
665        "operation expected u32 values"
666    )]
667    #[case::ciphertext_mismatch(
668        |entry: &mut AeadStreamReplayEntry, lane: usize| {
669            entry.ciphertext[lane] =
670                Felt::new_unchecked(entry.ciphertext[lane].as_canonical_u64() ^ 1);
671        },
672        "AEAD stream ciphertext does not match plaintext XOR keystream"
673    )]
674    fn build_trace_rejects_inconsistent_aead_stream_replay(
675        #[case] tamper: fn(&mut AeadStreamReplayEntry, usize),
676        #[case] expected_error: &str,
677        #[values(0, 7)] lane: usize,
678    ) {
679        let mut witness = aead_stream_witness();
680        let replay = &mut witness.vm.trace_replay_mut().bitwise_replay;
681        let mut tampered = false;
682        for entry in core::mem::take(replay) {
683            match entry {
684                BitwiseReplayEntry::AeadStream(mut entry) => {
685                    if !tampered {
686                        tamper(&mut entry, lane);
687                        tampered = true;
688                    }
689                    replay.record_aead_stream(*entry);
690                },
691                BitwiseReplayEntry::U32(BitwiseOp::U32And, a, b) => replay.record_u32and(a, b),
692                BitwiseReplayEntry::U32(BitwiseOp::U32Xor, a, b) => replay.record_u32xor(a, b),
693            }
694        }
695        assert!(tampered, "the witness should contain an AEAD stream entry");
696
697        let witness = ExecutionWitness::read_from_bytes(&witness.to_bytes())
698            .expect("the wire reader accepts Felt-valued replay lanes");
699        let Err(error) = build_trace(witness.into_parts().0) else {
700            panic!("invalid AEAD stream replay must return an error");
701        };
702        assert!(matches!(error, ExecutionError::OperationError { .. }));
703        assert!(format!("{error}").contains(expected_error), "unexpected error: {error}");
704    }
705
706    #[test]
707    fn witness_wire_rejects_precompile_witness_with_true_root() {
708        let bytes = deferred_witness_bytes();
709        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
710        let (vm, precompile) = restored.into_parts();
711        let precompile = precompile.expect("deferred execution should carry a precompile witness");
712        assert_ne!(vm.precompile_root, TRUE_DIGEST);
713
714        // A carried witness with an explicitly empty VM-side root is not a valid transport shape.
715        let tampered = ExecutionWitness {
716            vm: super::VmWitness { precompile_root: TRUE_DIGEST, ..vm },
717            precompile: Some(precompile),
718        };
719        let err = ExecutionWitness::read_from_bytes(&tampered.to_bytes())
720            .expect_err("tampered witness should be rejected");
721        assert!(
722            format!("{err:?}")
723                .contains("VM witness carries a precompile witness for an empty deferred root"),
724            "unexpected error: {err:?}"
725        );
726
727        // Sanity: the untampered wire still round-trips.
728        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
729    }
730
731    #[test]
732    fn witness_wire_rejects_missing_precompile_witness() {
733        let bytes = deferred_witness_bytes();
734        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
735        let (vm, precompile) = restored.into_parts();
736        assert!(precompile.is_some(), "deferred execution should carry a precompile witness");
737
738        // Drop only the precompile witness while the VM side still claims deferred work: the
739        // wire must not validate as a complete execution.
740        let stripped = ExecutionWitness { vm, precompile: None };
741        let err = ExecutionWitness::read_from_bytes(&stripped.to_bytes())
742            .expect_err("witness without its precompile half should be rejected");
743        assert!(
744            format!("{err:?}")
745                .contains("VM witness claims deferred work but no precompile witness is present"),
746            "unexpected error: {err:?}"
747        );
748    }
749}