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
38pub(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#[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#[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 pub fn claim(&self) -> ExecutionClaim {
96 self.vm.claim()
97 }
98
99 pub const fn has_precompiles(&self) -> bool {
101 self.precompile.is_some()
102 }
103
104 pub fn into_parts(self) -> (VmWitness, Option<PrecompileWitness>) {
109 (self.vm, self.precompile)
110 }
111
112 #[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 #[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
147const EXECUTION_WITNESS_BYTE_READ_BUDGET_MULTIPLIER: usize = 4;
149
150const 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#[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 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 pub fn has_precompiles(&self) -> bool {
243 self.precompile_root != TRUE_DIGEST
244 }
245
246 pub fn precompile_root(&self) -> Digest {
248 self.precompile_root
249 }
250
251 #[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 #[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 #[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 #[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#[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 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 pub fn program_info(&self) -> &ProgramInfo {
354 &self.program_info
355 }
356
357 pub fn program_hash(&self) -> &Word {
359 self.program_info.program_hash()
360 }
361
362 pub fn stack_outputs(&self) -> &StackOutputs {
364 &self.stack_outputs
365 }
366
367 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 pub fn to_public_values(&self) -> Vec<Felt> {
379 self.public_inputs().to_elements()
380 }
381
382 pub fn main_trace(&self) -> &MainTrace {
384 &self.main_trace
385 }
386
387 pub fn main_trace_mut(&mut self) -> &mut MainTrace {
389 &mut self.main_trace
390 }
391
392 pub fn precompile_root(&self) -> Digest {
394 self.precompile_root
395 }
396
397 pub fn into_outputs(self) -> StackOutputs {
399 self.stack_outputs
400 }
401
402 pub fn init_stack_state(&self) -> StackInputs {
404 self.stack_inputs
405 }
406
407 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 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 pub fn get_trace_len(&self) -> usize {
429 self.main_trace.num_rows()
430 }
431
432 pub fn length(&self) -> usize {
434 self.get_trace_len()
435 }
436
437 pub fn trace_len_summary(&self) -> &TraceLenSummary {
439 &self.trace_len_summary
440 }
441
442 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 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 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 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, 0, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0, 0, ]
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 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 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 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 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}