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::{
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            deferred_state: precompiles,
81        } = execution_output;
82        let precompile_root = precompiles.root();
83        let vm = VmWitness {
84            program_info,
85            stack_inputs,
86            stack_outputs,
87            trace,
88            precompile_root,
89        };
90        let precompile = (precompile_root != TRUE_DIGEST).then(|| {
91            PrecompileWitness::new(precompiles)
92                .expect("a non-TRUE execution root must produce a singleton precompile witness")
93        });
94
95        Self { vm, precompile }
96    }
97
98    /// Returns the public claim associated with this witness.
99    pub fn claim(&self) -> ExecutionClaim {
100        self.vm.claim()
101    }
102
103    /// Returns whether this witness contains precompile work.
104    pub const fn has_precompiles(&self) -> bool {
105        self.precompile.is_some()
106    }
107
108    /// Consumes this witness and returns its supported low-level proving components.
109    ///
110    /// The [`VmWitness`] can be passed to [`build_trace`]. The optional [`PrecompileWitness`] is
111    /// present only when execution authenticated deferred precompile work.
112    pub fn into_parts(self) -> (VmWitness, Option<PrecompileWitness>) {
113        (self.vm, self.precompile)
114    }
115
116    /// Decodes one execution witness from a potentially adversarial byte slice.
117    ///
118    /// The reader applies an input-proportional limit to byte consumption and collection
119    /// preallocation, then rejects trailing bytes. These checks establish safe transport syntax.
120    /// They do not prove that the sparse MAST replay is a subset of a particular source forest
121    /// because the witness wire does not carry such a proof.
122    ///
123    /// Use [`Self::read_from_bytes_trusted`] for bytes retained inside a trusted prover system.
124    #[track_caller]
125    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
126        let budget = bytes.len().saturating_mul(EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER);
127        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
128        let witness = <Self as Deserializable>::read_from(&mut reader)?;
129
130        if reader.has_more_bytes() {
131            return Err(DeserializationError::InvalidValue(
132                "extra bytes after execution witness payload".into(),
133            ));
134        }
135        Ok(witness)
136    }
137
138    /// Decodes execution witness bytes retained inside a trusted prover system.
139    ///
140    /// This preserves the original permissive byte-slice behavior. It trusts sparse MAST replay
141    /// hashes, accepts trailing bytes, and uses a replay-sized byte budget. Use
142    /// [`Self::read_from_bytes`] for bytes received across a trust boundary.
143    #[track_caller]
144    pub fn read_from_bytes_trusted(bytes: &[u8]) -> Result<Self, DeserializationError> {
145        let budget = bytes.len().saturating_mul(EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER);
146        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
147        <Self as Deserializable>::read_from(&mut reader)
148    }
149}
150
151/// Covers nested replay length checks while keeping untrusted work proportional to input size.
152const EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER: usize = 4;
153
154/// Current wire format version for [`ExecutionWitness`] serialization.
155///
156/// The version is written as the first byte of every serialized witness. Deserialization only
157/// accepts this exact value, so a future format change only needs to add a new accepted version
158/// and keep the old readers where compatibility matters.
159const EXECUTION_WITNESS_WIRE_VERSION: u8 = 1;
160
161impl Serializable for ExecutionWitness {
162    fn write_into<W: ByteWriter>(&self, target: &mut W) {
163        EXECUTION_WITNESS_WIRE_VERSION.write_into(target);
164        self.vm.write_into(target);
165        match &self.precompile {
166            Some(precompile) => {
167                target.write_u8(1);
168                write_precompile_witness(precompile, target);
169            },
170            None => target.write_u8(0),
171        }
172    }
173}
174
175impl Deserializable for ExecutionWitness {
176    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
177        let version = u8::read_from(source)?;
178        if version != EXECUTION_WITNESS_WIRE_VERSION {
179            return Err(DeserializationError::InvalidValue(format!(
180                "unsupported execution witness wire version {version} (expected \
181                 {EXECUTION_WITNESS_WIRE_VERSION})"
182            )));
183        }
184        let vm = VmWitness::read_from(source)?;
185        let precompile = match source.read_u8()? {
186            0 => {
187                if vm.precompile_root != TRUE_DIGEST {
188                    return Err(DeserializationError::InvalidValue(
189                        "VM witness claims deferred work but no precompile witness is present"
190                            .into(),
191                    ));
192                }
193                None
194            },
195            1 => {
196                let witness = read_precompile_witness(source)?;
197                // `read_precompile_witness` only produces singleton witnesses, but do not index
198                // blindly: keep deserialization panic-free even if that invariant changes.
199                let [witness_root] = witness.roots() else {
200                    return Err(DeserializationError::InvalidValue(
201                        "expected a singleton precompile witness".into(),
202                    ));
203                };
204                if *witness_root != vm.precompile_root {
205                    return Err(DeserializationError::InvalidValue(
206                        "precompile witness root does not match the VM witness precompile root"
207                            .into(),
208                    ));
209                }
210                Some(witness)
211            },
212            tag => {
213                return Err(DeserializationError::InvalidValue(format!(
214                    "invalid precompile witness option tag {tag}"
215                )));
216            },
217        };
218        Ok(Self { vm, precompile })
219    }
220
221    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
222        ExecutionWitness::read_from_bytes(bytes)
223    }
224}
225
226/// Witness required to materialize and prove a VM execution trace.
227///
228/// This potentially large value contains private replay data and is consumed by trace-building and
229/// proving operations. Its binary form is trusted replay data: sparse MAST node and digest maps
230/// inside the trace replay are not checked against a source `MastForest` commitment; see
231/// <https://github.com/0xMiden/miden-vm/issues/3303>.
232#[derive(Debug)]
233pub struct VmWitness {
234    program_info: ProgramInfo,
235    stack_inputs: StackInputs,
236    stack_outputs: StackOutputs,
237    trace: TraceReplay,
238    precompile_root: Digest,
239}
240
241impl VmWitness {
242    /// Returns the public claim associated with this witness.
243    pub fn claim(&self) -> ExecutionClaim {
244        ExecutionClaim::from_program_info(
245            self.program_info.clone(),
246            self.stack_inputs,
247            self.stack_outputs,
248        )
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/// Writes a singleton precompile witness as its ordered roots followed by its canonical deferred
308/// wire.
309fn write_precompile_witness<W: ByteWriter>(witness: &PrecompileWitness, target: &mut W) {
310    let roots = witness.roots();
311    debug_assert_eq!(roots.len(), 1, "only singleton precompile witnesses are serializable");
312    target.write_usize(roots.len());
313    for root in roots {
314        root.write_into(target);
315    }
316    let deferred_wire = witness
317        .state()
318        .to_wire()
319        .expect("deferred state must serialize to canonical wire");
320    deferred_wire.write_into(target);
321}
322
323/// Reads a singleton precompile witness written by [`write_precompile_witness`].
324fn read_precompile_witness<R: ByteReader>(
325    source: &mut R,
326) -> Result<PrecompileWitness, DeserializationError> {
327    let roots = Vec::<Digest>::read_from(source)?;
328    if roots.len() != 1 {
329        return Err(DeserializationError::InvalidValue(
330            "expected a singleton precompile witness".into(),
331        ));
332    }
333    let deferred_wire = DeferredStateWire::read_from(source)?;
334    let deferred_state =
335        DeferredState::from_wire(Arc::new(miden_precompiles::registry()), &deferred_wire).map_err(
336            |err| DeserializationError::InvalidValue(format!("invalid deferred state: {err}")),
337        )?;
338
339    let witness = PrecompileWitness::new(deferred_state).map_err(|err| {
340        DeserializationError::InvalidValue(format!("invalid precompile witness: {err}"))
341    })?;
342    if witness.roots() != roots.as_slice() {
343        return Err(DeserializationError::InvalidValue(
344            "precompile witness roots do not match its deferred state".into(),
345        ));
346    }
347    Ok(witness)
348}
349
350// VM EXECUTION TRACE
351// ================================================================================================
352
353/// Execution trace which is generated when a program is executed on the VM.
354///
355/// The trace consists of the following components:
356/// - Per-AIR trace matrices for Core, Chiplets, and Poseidon2Permutation.
357/// - Information about the program (program hash and the kernel).
358/// - Information about the initial and final stack states and authenticated precompile root.
359/// - Summary of trace lengths of the main trace components.
360#[derive(Debug)]
361pub struct VmTrace {
362    main_trace: MainTrace,
363    program_info: ProgramInfo,
364    stack_inputs: StackInputs,
365    stack_outputs: StackOutputs,
366    precompile_root: Digest,
367    trace_len_summary: TraceLenSummary,
368}
369
370impl VmTrace {
371    // CONSTRUCTOR
372    // --------------------------------------------------------------------------------------------
373
374    pub(crate) fn new_from_parts(
375        program_info: ProgramInfo,
376        stack_inputs: StackInputs,
377        stack_outputs: StackOutputs,
378        precompile_root: Digest,
379        main_trace: MainTrace,
380        trace_len_summary: TraceLenSummary,
381    ) -> Self {
382        Self {
383            main_trace,
384            program_info,
385            stack_inputs,
386            stack_outputs,
387            precompile_root,
388            trace_len_summary,
389        }
390    }
391
392    // PUBLIC ACCESSORS
393    // --------------------------------------------------------------------------------------------
394
395    /// Returns the program info of this execution trace.
396    pub fn program_info(&self) -> &ProgramInfo {
397        &self.program_info
398    }
399
400    /// Returns hash of the program execution of which resulted in this execution trace.
401    pub fn program_hash(&self) -> &Word {
402        self.program_info.program_hash()
403    }
404
405    /// Returns outputs of the program execution which resulted in this execution trace.
406    pub fn stack_outputs(&self) -> &StackOutputs {
407        &self.stack_outputs
408    }
409
410    /// Returns the public inputs for this execution trace.
411    pub fn public_inputs(&self) -> PublicInputs {
412        PublicInputs::new(
413            self.program_info.clone(),
414            self.stack_inputs,
415            self.stack_outputs,
416            self.precompile_root,
417        )
418    }
419
420    /// Returns the public values for this execution trace.
421    pub fn to_public_values(&self) -> Vec<Felt> {
422        self.public_inputs().to_elements()
423    }
424
425    /// Returns a reference to the main trace.
426    pub fn main_trace(&self) -> &MainTrace {
427        &self.main_trace
428    }
429
430    /// Returns a mutable reference to the main trace.
431    pub fn main_trace_mut(&mut self) -> &mut MainTrace {
432        &mut self.main_trace
433    }
434
435    /// Returns the authenticated root of the deferred precompile state.
436    pub fn precompile_root(&self) -> Digest {
437        self.precompile_root
438    }
439
440    /// Returns the owned stack outputs required for proof packaging.
441    pub fn into_outputs(self) -> StackOutputs {
442        self.stack_outputs
443    }
444
445    /// Returns the initial state of the top 16 stack registers.
446    pub fn init_stack_state(&self) -> StackInputs {
447        self.stack_inputs
448    }
449
450    /// Returns the final state of the top 16 stack registers.
451    pub fn last_stack_state(&self) -> StackOutputs {
452        let last_step = RowIndex::from(self.last_step());
453        let mut result = [ZERO; MIN_STACK_DEPTH];
454        for (i, result) in result.iter_mut().enumerate() {
455            *result = self.main_trace.stack_element(i, last_step);
456        }
457        result.into()
458    }
459
460    /// Returns helper registers state at the specified `clk` of the VM
461    pub fn get_user_op_helpers_at(&self, clk: u32) -> [Felt; NUM_USER_OP_HELPERS] {
462        let mut result = [ZERO; NUM_USER_OP_HELPERS];
463        let row = RowIndex::from(clk);
464        for (i, result) in result.iter_mut().enumerate() {
465            *result = self.main_trace.helper_register(i, row);
466        }
467        result
468    }
469
470    /// Returns the trace length.
471    pub fn get_trace_len(&self) -> usize {
472        self.main_trace.num_rows()
473    }
474
475    /// Returns the length of the trace (number of rows in the main trace).
476    pub fn length(&self) -> usize {
477        self.get_trace_len()
478    }
479
480    /// Returns a summary of the per-component trace lengths.
481    pub fn trace_len_summary(&self) -> &TraceLenSummary {
482        &self.trace_len_summary
483    }
484
485    // DEBUG CONSTRAINT CHECKING
486    // --------------------------------------------------------------------------------------------
487
488    /// Validates this execution trace against all AIR constraints without generating a STARK
489    /// proof.
490    ///
491    /// This is the recommended way to test trace correctness. It is much faster than full STARK
492    /// proving and provides better error diagnostics (panics on the first constraint violation
493    /// with the instance and row index).
494    ///
495    /// # Panics
496    ///
497    /// Panics if any AIR constraint evaluates to nonzero.
498    pub fn check_constraints(&self) {
499        let public_inputs = self.public_inputs();
500        let (core_matrix, chiplets_matrix, poseidon2_matrix) = self.main_trace.to_air_matrices();
501
502        let (public_values, aux_inputs) = public_inputs.to_air_inputs();
503
504        let statement =
505            Statement::<Felt, QuadFelt, _>::new(MidenMultiAir::new(), public_values, aux_inputs)
506                .expect("valid statement inputs");
507        let prover_statement =
508            ProverStatement::new(statement, vec![core_matrix, chiplets_matrix, poseidon2_matrix])
509                .expect("valid trace shapes");
510
511        // A deterministic challenger seeds the debug constraint check; this is a local
512        // constraint debugger, not a full proof transcript, so any fixed challenge set works.
513        let config = config::poseidon2_config(config::pcs_params(), config::RELATION_DIGEST);
514        debug::check_constraints(&prover_statement, config.challenger());
515    }
516
517    /// Splits the trace into the per-AIR matrices consumed by the multi-AIR proving path.
518    pub fn to_air_matrices(
519        &self,
520    ) -> (RowMajorMatrix<Felt>, RowMajorMatrix<Felt>, RowMajorMatrix<Felt>) {
521        self.main_trace.to_air_matrices()
522    }
523
524    /// Consuming variant for the proving hot path.
525    pub fn into_air_matrices(
526        self,
527    ) -> (RowMajorMatrix<Felt>, RowMajorMatrix<Felt>, RowMajorMatrix<Felt>) {
528        self.main_trace.into_air_matrices()
529    }
530
531    // HELPER METHODS
532    // --------------------------------------------------------------------------------------------
533
534    /// Returns the index of the last row in the Core trace.
535    fn last_step(&self) -> usize {
536        self.main_trace.core_height() - 1
537    }
538
539    #[cfg(any(test, feature = "testing"))]
540    pub fn get_column_range(&self, range: Range<usize>) -> Vec<Vec<Felt>> {
541        self.main_trace.get_column_range(range)
542    }
543}
544
545#[cfg(test)]
546mod wire_tests {
547    use miden_assembly::Assembler;
548    use miden_core::deferred::TRUE_DIGEST;
549
550    use super::{ExecutionWitness, Serializable};
551    use crate::{DefaultHost, FastProcessor, StackInputs, mast::MastNodeId};
552
553    fn execution_witness(source: &str) -> ExecutionWitness {
554        let program = Assembler::default()
555            .assemble_program("program", source)
556            .expect("program should compile")
557            .unwrap_program();
558        let mut host = DefaultHost::default();
559        FastProcessor::new(StackInputs::default())
560            .execute_for_proving_sync(&program, &mut host)
561            .expect("execution should produce a witness")
562    }
563
564    fn deferred_witness() -> ExecutionWitness {
565        execution_witness("begin log_deferred end")
566    }
567
568    fn deferred_witness_bytes() -> alloc::vec::Vec<u8> {
569        deferred_witness().to_bytes()
570    }
571
572    #[test]
573    fn witness_reports_precompile_state() {
574        let plain = execution_witness("begin push.1 drop end");
575        assert!(!plain.has_precompiles());
576
577        let deferred = execution_witness("begin log_deferred end");
578        assert!(deferred.has_precompiles());
579    }
580
581    #[test]
582    fn witness_wire_rejects_unsupported_version() {
583        let mut bytes = deferred_witness_bytes();
584        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
585
586        // The first byte of the wire is the format version; any other value must be rejected
587        // before any payload is parsed.
588        bytes[0] = bytes[0].wrapping_add(1);
589        let err = ExecutionWitness::read_from_bytes(&bytes)
590            .expect_err("witness with an unknown wire version should be rejected");
591        assert!(
592            format!("{err:?}").contains("unsupported execution witness wire version"),
593            "unexpected error: {err:?}"
594        );
595    }
596
597    #[test]
598    fn witness_wire_rejects_trailing_bytes() {
599        let mut bytes = deferred_witness_bytes();
600        bytes.push(0);
601
602        let err = ExecutionWitness::read_from_bytes(&bytes)
603            .expect_err("witness payload with trailing bytes should be rejected");
604        assert!(
605            format!("{err:?}").contains("extra bytes after execution witness payload"),
606            "unexpected error: {err:?}"
607        );
608
609        assert!(
610            ExecutionWitness::read_from_bytes_trusted(&bytes).is_ok(),
611            "the explicit trusted reader should preserve the old permissive behavior"
612        );
613    }
614
615    #[test]
616    fn witness_wire_accepts_large_minimally_encoded_continuation_stack() {
617        let mut witness = deferred_witness();
618        let continuation = &mut witness
619            .vm
620            .trace_replay_mut()
621            .core_trace_contexts
622            .first_mut()
623            .expect("witness should contain a trace fragment")
624            .continuation;
625        for _ in 0..4096 {
626            continuation.push_start_node(MastNodeId::from(0));
627        }
628
629        let bytes = witness.to_bytes();
630        let restored = ExecutionWitness::read_from_bytes(&bytes)
631            .expect("valid witness should fit its input-proportional allocation budget");
632
633        assert_eq!(restored.to_bytes(), bytes);
634    }
635
636    #[test]
637    fn witness_wire_rejects_mismatched_precompile_root() {
638        let bytes = deferred_witness_bytes();
639        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
640        let (vm, precompile) = restored.into_parts();
641        let precompile = precompile.expect("deferred execution should carry a precompile witness");
642        assert_ne!(vm.precompile_root, TRUE_DIGEST);
643
644        // Tamper only the VM-side precompile root and re-serialize: the two halves of the wire
645        // no longer describe the same execution, so deserialization must reject them.
646        let tampered = ExecutionWitness {
647            vm: super::VmWitness { precompile_root: TRUE_DIGEST, ..vm },
648            precompile: Some(precompile),
649        };
650        let err = ExecutionWitness::read_from_bytes(&tampered.to_bytes())
651            .expect_err("tampered witness should be rejected");
652        assert!(
653            format!("{err:?}")
654                .contains("precompile witness root does not match the VM witness precompile root"),
655            "unexpected error: {err:?}"
656        );
657
658        // Sanity: the untampered wire still round-trips.
659        assert!(ExecutionWitness::read_from_bytes(&bytes).is_ok());
660    }
661
662    #[test]
663    fn witness_wire_rejects_missing_precompile_witness() {
664        let bytes = deferred_witness_bytes();
665        let restored = ExecutionWitness::read_from_bytes(&bytes).expect("witness round trip");
666        let (vm, precompile) = restored.into_parts();
667        assert!(precompile.is_some(), "deferred execution should carry a precompile witness");
668
669        // Drop only the precompile witness while the VM side still claims deferred work: the
670        // wire must not validate as a complete execution.
671        let stripped = ExecutionWitness { vm, precompile: None };
672        let err = ExecutionWitness::read_from_bytes(&stripped.to_bytes())
673            .expect_err("witness without its precompile half should be rejected");
674        assert!(
675            format!("{err:?}")
676                .contains("VM witness claims deferred work but no precompile witness is present"),
677            "unexpected error: {err:?}"
678        );
679    }
680}