From c2916ba652f782124a284ddd92e8c2d071d84370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 22 Jun 2026 19:00:24 -0400 Subject: [PATCH 01/12] Serialize trace proving inputs --- core/src/lib.rs | 49 ++ core/src/mast/mod.rs | 27 +- core/src/mast/serialization/layout.rs | 15 +- core/src/mast/serialization/mod.rs | 25 +- core/src/mast/serialization/seed_gen.rs | 89 ++- core/src/mast/serialization/sparse.rs | 384 +++++++++ core/src/mast/serialization/tests.rs | 363 ++++++++- core/src/mast/sparse.rs | 175 +++++ core/src/precompile.rs | 12 + miden-core-fuzz/Cargo.lock | 128 +++ miden-core-fuzz/Cargo.toml | 24 + .../sparse_mast_forest_deserialize.rs | 16 + .../sparse_mast_forest_validate.rs | 18 + .../trace_proving_inputs_deserialize.rs | 18 + miden-vm/tests/integration/prove_verify.rs | 102 ++- processor/src/continuation_stack.rs | 167 +++- processor/src/lib.rs | 18 + processor/src/trace/chiplets/ace/trace.rs | 279 +++++++ processor/src/trace/execution_tracer.rs | 181 ++++- processor/src/trace/mod.rs | 49 +- processor/src/trace/trace_state.rs | 730 ++++++++++++++++++ prover/src/lib.rs | 63 +- prover/src/proving_options.rs | 17 +- prover/tests/trace_proving_inputs_seeds.rs | 42 + 24 files changed, 2966 insertions(+), 25 deletions(-) create mode 100644 core/src/mast/serialization/sparse.rs create mode 100644 miden-core-fuzz/fuzz_targets/sparse_mast_forest_deserialize.rs create mode 100644 miden-core-fuzz/fuzz_targets/sparse_mast_forest_validate.rs create mode 100644 miden-core-fuzz/fuzz_targets/trace_proving_inputs_deserialize.rs create mode 100644 prover/tests/trace_proving_inputs_seeds.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index 7b43197af0..5abe99a88a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -31,6 +31,8 @@ pub mod field { } pub mod serde { + use alloc::collections::VecDeque; + pub use miden_crypto::utils::{ BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader, @@ -73,6 +75,53 @@ pub mod serde { err => err, }) } + + /// Serializable view over a [`VecDeque`]. + /// + /// This uses the same wire shape as `Vec`: a length prefix followed by items in iteration + /// order. + pub struct SerializableVecDeque<'a, T>(pub &'a VecDeque); + + impl Serializable for SerializableVecDeque<'_, T> { + fn write_into(&self, target: &mut W) { + target.write_usize(self.0.len()); + for item in self.0 { + item.write_into(target); + } + } + } + + /// Reads a [`VecDeque`] encoded by [`SerializableVecDeque`]. + pub fn read_vec_deque( + source: &mut R, + ) -> Result, DeserializationError> { + let len = read_bounded_len(source, "VecDeque", T::min_serialized_size())?; + let mut values = VecDeque::with_capacity(len); + for _ in 0..len { + values.push_back(T::read_from(source)?); + } + Ok(values) + } + + #[cfg(test)] + mod tests { + use alloc::{collections::VecDeque, vec::Vec}; + + use super::{Deserializable, Serializable, SerializableVecDeque, read_vec_deque}; + + #[test] + fn vec_deque_round_trip_uses_vec_shape() { + let values = VecDeque::from([1u32, 2, 3]); + let mut bytes = Vec::new(); + SerializableVecDeque(&values).write_into(&mut bytes); + + let restored = read_vec_deque(&mut super::SliceReader::new(&bytes)).unwrap(); + assert_eq!(values, restored); + + let vec = Vec::::read_from_bytes(&bytes).unwrap(); + assert_eq!(vec, [1, 2, 3]); + } + } } pub mod crypto { diff --git a/core/src/mast/mod.rs b/core/src/mast/mod.rs index a8c03ddb6d..5d1f095274 100644 --- a/core/src/mast/mod.rs +++ b/core/src/mast/mod.rs @@ -50,6 +50,9 @@ use proptest::prelude::*; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +#[cfg(feature = "serde")] +use crate::serde::SliceReader; + mod node; #[cfg(any(test, feature = "arbitrary"))] pub use node::arbitrary; @@ -61,19 +64,17 @@ pub use node::{ OpBatch, SplitNode, SplitNodeBuilder, }; -#[cfg(feature = "serde")] -use crate::serde::{Deserializable, Serializable, SliceReader}; use crate::{ Felt, Word, advice::AdviceMap, - serde::{ByteWriter, DeserializationError}, + serde::{ByteWriter, Deserializable, DeserializationError, Serializable}, utils::{Idx, IndexVec, hash_string_to_word}, }; mod serialization; pub use serialization::{ AdviceMapView, AdviceValueView, MastForestReadMode, MastForestReadView, MastForestView, - MastForestWireView, MastNodeEntry, MastNodeInfo, + MastForestWireView, MastNodeEntry, MastNodeInfo, SparseMastForestReadOptions, }; mod untrusted; @@ -907,6 +908,24 @@ impl From for u32 { } } +impl Serializable for MastNodeId { + fn write_into(&self, target: &mut W) { + Serializable::write_into(&self.0, target); + } +} + +impl Deserializable for MastNodeId { + fn read_from( + source: &mut R, + ) -> Result { + Ok(Self(::read_from(source)?)) + } + + fn min_serialized_size() -> usize { + ::min_serialized_size() + } +} + impl fmt::Display for MastNodeId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "MastNodeId({})", self.0) diff --git a/core/src/mast/serialization/layout.rs b/core/src/mast/serialization/layout.rs index 95fab0d2ca..b790e593b1 100644 --- a/core/src/mast/serialization/layout.rs +++ b/core/src/mast/serialization/layout.rs @@ -1,6 +1,8 @@ use alloc::{format, string::ToString, vec::Vec}; -use super::{FLAG_HASHLESS, FLAGS_RESERVED_MASK, MAGIC, MastForest, MastNodeEntry, VERSION}; +use super::{ + FLAG_HASHLESS, FLAG_SPARSE, FLAGS_RESERVED_MASK, MAGIC, MastForest, MastNodeEntry, VERSION, +}; use crate::{ mast::MastNodeId, serde::{ByteReader, Deserializable, DeserializationError, SliceReader}, @@ -157,6 +159,10 @@ impl WireFlags { pub(super) fn is_hashless(self) -> bool { self.0 & FLAG_HASHLESS != 0 } + + pub(super) fn is_sparse(self) -> bool { + self.0 & FLAG_SPARSE != 0 + } } // LAYOUT SCANNING @@ -171,6 +177,11 @@ pub(super) fn read_header_and_scan_layout( // untrusted deserialization path. let (raw_flags, _version) = read_and_validate_header(source)?; let flags = WireFlags::new(raw_flags); + if flags.is_sparse() { + return Err(DeserializationError::InvalidValue( + "SPARSE flag is set; use SparseMastForest for sparse replay input".to_string(), + )); + } if flags.is_hashless() && !allow_hashless { return Err(DeserializationError::InvalidValue( "HASHLESS flag is set; use UntrustedMastForest for untrusted input".to_string(), @@ -457,7 +468,7 @@ fn validate_budgeted_count( Ok(()) } -fn read_and_validate_header( +pub(super) fn read_and_validate_header( source: &mut R, ) -> Result<(u8, [u8; 3]), DeserializationError> { let magic: [u8; 4] = source.read_array()?; diff --git a/core/src/mast/serialization/mod.rs b/core/src/mast/serialization/mod.rs index 1fa158dbc7..a239023e87 100644 --- a/core/src/mast/serialization/mod.rs +++ b/core/src/mast/serialization/mod.rs @@ -76,9 +76,13 @@ //! same contiguous array on the wire. //! //! Public entry points adopt these policies: -//! - [`MastForest::read_from_bytes`]: trusted execution payload, no hashless support. +//! - [`MastForest::read_from_bytes`]: trusted dense execution payload, no hashless or sparse +//! support. //! - [`MastForestWireView::new`]: trusted wire-backed cache access; rejects hashless and legacy -//! debug-bearing payloads. +//! debug-bearing payloads, and rejects sparse payloads. +//! - [`crate::mast::SparseMastForest::read_from_bytes`] / +//! [`crate::mast::SparseMastForest::read_from_bytes_with_options`]: sparse replay payloads for +//! serialized trace-generation inputs. //! - [`crate::mast::UntrustedMastForest::read_from_bytes`] / //! [`crate::mast::UntrustedMastForest::read_from_bytes_with_options`]: untrusted parsing plus //! later validation before use. @@ -112,6 +116,9 @@ mod layout; pub(super) use layout::ForestLayout; use layout::{OffsetTrackingReader, TrackingReader, WireFlags, read_header_and_scan_layout}; +mod sparse; +pub use sparse::SparseMastForestReadOptions; + mod resolved; use resolved::{ResolvedSerializedForest, basic_block_offset_for_node_index}; @@ -171,10 +178,16 @@ const MAGIC: &[u8; 4] = b"MAST"; /// from local structure. pub(super) const FLAG_HASHLESS: u8 = 0x02; +/// Flag indicating that the payload uses sparse MAST replay serialization. +/// +/// Sparse payloads preserve the source forest's [`MastNodeId`] space and therefore cannot be read +/// through dense [`MastForest`] entry points. +pub(super) const FLAG_SPARSE: u8 = 0x04; + /// Mask for reserved flag bits that must be zero. /// -/// Bit 0 and bits 2-7 are reserved for future use. If any are set, deserialization fails. -const FLAGS_RESERVED_MASK: u8 = 0xfd; +/// Bit 0 and bits 3-7 are reserved for future use. If any are set, deserialization fails. +const FLAGS_RESERVED_MASK: u8 = 0xf9; /// The format version. /// @@ -201,13 +214,15 @@ const FLAGS_RESERVED_MASK: u8 = 0xfd; /// records. MAST nodes are metadata-free identifiers. Before any public release on this branch, /// the same unreleased wire version also reserved bit 0 and stopped using it as a forest-level /// debug-presence flag. +/// - [0, 0, 5]: Added SPARSE flag (bit 2). Sparse payloads preserve sparse replay IDs and are +/// accepted only by SparseMastForest readers. /// /// Legacy wire versions (pre-#3192 decorator terminology): /// [0,0,1] stored metadata as serialized decorator variants in CSR per-node slots. /// [0,0,2] removed AssemblyOp from the decorator enum and stored them separately in DebugInfo. /// [0,0,3] removed the unused decorator-count wire field. /// [0,0,4] eliminated the decorator wire slots entirely. -const VERSION: [u8; 3] = [0, 0, 4]; +const VERSION: [u8; 3] = [0, 0, 5]; // MAST FOREST SERIALIZATION/DESERIALIZATION // ================================================================================================ diff --git a/core/src/mast/serialization/seed_gen.rs b/core/src/mast/serialization/seed_gen.rs index d991d18abe..0397b6bc9b 100644 --- a/core/src/mast/serialization/seed_gen.rs +++ b/core/src/mast/serialization/seed_gen.rs @@ -5,11 +5,15 @@ use alloc::{sync::Arc, vec::Vec}; use std::println; +use super::{FLAG_HASHLESS, FLAG_SPARSE, MAGIC, VERSION}; use crate::{ Felt, Word, advice::{AdviceInputs, AdviceMap}, events::EventId, - mast::{BasicBlockNodeBuilder, JoinNodeBuilder, MastForest, MastForestContributor}, + mast::{ + BasicBlockNodeBuilder, ExternalNodeBuilder, JoinNodeBuilder, MastForest, + MastForestContributor, SparseMastForestBuilder, SplitNodeBuilder, VisitKind, + }, operations::Operation, precompile::PrecompileRequest, program::{Kernel, Program, StackInputs, StackOutputs}, @@ -125,6 +129,89 @@ fn generate_fuzz_seeds() { ); } + // Sparse MAST seeds. + { + let sparse_targets = &["sparse_mast_forest_deserialize", "sparse_mast_forest_validate"]; + + let mut forest = MastForest::new(); + let block_id = BasicBlockNodeBuilder::new(vec![Operation::Add]) + .add_to_forest(&mut forest) + .unwrap(); + forest.make_root(block_id); + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(block_id, VisitKind::FullVisit); + write_mast_seed(sparse_targets, "sparse_basic_block.bin", &builder.finalize().to_bytes()); + + let mut forest = MastForest::new(); + let true_branch = BasicBlockNodeBuilder::new(vec![Operation::Add]) + .add_to_forest(&mut forest) + .unwrap(); + let false_branch = BasicBlockNodeBuilder::new(vec![Operation::Mul]) + .add_to_forest(&mut forest) + .unwrap(); + let root = SplitNodeBuilder::new([true_branch, false_branch]) + .add_to_forest(&mut forest) + .unwrap(); + forest.make_root(root); + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(true_branch, VisitKind::FullVisit); + builder.record_visit(false_branch, VisitKind::DigestOnly); + builder.record_visit(root, VisitKind::FullVisit); + write_mast_seed( + sparse_targets, + "sparse_split_digest_only_child.bin", + &builder.finalize().to_bytes(), + ); + + let mut forest = MastForest::new(); + let left = BasicBlockNodeBuilder::new(vec![Operation::Add]) + .add_to_forest(&mut forest) + .unwrap(); + let right = BasicBlockNodeBuilder::new(vec![Operation::Mul]) + .add_to_forest(&mut forest) + .unwrap(); + let root = JoinNodeBuilder::new([left, right]).add_to_forest(&mut forest).unwrap(); + forest.make_root(root); + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(left, VisitKind::DigestOnly); + builder.record_visit(right, VisitKind::DigestOnly); + builder.record_visit(root, VisitKind::FullVisit); + write_mast_seed( + sparse_targets, + "sparse_join_digest_only_children.bin", + &builder.finalize().to_bytes(), + ); + + let mut forest = MastForest::new(); + let external_digest = Word::new([ + Felt::new_unchecked(11), + Felt::new_unchecked(12), + Felt::new_unchecked(13), + Felt::new_unchecked(14), + ]); + let external = + ExternalNodeBuilder::new(external_digest).add_to_forest(&mut forest).unwrap(); + forest.make_root(external); + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(external, VisitKind::FullVisit); + write_mast_seed( + sparse_targets, + "sparse_external_full_node.bin", + &builder.finalize().to_bytes(), + ); + + let mut invalid_sparse_header = Vec::new(); + invalid_sparse_header.write_bytes(MAGIC); + invalid_sparse_header.write_u8(FLAG_HASHLESS | FLAG_SPARSE); + invalid_sparse_header.write_bytes(&VERSION); + invalid_sparse_header.write_usize(usize::MAX); + write_mast_seed(sparse_targets, "invalid_sparse_header.bin", &invalid_sparse_header); + } + // Seed 5: Empty header (just magic + flags + version + minimal counts) { let bytes: &[u8] = b"MAST\x00\x00\x00\x01"; diff --git a/core/src/mast/serialization/sparse.rs b/core/src/mast/serialization/sparse.rs new file mode 100644 index 0000000000..a296df31fb --- /dev/null +++ b/core/src/mast/serialization/sparse.rs @@ -0,0 +1,384 @@ +use alloc::{format, string::ToString, vec::Vec}; + +use super::{ + FLAG_HASHLESS, FLAG_SPARSE, MAGIC, MastNodeEntry, VERSION, + basic_blocks::{BasicBlockDataBuilder, BasicBlockDataDecoder}, + layout::{OffsetTrackingReader, TrackingReader, WireFlags, read_and_validate_header}, +}; +use crate::{ + Word, + advice::AdviceMap, + mast::{MastForest, MastNode, MastNodeExt, MastNodeId, SparseMastForest}, + serde::{ + BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, + SliceReader, + }, +}; + +const SPARSE_FLAGS: u8 = FLAG_HASHLESS | FLAG_SPARSE; + +fn sparse_mast_forest_min_serialized_size() -> usize { + MAGIC.len() + + 1 + + VERSION.len() + + usize::min_serialized_size() * 7 + + Word::min_serialized_size() + + usize::min_serialized_size() +} + +/// Serializes a [`SparseMastForest`] in sparse replay form. +pub(super) fn write_sparse_into(forest: &SparseMastForest, target: &mut W) { + let mut basic_block_data_builder = BasicBlockDataBuilder::new(); + let mut full_ids = Vec::with_capacity(forest.nodes().len()); + let mut entries = Vec::with_capacity(forest.nodes().len()); + let mut full_digests = Vec::with_capacity(forest.nodes().len()); + + for (&node_id, node) in forest.nodes() { + let ops_offset = if let MastNode::Block(basic_block) = node { + basic_block_data_builder.encode_basic_block(basic_block) + } else { + 0 + }; + + full_ids.push(node_id); + entries.push(MastNodeEntry::new(node, ops_offset)); + full_digests.push(node.digest()); + } + + let basic_block_data = basic_block_data_builder.finalize(); + let external_full_node_count = + entries.iter().filter(|entry| matches!(entry, MastNodeEntry::External)).count(); + let non_external_count = + entries.iter().filter(|entry| !matches!(entry, MastNodeEntry::External)).count(); + + target.write_bytes(MAGIC); + target.write_u8(SPARSE_FLAGS); + target.write_bytes(&VERSION); + + target.write_usize(forest.procedure_roots().len()); + target.write_usize(forest.num_nodes()); + target.write_usize(full_ids.len()); + target.write_usize(forest.digest_entries().len()); + target.write_usize(external_full_node_count); + target.write_usize(non_external_count); + target.write_usize(basic_block_data.len()); + + for &root in forest.procedure_roots() { + root.0.write_into(target); + } + + forest.commitment().write_into(target); + target.write_bytes(&basic_block_data); + + for id in full_ids { + id.0.write_into(target); + } + + for entry in entries { + entry.write_into(target); + } + + for digest in full_digests { + digest.write_into(target); + } + + for (&id, &digest) in forest.digest_entries() { + id.0.write_into(target); + digest.write_into(target); + } + + forest.advice_map().write_into(target); +} + +impl Serializable for SparseMastForest { + fn write_into(&self, target: &mut W) { + write_sparse_into(self, target); + } +} + +impl Deserializable for SparseMastForest { + fn read_from(source: &mut R) -> Result { + read_sparse_from(source) + } + + fn min_serialized_size() -> usize { + sparse_mast_forest_min_serialized_size() + } + + fn read_from_bytes(bytes: &[u8]) -> Result { + SparseMastForest::read_from_bytes(bytes) + } +} + +pub(super) fn read_sparse_from( + source: &mut R, +) -> Result { + let mut reader = TrackingReader::new(source); + let (raw_flags, _version) = read_and_validate_header(&mut reader)?; + let flags = WireFlags::new(raw_flags); + validate_sparse_flags(flags)?; + + let root_count = read_bounded_count(&mut reader, size_of::(), "procedure root count")?; + let source_node_count = reader.read_usize()?; + if source_node_count > MastForest::MAX_NODES { + return Err(DeserializationError::InvalidValue(format!( + "source node count {source_node_count} exceeds maximum allowed {}", + MastForest::MAX_NODES + ))); + } + + let full_node_count = read_bounded_count( + &mut reader, + MastNodeEntry::SERIALIZED_SIZE + Word::min_serialized_size(), + "full node count", + )?; + let digest_only_count = read_bounded_count( + &mut reader, + size_of::() + Word::min_serialized_size(), + "digest-only node count", + )?; + let external_full_node_count = read_bounded_count( + &mut reader, + MastNodeEntry::SERIALIZED_SIZE, + "external full-node count", + )?; + let non_external_full_node_count = read_bounded_count( + &mut reader, + MastNodeEntry::SERIALIZED_SIZE, + "non-external full-node count", + )?; + let basic_block_data_len = read_bounded_count(&mut reader, 1, "basic-block data length")?; + + let counted_full = external_full_node_count + .checked_add(non_external_full_node_count) + .ok_or_else(|| { + DeserializationError::InvalidValue("full node count overflow".to_string()) + })?; + if counted_full != full_node_count { + return Err(DeserializationError::InvalidValue(format!( + "sparse header full node count {full_node_count} does not match external + non-external count {counted_full}" + ))); + } + + let roots = read_id_section(&mut reader, root_count, source_node_count, "procedure root")?; + let commitment = Word::read_from(&mut reader)?; + let basic_block_data = reader.read_slice(basic_block_data_len)?.to_vec(); + let full_ids = read_id_section(&mut reader, full_node_count, source_node_count, "full node")?; + validate_strictly_increasing_ids(&full_ids, "full node")?; + + let mut entries = Vec::with_capacity(full_node_count); + for _ in 0..full_node_count { + entries.push(MastNodeEntry::read_from(&mut reader)?); + } + + let counted_external = + entries.iter().filter(|entry| matches!(entry, MastNodeEntry::External)).count(); + if counted_external != external_full_node_count { + return Err(DeserializationError::InvalidValue(format!( + "sparse header external full-node count {external_full_node_count} does not match {counted_external} external entries" + ))); + } + + let mut full_digests = Vec::with_capacity(full_node_count); + for _ in 0..full_node_count { + full_digests.push(Word::read_from(&mut reader)?); + } + + let mut digest_entries = Vec::with_capacity(digest_only_count); + for _ in 0..digest_only_count { + let id = read_node_id(&mut reader, source_node_count, "digest-only node")?; + let digest = Word::read_from(&mut reader)?; + digest_entries.push((id, digest)); + } + validate_strictly_increasing_entry_ids(&digest_entries, "digest-only node")?; + + let advice_map = AdviceMap::read_from(&mut reader)?; + let nodes = materialize_sparse_nodes( + &full_ids, + &entries, + &full_digests, + source_node_count, + &basic_block_data, + )?; + + SparseMastForest::from_serialized_parts( + nodes, + digest_entries, + source_node_count, + roots, + advice_map, + commitment, + ) +} + +fn validate_sparse_flags(flags: WireFlags) -> Result<(), DeserializationError> { + if !flags.is_sparse() { + return Err(DeserializationError::InvalidValue( + "SPARSE flag is not set; use MastForest readers for dense input".to_string(), + )); + } + if !flags.is_hashless() { + return Err(DeserializationError::InvalidValue( + "sparse MAST payloads must also set HASHLESS".to_string(), + )); + } + if flags.bits() != SPARSE_FLAGS { + return Err(DeserializationError::InvalidValue(format!( + "invalid sparse MAST flag combination: {:#04x}", + flags.bits() + ))); + } + Ok(()) +} + +fn read_bounded_count( + source: &mut R, + element_size: usize, + label: &str, +) -> Result { + let count = source.read_usize()?; + let max_count = source.max_alloc(element_size); + if count > max_count { + return Err(DeserializationError::InvalidValue(format!( + "{label} {count} exceeds reader allocation bound {max_count} for {element_size}-byte elements" + ))); + } + Ok(count) +} + +fn read_id_section( + source: &mut R, + count: usize, + node_count: usize, + label: &str, +) -> Result, DeserializationError> { + let mut ids = Vec::with_capacity(count); + for _ in 0..count { + ids.push(read_node_id(source, node_count, label)?); + } + Ok(ids) +} + +fn read_node_id( + source: &mut R, + node_count: usize, + label: &str, +) -> Result { + let raw = u32::read_from(source)?; + MastNodeId::from_u32_with_node_count(raw, node_count).map_err(|err| { + DeserializationError::InvalidValue(format!("invalid {label} id {raw}: {err}")) + }) +} + +fn validate_strictly_increasing_ids( + ids: &[MastNodeId], + label: &str, +) -> Result<(), DeserializationError> { + for pair in ids.windows(2) { + if pair[0].0 >= pair[1].0 { + return Err(DeserializationError::InvalidValue(format!( + "{label} ids must be strictly increasing" + ))); + } + } + Ok(()) +} + +fn validate_strictly_increasing_entry_ids( + entries: &[(MastNodeId, Word)], + label: &str, +) -> Result<(), DeserializationError> { + for pair in entries.windows(2) { + if pair[0].0.0 >= pair[1].0.0 { + return Err(DeserializationError::InvalidValue(format!( + "{label} ids must be strictly increasing" + ))); + } + } + Ok(()) +} + +fn materialize_sparse_nodes( + full_ids: &[MastNodeId], + entries: &[MastNodeEntry], + full_digests: &[Word], + source_node_count: usize, + basic_block_data: &[u8], +) -> Result, DeserializationError> { + let basic_block_data_decoder = BasicBlockDataDecoder::new(basic_block_data); + if full_digests.len() != full_ids.len() { + return Err(DeserializationError::InvalidValue(format!( + "sparse full digest count {} does not match full node count {}", + full_digests.len(), + full_ids.len() + ))); + } + + let mut nodes = Vec::with_capacity(entries.len()); + for ((&node_id, &entry), &digest) in full_ids.iter().zip(entries).zip(full_digests) { + let node = entry + .try_into_mast_node_builder(source_node_count, &basic_block_data_decoder, digest)? + .build_linked() + .map_err(|err| { + DeserializationError::InvalidValue(format!( + "failed to build sparse MAST node {}: {err}", + node_id.0 + )) + })?; + nodes.push((node_id, node)); + } + + Ok(nodes) +} + +impl SparseMastForest { + /// Deserializes sparse MAST bytes using default untrusted budgets. + pub fn read_from_bytes(bytes: &[u8]) -> Result { + Self::read_from_bytes_with_options(bytes, SparseMastForestReadOptions::default()) + } + + /// Deserializes sparse MAST bytes using explicit read options. + pub fn read_from_bytes_with_options( + bytes: &[u8], + options: SparseMastForestReadOptions, + ) -> Result { + let wire_byte_budget = options.wire_byte_budget(bytes.len()); + if wire_byte_budget < bytes.len() { + return Err(DeserializationError::InvalidValue( + "SparseMastForest wire byte budget is smaller than payload length".to_string(), + )); + } + let allocation_budget = wire_byte_budget.min(bytes.len().saturating_mul(4)); + let mut reader = BudgetedReader::new(SliceReader::new(bytes), allocation_budget); + let forest = read_sparse_from(&mut reader)?; + if reader.has_more_bytes() { + return Err(DeserializationError::InvalidValue( + "extra bytes after SparseMastForest payload".to_string(), + )); + } + Ok(forest) + } +} + +/// Options for reading a [`SparseMastForest`] from bytes. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SparseMastForestReadOptions { + wire_byte_budget: Option, +} + +impl SparseMastForestReadOptions { + /// Creates options that use the default sparse read budgets. + pub fn new() -> Self { + Self::default() + } + + /// Sets the maximum number of serialized bytes consumed while parsing wire data. + pub fn with_wire_byte_budget(mut self, budget: usize) -> Self { + self.wire_byte_budget = Some(budget); + self + } + + fn wire_byte_budget(self, bytes_len: usize) -> usize { + self.wire_byte_budget.unwrap_or(bytes_len) + } +} diff --git a/core/src/mast/serialization/tests.rs b/core/src/mast/serialization/tests.rs index 0611b7fccd..f1049eded3 100644 --- a/core/src/mast/serialization/tests.rs +++ b/core/src/mast/serialization/tests.rs @@ -1,7 +1,7 @@ use core::assert_matches; use std::{ string::{String, ToString}, - sync::{Mutex, Once}, + sync::{Arc, Mutex, Once}, }; use super::*; @@ -9,10 +9,11 @@ use crate::{ Felt, Word, chiplets::hasher, mast::{ - BasicBlockNodeBuilder, CallNodeBuilder, DynNodeBuilder, ExternalNodeBuilder, - JoinNodeBuilder, LoopNodeBuilder, MastForestContributor, MastForestError, MastForestView, - MastNodeExt, MastNodeId, OP_BATCH_SIZE, OpBatch, SplitNodeBuilder, UntrustedMastForest, - UntrustedMastForestReadOptions, + BasicBlockNodeBuilder, CallNodeBuilder, DynNodeBuilder, ExecutableMastForest, + ExternalNodeBuilder, JoinNodeBuilder, LoopNodeBuilder, MastForestContributor, + MastForestError, MastForestView, MastNodeExt, MastNodeId, OP_BATCH_SIZE, OpBatch, + SparseMastForest, SparseMastForestBuilder, SparseMastForestReadOptions, SplitNodeBuilder, + UntrustedMastForest, UntrustedMastForestReadOptions, VisitKind, }, operations::Operation, serde::{ByteReader, Deserializable, DeserializationError, Serializable, SliceReader}, @@ -679,6 +680,354 @@ fn test_untrusted_hashless_keeps_external_digests_by_node_index() { assert_eq!(restored[low_id].digest(), external_low); } +fn sparse_split_fixture() -> (Arc, SparseMastForest, MastNodeId, MastNodeId, MastNodeId) +{ + let mut forest = MastForest::new(); + let true_branch = BasicBlockNodeBuilder::new(vec![Operation::Add]) + .add_to_forest(&mut forest) + .unwrap(); + let false_branch = BasicBlockNodeBuilder::new(vec![Operation::Mul]) + .add_to_forest(&mut forest) + .unwrap(); + let root = SplitNodeBuilder::new([true_branch, false_branch]) + .add_to_forest(&mut forest) + .unwrap(); + forest.make_root(root); + + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(true_branch, VisitKind::FullVisit); + builder.record_visit(false_branch, VisitKind::DigestOnly); + builder.record_visit(root, VisitKind::FullVisit); + let sparse = builder.finalize(); + + (forest, sparse, true_branch, false_branch, root) +} + +#[test] +fn sparse_mast_round_trip_preserves_sparse_replay_ids() { + let (source, sparse, true_branch, false_branch, root) = sparse_split_fixture(); + + let bytes = sparse.to_bytes(); + let restored = SparseMastForest::read_from_bytes(&bytes).unwrap(); + + assert_eq!(restored.num_nodes(), source.num_nodes() as usize); + assert_eq!(restored.procedure_roots(), &[root]); + assert_eq!(restored.commitment(), source.commitment()); + assert_eq!( + restored.get_node_by_id(true_branch).unwrap().digest(), + source[true_branch].digest() + ); + assert_eq!(restored.get_node_by_id(root).unwrap().digest(), source[root].digest()); + assert!(restored.get_node_by_id(false_branch).is_none()); + assert_eq!(restored.get_digest_by_id(true_branch), Some(source[true_branch].digest())); + assert_eq!(restored.get_digest_by_id(false_branch), Some(source[false_branch].digest())); + assert_eq!(restored.get_digest_by_id(root), Some(source[root].digest())); +} + +fn write_sparse_test_payload( + source_node_count: usize, + roots: &[MastNodeId], + full_ids: &[MastNodeId], + entries: &[MastNodeEntry], + full_digests: &[Word], + basic_block_data: &[u8], + commitment: Word, +) -> Vec { + assert_eq!(full_ids.len(), entries.len()); + assert_eq!(full_ids.len(), full_digests.len()); + + let mut bytes = Vec::new(); + bytes.write_bytes(MAGIC); + bytes.write_u8(FLAG_HASHLESS | FLAG_SPARSE); + bytes.write_bytes(&VERSION); + + bytes.write_usize(roots.len()); + bytes.write_usize(source_node_count); + bytes.write_usize(full_ids.len()); + bytes.write_usize(0); + bytes.write_usize( + entries.iter().filter(|entry| matches!(entry, MastNodeEntry::External)).count(), + ); + bytes.write_usize(full_ids.len()); + bytes.write_usize(basic_block_data.len()); + + for root in roots { + root.0.write_into(&mut bytes); + } + commitment.write_into(&mut bytes); + bytes.write_bytes(basic_block_data); + for id in full_ids { + id.0.write_into(&mut bytes); + } + for entry in entries { + entry.write_into(&mut bytes); + } + for digest in full_digests { + digest.write_into(&mut bytes); + } + AdviceMap::default().write_into(&mut bytes); + bytes +} + +#[test] +fn sparse_reader_allows_large_source_node_count_with_small_payload() { + let block = BasicBlockNodeBuilder::new(vec![Operation::Add]).build().unwrap(); + let mut basic_block_data = BasicBlockDataBuilder::new(); + let block_offset = basic_block_data.encode_basic_block(&block); + let basic_block_data = basic_block_data.finalize(); + + let root = MastNodeId::from(0); + let bytes = write_sparse_test_payload( + MastForest::MAX_NODES, + &[root], + &[root], + &[MastNodeEntry::Block { ops_offset: block_offset }], + &[block.digest()], + &basic_block_data, + block.digest(), + ); + + let restored = SparseMastForest::read_from_bytes(&bytes).unwrap(); + assert_eq!(restored.num_nodes(), MastForest::MAX_NODES); + assert_eq!(restored.get_digest_by_id(root), Some(block.digest())); +} + +#[test] +fn sparse_reader_reconstructs_forward_full_child_digests() { + let left_block = BasicBlockNodeBuilder::new(vec![Operation::Add]).build().unwrap(); + let right_block = BasicBlockNodeBuilder::new(vec![Operation::Mul]).build().unwrap(); + + let mut basic_block_data = BasicBlockDataBuilder::new(); + let left_offset = basic_block_data.encode_basic_block(&left_block); + let right_offset = basic_block_data.encode_basic_block(&right_block); + let basic_block_data = basic_block_data.finalize(); + + let root = MastNodeId::from(0); + let left = MastNodeId::from(1); + let right = MastNodeId::from(2); + let expected_root_digest = hasher::merge_in_domain( + &[left_block.digest(), right_block.digest()], + crate::mast::JoinNode::DOMAIN, + ); + let bytes = write_sparse_test_payload( + 3, + &[root], + &[root, left, right], + &[ + MastNodeEntry::Join { + left_child_id: left.0, + right_child_id: right.0, + }, + MastNodeEntry::Block { ops_offset: left_offset }, + MastNodeEntry::Block { ops_offset: right_offset }, + ], + &[expected_root_digest, left_block.digest(), right_block.digest()], + &basic_block_data, + expected_root_digest, + ); + + let restored = SparseMastForest::read_from_bytes(&bytes).unwrap(); + assert_eq!(restored.get_digest_by_id(root), Some(expected_root_digest)); + assert_eq!(restored.get_digest_by_id(left), Some(left_block.digest())); + assert_eq!(restored.get_digest_by_id(right), Some(right_block.digest())); +} + +#[test] +fn sparse_reader_preserves_forced_full_node_digest() { + let child_block = BasicBlockNodeBuilder::new(vec![Operation::Add]).build().unwrap(); + let mut basic_block_data = BasicBlockDataBuilder::new(); + let child_offset = basic_block_data.encode_basic_block(&child_block); + let basic_block_data = basic_block_data.finalize(); + + let root = MastNodeId::from(0); + let child = MastNodeId::from(1); + let canonical_root_digest = hasher::merge_in_domain( + &[child_block.digest(), Word::default()], + crate::mast::CallNode::CALL_DOMAIN, + ); + let forced_root_digest = Word::new([ + Felt::from(101_u32), + Felt::from(102_u32), + Felt::from(103_u32), + Felt::from(104_u32), + ]); + assert_ne!(forced_root_digest, canonical_root_digest); + + let bytes = write_sparse_test_payload( + 2, + &[root], + &[root, child], + &[ + MastNodeEntry::Call { callee_id: child.0 }, + MastNodeEntry::Block { ops_offset: child_offset }, + ], + &[forced_root_digest, child_block.digest()], + &basic_block_data, + forced_root_digest, + ); + + let restored = SparseMastForest::read_from_bytes(&bytes).unwrap(); + assert_eq!(restored.get_digest_by_id(root), Some(forced_root_digest)); + assert_eq!(restored.commitment(), forced_root_digest); +} + +#[test] +fn sparse_reader_reconstructs_deep_forward_full_child_chain() { + const CHAIN_LEN: usize = 4096; + + let block = BasicBlockNodeBuilder::new(vec![Operation::Add]).build().unwrap(); + let mut basic_block_data = BasicBlockDataBuilder::new(); + let block_offset = basic_block_data.encode_basic_block(&block); + let basic_block_data = basic_block_data.finalize(); + + let full_ids: Vec<_> = (0..CHAIN_LEN).map(|id| MastNodeId::from(id as u32)).collect(); + let mut entries = Vec::with_capacity(CHAIN_LEN); + for id in 0..CHAIN_LEN - 1 { + entries.push(MastNodeEntry::Call { callee_id: (id + 1) as u32 }); + } + entries.push(MastNodeEntry::Block { ops_offset: block_offset }); + + let mut full_digests = vec![Word::default(); CHAIN_LEN]; + full_digests[CHAIN_LEN - 1] = block.digest(); + for id in (0..CHAIN_LEN - 1).rev() { + full_digests[id] = hasher::merge_in_domain( + &[full_digests[id + 1], Word::default()], + crate::mast::CallNode::CALL_DOMAIN, + ); + } + let expected_root_digest = full_digests[0]; + + let root = MastNodeId::from(0); + let bytes = write_sparse_test_payload( + CHAIN_LEN, + &[root], + &full_ids, + &entries, + &full_digests, + &basic_block_data, + expected_root_digest, + ); + + let restored = SparseMastForest::read_from_bytes(&bytes).unwrap(); + assert_eq!(restored.get_digest_by_id(root), Some(expected_root_digest)); +} + +#[test] +fn sparse_reader_rejects_trailing_bytes_with_exact_prefix_budget() { + let block = BasicBlockNodeBuilder::new(vec![Operation::Add]).build().unwrap(); + let mut basic_block_data = BasicBlockDataBuilder::new(); + let block_offset = basic_block_data.encode_basic_block(&block); + let basic_block_data = basic_block_data.finalize(); + + let root = MastNodeId::from(0); + let bytes = write_sparse_test_payload( + 1, + &[root], + &[root], + &[MastNodeEntry::Block { ops_offset: block_offset }], + &[block.digest()], + &basic_block_data, + block.digest(), + ); + + let mut bytes_with_trailing = bytes.clone(); + bytes_with_trailing.push(0); + let err = SparseMastForest::read_from_bytes_with_options( + &bytes_with_trailing, + SparseMastForestReadOptions::new().with_wire_byte_budget(bytes.len()), + ) + .unwrap_err(); + assert!(err.to_string().contains("budget is smaller than payload length")); +} + +#[test] +fn dense_mast_readers_reject_sparse_payloads() { + let (_source, sparse, _true_branch, _false_branch, _root) = sparse_split_fixture(); + let bytes = sparse.to_bytes(); + + let materialized = MastForest::read_from_bytes(&bytes); + assert_matches!( + materialized, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("SPARSE flag is set") + ); + + let wire_view = MastForestWireView::new(&bytes); + assert_matches!( + wire_view, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("SPARSE flag is set") + ); + + let untrusted = UntrustedMastForest::read_from_bytes(&bytes); + assert_matches!( + untrusted, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("SPARSE flag is set") + ); +} + +#[test] +fn sparse_reader_rejects_dense_payloads() { + let mut forest = MastForest::new(); + let root = BasicBlockNodeBuilder::new(vec![Operation::Add]) + .add_to_forest(&mut forest) + .unwrap(); + forest.make_root(root); + + let result = SparseMastForest::read_from_bytes(&forest.to_bytes()); + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("SPARSE flag is not set") + ); +} + +#[test] +fn sparse_serialized_parts_reject_missing_child_digest() { + let (_source, sparse, _true_branch, false_branch, _root) = sparse_split_fixture(); + let nodes = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + let digests = sparse + .digest_entries() + .iter() + .filter_map(|(&id, &digest)| (id != false_branch).then_some((id, digest))) + .collect(); + + let result = SparseMastForest::from_serialized_parts( + nodes, + digests, + sparse.num_nodes(), + sparse.procedure_roots().to_vec(), + sparse.advice_map().clone(), + sparse.commitment(), + ); + + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("without a full node or digest-only entry") + ); +} + +#[test] +fn sparse_serialized_parts_reject_duplicate_full_ids() { + let (_source, sparse, true_branch, _false_branch, _root) = sparse_split_fixture(); + let mut nodes: Vec<_> = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + let duplicate_node = sparse.get_node_by_id(true_branch).unwrap().clone(); + nodes.push((true_branch, duplicate_node)); + let digests = sparse.digest_entries().iter().map(|(&id, &digest)| (id, digest)).collect(); + + let result = SparseMastForest::from_serialized_parts( + nodes, + digests, + sparse.num_nodes(), + sparse.procedure_roots().to_vec(), + sparse.advice_map().clone(), + sparse.commitment(), + ); + + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("duplicate sparse full-node id") + ); +} + /// Test that a forest with a node whose child ids are larger than its own id serializes and /// deserializes successfully. #[test] @@ -1038,7 +1387,7 @@ fn test_batched_construction_preserves_structure() { fn assert_header_flags(bytes: &[u8], expected_flags: u8) { assert_eq!(&bytes[0..4], b"MAST", "Magic should be MAST"); assert_eq!(bytes[4], expected_flags, "unexpected serialization flags"); - assert_eq!(&bytes[5..8], &[0, 0, 4], "Version should be [0, 0, 4]"); + assert_eq!(&bytes[5..8], &[0, 0, 5], "Version should be [0, 0, 5]"); } fn read_header_counts(bytes: &[u8]) -> (usize, usize) { @@ -1179,7 +1528,7 @@ fn test_deserialize_rejects_unknown_flags() { let bytes = forest.to_bytes(); - for flag in [0x01, 0x04] { + for flag in [0x01, 0x08] { let mut bytes = bytes.clone(); bytes[4] = flag; diff --git a/core/src/mast/sparse.rs b/core/src/mast/sparse.rs index 9ceeafda46..c555f5ea9b 100644 --- a/core/src/mast/sparse.rs +++ b/core/src/mast/sparse.rs @@ -10,6 +10,7 @@ use crate::{ Word, advice::AdviceMap, mast::{ExecutableMastForest, MastForest, MastNode, MastNodeExt, MastNodeId}, + utils::Idx, }; // MAST FOREST ID @@ -21,6 +22,24 @@ use crate::{ // `MastNodeId`, which is meaningful only within one forest's node store. newtype_id!(MastForestId); +impl crate::serde::Serializable for MastForestId { + fn write_into(&self, target: &mut W) { + crate::serde::Serializable::write_into(&u32::from(*self), target); + } +} + +impl crate::serde::Deserializable for MastForestId { + fn read_from( + source: &mut R, + ) -> Result { + Ok(Self::from(::read_from(source)?)) + } + + fn min_serialized_size() -> usize { + ::min_serialized_size() + } +} + // SPARSE MAST FOREST // ================================================================================================ @@ -90,6 +109,11 @@ impl SparseMastForest { &self.advice_map } + /// Returns the digest-only entries associated with this sparse forest. + pub(in crate::mast) fn digest_entries(&self) -> &BTreeMap { + &self.digests + } + /// Returns the commitment to this sparse forest, computed from the procedure roots. /// /// The commitment value is derived from the digests of the procedure roots in the original @@ -98,6 +122,157 @@ impl SparseMastForest { pub fn commitment(&self) -> Word { self.commitment_cache } + + /// Builds a sparse forest from parts decoded from the sparse wire format. + pub(in crate::mast) fn from_serialized_parts( + nodes: Vec<(MastNodeId, MastNode)>, + digests: Vec<(MastNodeId, Word)>, + num_nodes: usize, + roots: Vec, + advice_map: AdviceMap, + commitment_cache: Word, + ) -> Result { + validate_sparse_node_bound(num_nodes)?; + + let nodes = collect_unique_nodes(nodes, num_nodes)?; + let digests = collect_unique_digests(digests, num_nodes)?; + + for &root in &roots { + validate_sparse_id(root, num_nodes, "procedure root")?; + } + + for node_id in nodes.keys() { + if digests.contains_key(node_id) { + return Err(crate::serde::DeserializationError::InvalidValue(format!( + "sparse full-node id {} overlaps a digest-only entry", + node_id.0 + ))); + } + } + + validate_full_node_child_digests(&nodes, &digests, num_nodes)?; + + Ok(Self { + nodes, + digests, + num_nodes, + roots, + advice_map, + commitment_cache, + }) + } +} + +fn validate_sparse_node_bound(num_nodes: usize) -> Result<(), crate::serde::DeserializationError> { + if num_nodes > MastForest::MAX_NODES { + return Err(crate::serde::DeserializationError::InvalidValue(format!( + "sparse source node count {num_nodes} exceeds maximum allowed {}", + MastForest::MAX_NODES + ))); + } + Ok(()) +} + +fn validate_sparse_id( + id: MastNodeId, + num_nodes: usize, + label: &str, +) -> Result<(), crate::serde::DeserializationError> { + if id.to_usize() >= num_nodes { + return Err(crate::serde::DeserializationError::InvalidValue(format!( + "{label} id {} is out of range for sparse source node count {num_nodes}", + id.0 + ))); + } + Ok(()) +} + +fn collect_unique_nodes( + nodes: Vec<(MastNodeId, MastNode)>, + num_nodes: usize, +) -> Result, crate::serde::DeserializationError> { + let mut result = BTreeMap::new(); + for (id, node) in nodes { + validate_sparse_id(id, num_nodes, "full node")?; + if result.insert(id, node).is_some() { + return Err(crate::serde::DeserializationError::InvalidValue(format!( + "duplicate sparse full-node id {}", + id.0 + ))); + } + } + Ok(result) +} + +fn collect_unique_digests( + digests: Vec<(MastNodeId, Word)>, + num_nodes: usize, +) -> Result, crate::serde::DeserializationError> { + let mut result = BTreeMap::new(); + for (id, digest) in digests { + validate_sparse_id(id, num_nodes, "digest-only node")?; + if result.insert(id, digest).is_some() { + return Err(crate::serde::DeserializationError::InvalidValue(format!( + "duplicate sparse digest-only id {}", + id.0 + ))); + } + } + Ok(result) +} + +fn validate_full_node_child_digests( + nodes: &BTreeMap, + digests: &BTreeMap, + num_nodes: usize, +) -> Result<(), crate::serde::DeserializationError> { + for (&node_id, node) in nodes { + validate_sparse_id(node_id, num_nodes, "full node")?; + + match node { + MastNode::Block(block) => { + block.validate_batch_invariants().map_err(|error_msg| { + crate::serde::DeserializationError::InvalidValue(format!( + "invalid sparse basic block {}: {error_msg}", + node_id.0 + )) + })?; + }, + MastNode::External(_) | MastNode::Dyn(_) => {}, + MastNode::Join(join) => { + require_child_digest(node_id, join.first(), nodes, digests, num_nodes)?; + require_child_digest(node_id, join.second(), nodes, digests, num_nodes)?; + }, + MastNode::Split(split) => { + require_child_digest(node_id, split.on_true(), nodes, digests, num_nodes)?; + require_child_digest(node_id, split.on_false(), nodes, digests, num_nodes)?; + }, + MastNode::Loop(loop_node) => { + require_child_digest(node_id, loop_node.body(), nodes, digests, num_nodes)?; + }, + MastNode::Call(call) => { + require_child_digest(node_id, call.callee(), nodes, digests, num_nodes)?; + }, + } + } + Ok(()) +} + +fn require_child_digest( + parent_id: MastNodeId, + child_id: MastNodeId, + nodes: &BTreeMap, + digests: &BTreeMap, + num_nodes: usize, +) -> Result<(), crate::serde::DeserializationError> { + validate_sparse_id(child_id, num_nodes, "child")?; + if !nodes.contains_key(&child_id) && !digests.contains_key(&child_id) { + return Err(crate::serde::DeserializationError::InvalidValue(format!( + "sparse full node {} references child {} without a full node or digest-only entry", + parent_id.0, child_id.0 + ))); + } + Ok(()) } impl ExecutableMastForest for SparseMastForest { diff --git a/core/src/precompile.rs b/core/src/precompile.rs index b7b8f09d05..1a963d3672 100644 --- a/core/src/precompile.rs +++ b/core/src/precompile.rs @@ -366,6 +366,18 @@ impl PrecompileTranscript { } } +impl Serializable for PrecompileTranscript { + fn write_into(&self, target: &mut W) { + self.state.write_into(target); + } +} + +impl Deserializable for PrecompileTranscript { + fn read_from(source: &mut R) -> Result { + Ok(Self::from_state(Word::read_from(source)?)) + } +} + // PRECOMPILE ERROR // ================================================================================================ diff --git a/miden-core-fuzz/Cargo.lock b/miden-core-fuzz/Cargo.lock index 857628c125..028444ef02 100644 --- a/miden-core-fuzz/Cargo.lock +++ b/miden-core-fuzz/Cargo.lock @@ -178,6 +178,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.4" @@ -706,6 +731,28 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "miden-ace-codegen" +version = "0.24.0" +dependencies = [ + "miden-core", + "miden-crypto", + "thiserror", +] + +[[package]] +name = "miden-air" +version = "0.24.0" +dependencies = [ + "miden-ace-codegen", + "miden-core", + "miden-crypto", + "miden-utils-indexing", + "proptest", + "thiserror", + "tracing", +] + [[package]] name = "miden-assembly" version = "0.25.0" @@ -777,6 +824,7 @@ dependencies = [ "miden-mast-package", "miden-package-registry", "miden-project", + "miden-prover", "serde_json", "toml", ] @@ -976,6 +1024,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "miden-processor" +version = "0.24.0" +dependencies = [ + "itertools", + "miden-air", + "miden-core", + "miden-debug-types", + "miden-mast-package", + "miden-utils-diagnostics", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", + "tracing", +] + [[package]] name = "miden-project" version = "0.25.0" @@ -991,6 +1056,20 @@ dependencies = [ "toml", ] +[[package]] +name = "miden-prover" +version = "0.24.0" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "miden-processor", + "serde", + "serde-wincode", + "tracing", + "wincode", +] + [[package]] name = "miden-rowan" version = "0.16.4" @@ -1416,6 +1495,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1617,6 +1702,26 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1768,6 +1873,17 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-wincode" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a62dfa6ae65cb073dfe001fb076eaf827fd4267fcba7eb3be2fee4f1e69ccb5" +dependencies = [ + "serde", + "thiserror", + "wincode", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2254,6 +2370,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "wincode" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657690780ce23e6f66576a782ffd88eb353512381817029cc1d7a99154bb6d1f" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/miden-core-fuzz/Cargo.toml b/miden-core-fuzz/Cargo.toml index e8f826dfde..0fb1dc0575 100644 --- a/miden-core-fuzz/Cargo.toml +++ b/miden-core-fuzz/Cargo.toml @@ -39,6 +39,9 @@ features = ["std", "serde", "resolver"] path = "../crates/project" features = ["std", "serde"] +[dependencies.miden-prover] +path = "../prover" + [dependencies.toml] version = "1.0" features = ["parse", "display", "serde"] @@ -58,6 +61,27 @@ test = false doc = false bench = false +[[bin]] +name = "sparse_mast_forest_deserialize" +path = "fuzz_targets/sparse_mast_forest_deserialize.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "sparse_mast_forest_validate" +path = "fuzz_targets/sparse_mast_forest_validate.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "trace_proving_inputs_deserialize" +path = "fuzz_targets/trace_proving_inputs_deserialize.rs" +test = false +doc = false +bench = false + [[bin]] name = "mast_node_info" path = "fuzz_targets/mast_node_info.rs" diff --git a/miden-core-fuzz/fuzz_targets/sparse_mast_forest_deserialize.rs b/miden-core-fuzz/fuzz_targets/sparse_mast_forest_deserialize.rs new file mode 100644 index 0000000000..20eaf52b43 --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/sparse_mast_forest_deserialize.rs @@ -0,0 +1,16 @@ +//! Fuzz target for SparseMastForest deserialization. +//! +//! Run with: cargo +nightly fuzz run sparse_mast_forest_deserialize --fuzz-dir miden-core-fuzz + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use miden_core::{mast::SparseMastForest, serde::Deserializable}; + +fuzz_target!(|data: &[u8]| { + let budget = data.len().saturating_mul(64); + + let _ = SparseMastForest::read_from_bytes(data); + let _ = Vec::::read_from_bytes_with_budget(data, budget); + let _ = Option::::read_from_bytes_with_budget(data, budget); +}); diff --git a/miden-core-fuzz/fuzz_targets/sparse_mast_forest_validate.rs b/miden-core-fuzz/fuzz_targets/sparse_mast_forest_validate.rs new file mode 100644 index 0000000000..98e3ba872a --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/sparse_mast_forest_validate.rs @@ -0,0 +1,18 @@ +//! Fuzz target for sparse MAST deserialization with explicit budgets. +//! +//! Run with: cargo +nightly fuzz run sparse_mast_forest_validate --fuzz-dir miden-core-fuzz + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use miden_core::mast::{SparseMastForest, SparseMastForestReadOptions}; + +fuzz_target!(|data: &[u8]| { + let small_budget_options = SparseMastForestReadOptions::new().with_wire_byte_budget(64); + let explicit_budget_options = + SparseMastForestReadOptions::new().with_wire_byte_budget(data.len()); + + let _ = SparseMastForest::read_from_bytes(data); + let _ = SparseMastForest::read_from_bytes_with_options(data, small_budget_options); + let _ = SparseMastForest::read_from_bytes_with_options(data, explicit_budget_options); +}); diff --git a/miden-core-fuzz/fuzz_targets/trace_proving_inputs_deserialize.rs b/miden-core-fuzz/fuzz_targets/trace_proving_inputs_deserialize.rs new file mode 100644 index 0000000000..1dffa70db8 --- /dev/null +++ b/miden-core-fuzz/fuzz_targets/trace_proving_inputs_deserialize.rs @@ -0,0 +1,18 @@ +//! Fuzz target for TraceProvingInputs deserialization. +//! +//! This target feeds arbitrary byte sequences to the bounded remote proving input reader. It should +//! reject malformed inputs with errors rather than panicking. +//! +//! Run with: cargo +nightly fuzz run trace_proving_inputs_deserialize --fuzz-dir miden-core-fuzz + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use miden_prover::TraceProvingInputs; + +fuzz_target!(|data: &[u8]| { + let explicit_budget = data.len().saturating_mul(64); + + let _ = TraceProvingInputs::read_from_bytes_with_budget(data, 64); + let _ = TraceProvingInputs::read_from_bytes_with_budget(data, explicit_budget); +}); diff --git a/miden-vm/tests/integration/prove_verify.rs b/miden-vm/tests/integration/prove_verify.rs index 1f35fd177b..cc39512d33 100644 --- a/miden-vm/tests/integration/prove_verify.rs +++ b/miden-vm/tests/integration/prove_verify.rs @@ -223,6 +223,11 @@ mod fast_parallel { use miden_core::{ Felt, Word, events::{EventId, EventName}, + mast::{ + BasicBlockNodeBuilder, ExternalNodeBuilder, JoinNodeBuilder, MastForest, + MastForestContributor, MastNodeExt, + }, + operations::Operation, precompile::{ PrecompileCommitment, PrecompileError, PrecompileRequest, PrecompileTranscript, PrecompileVerifier, PrecompileVerifierRegistry, @@ -231,13 +236,15 @@ mod fast_parallel { }; use miden_core_lib::CoreLibrary; use miden_processor::{ - DefaultHost, ExecutionOptions, FastProcessor, ProcessorState, StackInputs, StackOutputs, + DefaultHost, ExecutionOptions, FastProcessor, HostLibrary, ProcessorState, StackInputs, + StackOutputs, advice::{AdviceInputs, AdviceMutation}, event::{EventError, EventHandler}, trace::build_trace, }; use miden_prover::{ ProvingOptions, TraceProvingInputs, config, prove_from_trace_sync, prove_stark, + serde::{Deserializable, Serializable}, }; use miden_verifier::{verify, verify_with_precompiles}; use miden_vm::{Program, TraceBuildInputs}; @@ -267,6 +274,38 @@ mod fast_parallel { DefaultHost::default().with_source_manager(Arc::new(DefaultSourceManager::default())) } + fn create_simple_library() -> HostLibrary { + let mut mast_forest = MastForest::new(); + let swap_block = BasicBlockNodeBuilder::new(vec![Operation::Swap, Operation::Swap]) + .add_to_forest(&mut mast_forest) + .unwrap(); + mast_forest.make_root(swap_block); + HostLibrary::from(Arc::new(mast_forest)) + } + + fn external_lib_proc_digest() -> Word { + let mut forest = MastForest::new(); + let swap_block = BasicBlockNodeBuilder::new(vec![Operation::Swap, Operation::Swap]) + .add_to_forest(&mut forest) + .unwrap(); + forest.get_node_by_id(swap_block).unwrap().digest() + } + + fn external_program() -> Program { + let mut program = MastForest::new(); + let basic_block = BasicBlockNodeBuilder::new(vec![Operation::Pad, Operation::Drop]) + .add_to_forest(&mut program) + .unwrap(); + let external_node = ExternalNodeBuilder::new(external_lib_proc_digest()) + .add_to_forest(&mut program) + .unwrap(); + let root = JoinNodeBuilder::new([basic_block, external_node]) + .add_to_forest(&mut program) + .unwrap(); + program.make_root(root); + Program::new(Arc::new(program), root) + } + /// Test that proves and verifies using the fast processor + parallel trace generation path. /// This verifies the complete code path works end-to-end. /// @@ -354,6 +393,67 @@ mod fast_parallel { verify(program.into(), stack_inputs, stack_outputs, proof).expect("Verification failed"); } + #[test] + fn test_trace_proving_inputs_round_trip_proves_external_library_program() { + std::thread::Builder::new() + .name("trace-proving-inputs-round-trip".into()) + .stack_size(8 * 1024 * 1024) + .spawn(trace_proving_inputs_round_trip_proves_external_library_program) + .expect("failed to spawn round-trip test thread") + .join() + .expect("round-trip test thread panicked"); + } + + fn trace_proving_inputs_round_trip_proves_external_library_program() { + let program = external_program(); + let stack_inputs = StackInputs::default(); + let advice_inputs = AdviceInputs::default(); + let mut host = default_source_manager_host(); + host.load_library(create_simple_library()) + .expect("failed to load test library into host"); + let trace_inputs = + execute_parallel_trace_inputs(&program, stack_inputs, advice_inputs, &mut host); + + let trace_inputs_bytes = trace_inputs.to_bytes(); + let restored_trace_inputs = TraceBuildInputs::read_from_bytes(&trace_inputs_bytes) + .expect("trace inputs round trip"); + assert!( + restored_trace_inputs.trace_generation_context().mast_forest_store.len() > 1, + "expected dynamic library execution to serialize multiple MAST forests" + ); + let _trace = build_trace(restored_trace_inputs).expect("restored trace inputs build trace"); + + let trace_inputs = TraceBuildInputs::read_from_bytes(&trace_inputs_bytes) + .expect("trace inputs round trip"); + let proving_inputs = TraceProvingInputs::new( + trace_inputs, + ProvingOptions::with_96_bit_security(HashFunction::Blake3_256), + ); + let proving_inputs_bytes = proving_inputs.to_bytes(); + let proving_inputs_budget = + proving_inputs_bytes.len().checked_mul(4).expect("test input budget overflow"); + let mut proving_inputs_with_trailing_byte = proving_inputs_bytes.clone(); + proving_inputs_with_trailing_byte.push(0); + assert!( + TraceProvingInputs::read_from_bytes_with_budget( + &proving_inputs_with_trailing_byte, + proving_inputs_bytes.len(), + ) + .is_err(), + "TraceProvingInputs should reject trailing bytes even when the budget matches the valid prefix" + ); + let restored_proving_inputs = TraceProvingInputs::read_from_bytes_with_budget( + &proving_inputs_bytes, + proving_inputs_budget, + ) + .expect("trace proving inputs round trip"); + + let (stack_outputs, proof) = + prove_from_trace_sync(restored_proving_inputs).expect("prove_from_trace_sync failed"); + + verify(program.into(), stack_inputs, stack_outputs, proof).expect("Verification failed"); + } + #[test] fn test_prove_from_trace_sync_preserves_precompile_requests() { let LoggedPrecompileProofFixture { diff --git a/processor/src/continuation_stack.rs b/processor/src/continuation_stack.rs index a65332f0f6..b7ea4337b0 100644 --- a/processor/src/continuation_stack.rs +++ b/processor/src/continuation_stack.rs @@ -1,6 +1,10 @@ use alloc::{sync::Arc, vec::Vec}; -use miden_core::{mast::MastNodeId, program::Program}; +use miden_core::{ + mast::{MastForestId, MastNodeId}, + program::Program, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; use miden_mast_package::debug_info::{DebugSourceNodeId, PackageDebugInfo}; /// A hint for the initial size of the continuation stack. @@ -341,6 +345,122 @@ impl ContinuationStack { } } +impl ContinuationStack { + pub(crate) fn iter_enter_forest_ids(&self) -> impl Iterator + '_ { + self.stack.iter().filter_map(|continuation| match continuation { + Continuation::EnterForest { forest, .. } => Some(*forest), + _ => None, + }) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for Continuation { + fn write_into(&self, target: &mut W) { + match self { + Self::StartNode(node_id) => { + 0u8.write_into(target); + node_id.write_into(target); + }, + Self::FinishJoin(node_id) => { + 1u8.write_into(target); + node_id.write_into(target); + }, + Self::FinishSplit(node_id) => { + 2u8.write_into(target); + node_id.write_into(target); + }, + Self::FinishLoop(node_id) => { + 3u8.write_into(target); + node_id.write_into(target); + }, + Self::FinishCall(node_id) => { + 4u8.write_into(target); + node_id.write_into(target); + }, + Self::FinishDyn(node_id) => { + 5u8.write_into(target); + node_id.write_into(target); + }, + Self::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => { + 6u8.write_into(target); + node_id.write_into(target); + batch_index.write_into(target); + op_idx_in_batch.write_into(target); + }, + Self::Respan { node_id, batch_index } => { + 7u8.write_into(target); + node_id.write_into(target); + batch_index.write_into(target); + }, + Self::FinishBasicBlock(node_id) => { + 8u8.write_into(target); + node_id.write_into(target); + }, + Self::EnterForest { forest, package_debug_info: _ } => { + 9u8.write_into(target); + forest.write_into(target); + }, + } + } +} + +impl Deserializable for Continuation { + fn read_from(source: &mut R) -> Result { + match u8::read_from(source)? { + 0 => Ok(Self::StartNode(MastNodeId::read_from(source)?)), + 1 => Ok(Self::FinishJoin(MastNodeId::read_from(source)?)), + 2 => Ok(Self::FinishSplit(MastNodeId::read_from(source)?)), + 3 => Ok(Self::FinishLoop(MastNodeId::read_from(source)?)), + 4 => Ok(Self::FinishCall(MastNodeId::read_from(source)?)), + 5 => Ok(Self::FinishDyn(MastNodeId::read_from(source)?)), + 6 => Ok(Self::ResumeBasicBlock { + node_id: MastNodeId::read_from(source)?, + batch_index: usize::read_from(source)?, + op_idx_in_batch: usize::read_from(source)?, + }), + 7 => Ok(Self::Respan { + node_id: MastNodeId::read_from(source)?, + batch_index: usize::read_from(source)?, + }), + 8 => Ok(Self::FinishBasicBlock(MastNodeId::read_from(source)?)), + 9 => Ok(Self::EnterForest { + forest: MastForestId::read_from(source)?, + package_debug_info: None, + }), + tag => { + Err(DeserializationError::InvalidValue(format!("invalid continuation tag {tag}"))) + }, + } + } +} + +impl Serializable for ContinuationStack { + fn write_into(&self, target: &mut W) { + self.stack.write_into(target); + self.source_node_ids.write_into(target); + } +} + +impl Deserializable for ContinuationStack { + fn read_from(source: &mut R) -> Result { + let stack = Vec::>::read_from(source)?; + let source_node_ids = Option::>>::read_from(source)?; + if let Some(source_node_ids) = &source_node_ids + && source_node_ids.len() != stack.len() + { + return Err(DeserializationError::InvalidValue(format!( + "continuation source_node_ids length {} does not match stack length {}", + source_node_ids.len(), + stack.len() + ))); + } + Ok(Self { stack, source_node_ids }) + } +} + // TESTS // ================================================================================================ @@ -409,4 +529,49 @@ mod tests { assert!(matches!(result[1], Continuation::EnterForest { .. })); assert!(matches!(result[2], Continuation::StartNode(_))); } + + #[test] + fn continuation_stack_mast_forest_id_round_trip_omits_package_debug_info() { + let mut stack: ContinuationStack = ContinuationStack::default(); + stack.push_continuation(Continuation::StartNode(MastNodeId::from(1))); + stack.push_continuation(Continuation::EnterForest { + forest: MastForestId::from(2), + package_debug_info: None, + }); + stack.push_continuation(Continuation::ResumeBasicBlock { + node_id: MastNodeId::from(3), + batch_index: 4, + op_idx_in_batch: 5, + }); + stack.source_node_ids = + Some(vec![Some(DebugSourceNodeId::from(10)), None, Some(DebugSourceNodeId::from(11))]); + + let bytes = stack.to_bytes(); + let restored = ContinuationStack::::read_from_bytes(&bytes).unwrap(); + + assert_eq!(restored.stack.len(), 3); + assert!(matches!( + restored.stack[0], + Continuation::StartNode(node_id) if node_id == MastNodeId::from(1) + )); + assert!(matches!( + restored.stack[1], + Continuation::EnterForest { + forest, + package_debug_info: None, + } if forest == MastForestId::from(2) + )); + assert!(matches!( + restored.stack[2], + Continuation::ResumeBasicBlock { + node_id, + batch_index: 4, + op_idx_in_batch: 5, + } if node_id == MastNodeId::from(3) + )); + assert_eq!( + restored.source_node_ids, + Some(vec![Some(DebugSourceNodeId::from(10)), None, Some(DebugSourceNodeId::from(11)),]) + ); + } } diff --git a/processor/src/lib.rs b/processor/src/lib.rs index d30b9da65f..3d0dc6ef2f 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -353,6 +353,24 @@ impl From for Felt { } } +impl serde::Serializable for ContextId { + fn write_into(&self, target: &mut W) { + serde::Serializable::write_into(&self.0, target); + } +} + +impl serde::Deserializable for ContextId { + fn read_from( + source: &mut R, + ) -> Result { + Ok(Self(::read_from(source)?)) + } + + fn min_serialized_size() -> usize { + ::min_serialized_size() + } +} + impl Display for ContextId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) diff --git a/processor/src/trace/chiplets/ace/trace.rs b/processor/src/trace/chiplets/ace/trace.rs index db8bb444bd..8adaac88fc 100644 --- a/processor/src/trace/chiplets/ace/trace.rs +++ b/processor/src/trace/chiplets/ace/trace.rs @@ -8,6 +8,7 @@ use miden_air::{ use miden_core::{ Felt, Word, field::{BasedVectorSpace, QuadFelt}, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, }; use super::{ @@ -289,3 +290,281 @@ impl WireBus { self.wires.len() == self.num_wires as usize } } + +// SERIALIZATION +// ================================================================================================ + +fn write_row_index(row: RowIndex, target: &mut W) { + u32::from(row).write_into(target); +} + +fn read_row_index(source: &mut R) -> Result { + Ok(RowIndex::from(u32::read_from(source)?)) +} + +fn write_quad_felt(value: QuadFelt, target: &mut W) { + let coefficients: &[Felt] = value.as_basis_coefficients_slice(); + coefficients[0].write_into(target); + coefficients[1].write_into(target); +} + +fn read_quad_felt(source: &mut R) -> Result { + let c0 = Felt::read_from(source)?; + let c1 = Felt::read_from(source)?; + Ok(QuadFelt::from_basis_coefficients_fn(|i| [c0, c1][i])) +} + +fn quad_felt_min_serialized_size() -> usize { + Felt::min_serialized_size() * 2 +} + +impl Serializable for ReadNode { + fn write_into(&self, target: &mut W) { + self.ptr.write_into(target); + self.id_0.write_into(target); + write_quad_felt(self.v_0, target); + self.id_1.write_into(target); + write_quad_felt(self.v_1, target); + } +} + +impl Deserializable for ReadNode { + fn read_from(source: &mut R) -> Result { + Ok(Self { + ptr: Felt::read_from(source)?, + id_0: Felt::read_from(source)?, + v_0: read_quad_felt(source)?, + id_1: Felt::read_from(source)?, + v_1: read_quad_felt(source)?, + }) + } + + fn min_serialized_size() -> usize { + Felt::min_serialized_size() * 3 + quad_felt_min_serialized_size() * 2 + } +} + +impl Serializable for EvalNode { + fn write_into(&self, target: &mut W) { + self.ptr.write_into(target); + self.eval_op.write_into(target); + self.id_0.write_into(target); + write_quad_felt(self.v_0, target); + self.id_1.write_into(target); + write_quad_felt(self.v_1, target); + self.id_2.write_into(target); + write_quad_felt(self.v_2, target); + } +} + +impl Deserializable for EvalNode { + fn read_from(source: &mut R) -> Result { + Ok(Self { + ptr: Felt::read_from(source)?, + eval_op: Felt::read_from(source)?, + id_0: Felt::read_from(source)?, + v_0: read_quad_felt(source)?, + id_1: Felt::read_from(source)?, + v_1: read_quad_felt(source)?, + id_2: Felt::read_from(source)?, + v_2: read_quad_felt(source)?, + }) + } + + fn min_serialized_size() -> usize { + Felt::min_serialized_size() * 5 + quad_felt_min_serialized_size() * 3 + } +} + +impl Serializable for WireBus { + fn write_into(&self, target: &mut W) { + self.id_next.write_into(target); + self.wires.len().write_into(target); + for (value, multiplicity) in &self.wires { + write_quad_felt(*value, target); + multiplicity.write_into(target); + } + self.num_wires.write_into(target); + } +} + +impl Deserializable for WireBus { + fn read_from(source: &mut R) -> Result { + let id_next = Felt::read_from(source)?; + let wire_count = usize::read_from(source)?; + let max_count = + source.max_alloc(Felt::min_serialized_size() * 2 + u32::min_serialized_size()); + if wire_count > max_count { + return Err(DeserializationError::InvalidValue(format!( + "ACE wire count {wire_count} exceeds reader allocation bound {max_count}" + ))); + } + + let mut wires = Vec::with_capacity(wire_count); + for _ in 0..wire_count { + wires.push((read_quad_felt(source)?, u32::read_from(source)?)); + } + let num_wires = u32::read_from(source)?; + if num_wires == 0 { + return Err(DeserializationError::InvalidValue( + "ACE wire bus must contain at least one wire".into(), + )); + } + if num_wires > MAX_NUM_ACE_WIRES { + return Err(DeserializationError::InvalidValue(format!( + "ACE declared wire count {num_wires} exceeds maximum {MAX_NUM_ACE_WIRES}" + ))); + } + if wire_count != num_wires as usize { + return Err(DeserializationError::InvalidValue(format!( + "ACE wire count {wire_count} does not match declared wire count {num_wires}" + ))); + } + Ok(Self { id_next, wires, num_wires }) + } + + fn min_serialized_size() -> usize { + Felt::min_serialized_size() + Vec::::min_serialized_size() + u32::min_serialized_size() + } +} + +impl Serializable for CircuitEvaluation { + fn write_into(&self, target: &mut W) { + self.ctx.write_into(target); + write_row_index(self.clk, target); + self.wire_bus.write_into(target); + self.read_nodes.write_into(target); + self.eval_nodes.write_into(target); + } +} + +impl Deserializable for CircuitEvaluation { + fn read_from(source: &mut R) -> Result { + let evaluation = Self { + ctx: ContextId::read_from(source)?, + clk: read_row_index(source)?, + wire_bus: WireBus::read_from(source)?, + read_nodes: Vec::::read_from(source)?, + eval_nodes: Vec::::read_from(source)?, + }; + evaluation.validate_wire_count()?; + Ok(evaluation) + } + + fn min_serialized_size() -> usize { + ContextId::min_serialized_size() + + u32::min_serialized_size() + + WireBus::min_serialized_size() + + Vec::::min_serialized_size() + + Vec::::min_serialized_size() + } +} + +impl CircuitEvaluation { + fn validate_wire_count(&self) -> Result<(), DeserializationError> { + if self.eval_nodes.is_empty() { + return Err(DeserializationError::InvalidValue( + "ACE circuit evaluation must contain at least one eval node".into(), + )); + } + let read_wires = self.read_nodes.len().checked_mul(2).ok_or_else(|| { + DeserializationError::InvalidValue("ACE read-node wire count overflow".into()) + })?; + let expected_wires = read_wires.checked_add(self.eval_nodes.len()).ok_or_else(|| { + DeserializationError::InvalidValue("ACE total wire count overflow".into()) + })?; + + if expected_wires == 0 { + return Err(DeserializationError::InvalidValue( + "ACE circuit evaluation must contain at least one wire".into(), + )); + } + if expected_wires > MAX_NUM_ACE_WIRES as usize { + return Err(DeserializationError::InvalidValue(format!( + "ACE circuit evaluation wire count {expected_wires} exceeds maximum {MAX_NUM_ACE_WIRES}" + ))); + } + if self.wire_bus.num_wires as usize != expected_wires { + return Err(DeserializationError::InvalidValue(format!( + "ACE wire bus count {} does not match read/eval node wire count {expected_wires}", + self.wire_bus.num_wires + ))); + } + + Ok(()) + } +} + +#[cfg(test)] +mod serialization_tests { + use alloc::vec; + + use super::*; + + fn sample_read_node(value: QuadFelt) -> ReadNode { + ReadNode { + ptr: Felt::ZERO, + id_0: Felt::ZERO, + v_0: value, + id_1: Felt::ONE, + v_1: value, + } + } + + fn sample_eval_node(value: QuadFelt) -> EvalNode { + EvalNode { + ptr: Felt::ZERO, + eval_op: Felt::ZERO, + id_0: Felt::ZERO, + v_0: value, + id_1: Felt::ZERO, + v_1: value, + id_2: Felt::ONE, + v_2: value, + } + } + + #[test] + fn circuit_evaluation_read_rejects_mismatched_wire_bus_count() { + let value = QuadFelt::new([Felt::ONE, Felt::ZERO]); + let evaluation = CircuitEvaluation { + ctx: ContextId::from(0), + clk: RowIndex::from(0_u32), + wire_bus: WireBus { + id_next: Felt::ZERO, + wires: vec![(value, 0), (value, 0)], + num_wires: 2, + }, + read_nodes: vec![sample_read_node(value)], + eval_nodes: vec![sample_eval_node(value)], + }; + + let err = CircuitEvaluation::read_from_bytes(&evaluation.to_bytes()).unwrap_err(); + let DeserializationError::InvalidValue(message) = err else { + panic!("expected invalid ACE wire count error"); + }; + assert!(message.contains("does not match read/eval node wire count")); + } + + #[test] + fn circuit_evaluation_read_rejects_empty_eval_section() { + let value = QuadFelt::new([Felt::ONE, Felt::ZERO]); + let evaluation = CircuitEvaluation { + ctx: ContextId::from(0), + clk: RowIndex::from(0_u32), + wire_bus: WireBus { + id_next: Felt::ZERO, + wires: vec![(value, 0), (value, 0)], + num_wires: 2, + }, + read_nodes: vec![sample_read_node(value)], + eval_nodes: Vec::new(), + }; + + let err = CircuitEvaluation::read_from_bytes(&evaluation.to_bytes()).unwrap_err(); + let DeserializationError::InvalidValue(message) = err else { + panic!("expected invalid ACE eval section error"); + }; + assert!(message.contains("at least one eval node")); + } +} diff --git a/processor/src/trace/execution_tracer.rs b/processor/src/trace/execution_tracer.rs index 2ea588c21b..9fdd13e818 100644 --- a/processor/src/trace/execution_tracer.rs +++ b/processor/src/trace/execution_tracer.rs @@ -3,7 +3,11 @@ use alloc::{collections::BTreeMap, sync::Arc, vec::Vec}; use miden_air::trace::chiplets::hasher::{ CONTROLLER_ROWS_PER_PERM_FELT, CONTROLLER_ROWS_PER_PERMUTATION, STATE_WIDTH, }; -use miden_core::{FMP_ADDR, FMP_INIT_VALUE, operations::Operation}; +use miden_core::{ + FMP_ADDR, FMP_INIT_VALUE, + operations::Operation, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; use super::{ block_stack::{BlockInfo, BlockStack, ExecutionContextInfo}, @@ -79,6 +83,136 @@ pub struct TraceGenerationContext { pub max_stack_depth: usize, } +impl Serializable for TraceGenerationContext { + fn write_into(&self, target: &mut W) { + target.write_usize(self.mast_forest_store.len()); + for forest in &self.mast_forest_store { + forest.write_into(target); + } + self.core_trace_contexts.write_into(target); + self.range_checker_replay.write_into(target); + self.memory_writes.write_into(target); + self.bitwise_replay.write_into(target); + self.hasher_for_chiplet.write_into(target); + self.kernel_replay.write_into(target); + self.ace_replay.write_into(target); + self.fragment_size.write_into(target); + self.max_stack_depth.write_into(target); + } +} + +impl Deserializable for TraceGenerationContext { + fn read_from(source: &mut R) -> Result { + let store_len = source.read_usize()?; + let max_store_len = source.max_alloc(SparseMastForest::min_serialized_size()); + if store_len > max_store_len { + return Err(DeserializationError::InvalidValue(format!( + "MAST forest store length {store_len} exceeds reader allocation bound {max_store_len}" + ))); + } + + let mut mast_forest_store = Vec::with_capacity(store_len); + for _ in 0..store_len { + mast_forest_store.push(Arc::new(SparseMastForest::read_from(source)?)); + } + + let context = Self { + mast_forest_store, + core_trace_contexts: Vec::::read_from(source)?, + range_checker_replay: RangeCheckerReplay::read_from(source)?, + memory_writes: MemoryWritesReplay::read_from(source)?, + bitwise_replay: BitwiseReplay::read_from(source)?, + hasher_for_chiplet: HasherRequestReplay::read_from(source)?, + kernel_replay: KernelReplay::read_from(source)?, + ace_replay: AceReplay::read_from(source)?, + fragment_size: usize::read_from(source)?, + max_stack_depth: usize::read_from(source)?, + }; + validate_trace_generation_context_invariants(&context)?; + validate_trace_generation_context_forest_ids(&context)?; + Ok(context) + } +} + +fn validate_trace_generation_context_invariants( + context: &TraceGenerationContext, +) -> Result<(), DeserializationError> { + if context.fragment_size == 0 { + return Err(DeserializationError::InvalidValue( + "trace generation fragment_size must be non-zero".into(), + )); + } + if context.max_stack_depth < MIN_STACK_DEPTH { + return Err(DeserializationError::InvalidValue(format!( + "trace generation max_stack_depth {} is below minimum {MIN_STACK_DEPTH}", + context.max_stack_depth + ))); + } + for (fragment_index, fragment) in context.core_trace_contexts.iter().enumerate() { + let stack_depth = fragment.state.stack.stack_depth(); + if stack_depth > context.max_stack_depth { + return Err(DeserializationError::InvalidValue(format!( + "fragment {fragment_index}: stack depth {stack_depth} exceeds max_stack_depth {}", + context.max_stack_depth + ))); + } + } + Ok(()) +} + +fn validate_trace_generation_context_forest_ids( + context: &TraceGenerationContext, +) -> Result<(), DeserializationError> { + let store_len = context.mast_forest_store.len(); + for (fragment_index, fragment) in context.core_trace_contexts.iter().enumerate() { + validate_mast_forest_id( + fragment.initial_mast_forest_id, + store_len, + "core trace fragment initial_mast_forest_id", + )?; + for forest_id in fragment.continuation.iter_enter_forest_ids() { + validate_mast_forest_id( + forest_id, + store_len, + "core trace fragment continuation EnterForest", + ) + .map_err(|err| { + DeserializationError::InvalidValue(format!("fragment {fragment_index}: {err}")) + })?; + } + for forest_id in fragment.replay.mast_forest_resolution.iter_forest_ids() { + validate_mast_forest_id( + forest_id, + store_len, + "core trace fragment MastForestResolutionReplay", + ) + .map_err(|err| { + DeserializationError::InvalidValue(format!("fragment {fragment_index}: {err}")) + })?; + } + } + + for forest_id in context.hasher_for_chiplet.iter_hash_basic_block_forest_ids() { + validate_mast_forest_id(forest_id, store_len, "hasher HashBasicBlock replay")?; + } + + Ok(()) +} + +fn validate_mast_forest_id( + forest_id: MastForestId, + store_len: usize, + label: &str, +) -> Result<(), DeserializationError> { + if forest_id.to_usize() >= store_len { + return Err(DeserializationError::InvalidValue(format!( + "{label} id {} is out of range for mast_forest_store length {store_len}", + u32::from(forest_id) + ))); + } + Ok(()) +} + /// Builder for recording the context to generate trace fragments during execution. /// /// Specifically, this records the information necessary to be able to generate the trace in @@ -1080,3 +1214,48 @@ impl Default for HasherChipletShim { Self::new() } } + +#[cfg(test)] +mod serialization_tests { + use super::*; + + fn empty_trace_generation_context( + fragment_size: usize, + max_stack_depth: usize, + ) -> TraceGenerationContext { + TraceGenerationContext { + mast_forest_store: Vec::new(), + core_trace_contexts: Vec::new(), + range_checker_replay: RangeCheckerReplay::default(), + memory_writes: MemoryWritesReplay::default(), + bitwise_replay: BitwiseReplay::default(), + hasher_for_chiplet: HasherRequestReplay::default(), + kernel_replay: KernelReplay::default(), + ace_replay: AceReplay::default(), + fragment_size, + max_stack_depth, + } + } + + #[test] + fn trace_generation_context_read_rejects_zero_fragment_size() { + let context = empty_trace_generation_context(0, MIN_STACK_DEPTH); + + let err = TraceGenerationContext::read_from_bytes(&context.to_bytes()).unwrap_err(); + let DeserializationError::InvalidValue(message) = err else { + panic!("expected invalid fragment size error"); + }; + assert!(message.contains("fragment_size must be non-zero")); + } + + #[test] + fn trace_generation_context_read_rejects_max_stack_depth_below_minimum() { + let context = empty_trace_generation_context(1, MIN_STACK_DEPTH - 1); + + let err = TraceGenerationContext::read_from_bytes(&context.to_bytes()).unwrap_err(); + let DeserializationError::InvalidValue(message) = err else { + panic!("expected invalid max stack depth error"); + }; + assert!(message.contains("max_stack_depth")); + } +} diff --git a/processor/src/trace/mod.rs b/processor/src/trace/mod.rs index 0371fda08c..d897e0752e 100644 --- a/processor/src/trace/mod.rs +++ b/processor/src/trace/mod.rs @@ -6,7 +6,10 @@ use miden_air::{ MidenMultiAir, ProverStatement, PublicInputs, StarkConfig, Statement, config, debug, trace::{MainTrace, decoder::NUM_USER_OP_HELPERS}, }; -use miden_core::{crypto::hash::Blake3_256, serde::Serializable}; +use miden_core::{ + crypto::hash::Blake3_256, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; use crate::{ Felt, MIN_STACK_DEPTH, Program, ProgramInfo, StackInputs, StackOutputs, Word, ZERO, @@ -86,6 +89,32 @@ impl TraceBuildOutput { } } +impl Serializable for TraceBuildOutput { + fn write_into(&self, target: &mut W) { + self.stack_outputs.write_into(target); + self.final_precompile_transcript.write_into(target); + self.precompile_requests.write_into(target); + self.precompile_requests_digest.write_into(target); + } +} + +impl Deserializable for TraceBuildOutput { + fn read_from(source: &mut R) -> Result { + let trace_output = Self { + stack_outputs: StackOutputs::read_from(source)?, + final_precompile_transcript: PrecompileTranscript::read_from(source)?, + precompile_requests: Vec::::read_from(source)?, + precompile_requests_digest: <[u8; 32]>::read_from(source)?, + }; + if !trace_output.has_matching_precompile_requests_digest() { + return Err(DeserializationError::InvalidValue( + "precompile request digest does not match serialized requests".into(), + )); + } + Ok(trace_output) + } +} + impl TraceBuildInputs { pub(crate) fn from_execution( program: &Program, @@ -155,6 +184,24 @@ impl TraceBuildInputs { } } +impl Serializable for TraceBuildInputs { + fn write_into(&self, target: &mut W) { + self.trace_output.write_into(target); + self.trace_generation_context.write_into(target); + self.program_info.write_into(target); + } +} + +impl Deserializable for TraceBuildInputs { + fn read_from(source: &mut R) -> Result { + Ok(Self { + trace_output: TraceBuildOutput::read_from(source)?, + trace_generation_context: TraceGenerationContext::read_from(source)?, + program_info: ProgramInfo::read_from(source)?, + }) + } +} + // VM EXECUTION TRACE // ================================================================================================ diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 861d9400bf..36df64abe8 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -4,6 +4,10 @@ use miden_air::trace::{ RowIndex, chiplets::hasher::{HasherState, STATE_WIDTH}, }; +use miden_core::serde::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, + SerializableVecDeque, read_vec_deque, +}; use crate::{ ContextId, ExecutionError, Felt, MIN_STACK_DEPTH, MemoryError, ONE, Word, ZERO, @@ -479,6 +483,10 @@ impl MastForestResolutionReplay { .pop_front() .ok_or(ExecutionError::Internal("no MastForest resolutions recorded")) } + + pub(crate) fn iter_forest_ids(&self) -> impl Iterator + '_ { + self.mast_forest_resolutions.iter().map(|(_node_id, forest_id)| *forest_id) + } } // MEMORY REPLAY @@ -1070,6 +1078,15 @@ impl HasherRequestReplay { self.hasher_ops .push_back(HasherOp::UpdateMerkleRoot((old_value, new_value, path, index))); } + + pub(crate) fn iter_hash_basic_block_forest_ids( + &self, + ) -> impl Iterator + '_ { + self.hasher_ops.iter().filter_map(|op| match op { + HasherOp::HashBasicBlock((forest_id, _node_id, _expected_hash)) => Some(*forest_id), + _ => None, + }) + } } impl IntoIterator for HasherRequestReplay { @@ -1182,3 +1199,716 @@ impl StackOverflowReplay { .ok_or(OperationError::Internal("no overflow address operations recorded")) } } + +// SERIALIZATION +// ================================================================================================ + +fn write_row_index(row: RowIndex, target: &mut W) { + u32::from(row).write_into(target); +} + +fn read_row_index(source: &mut R) -> Result { + Ok(RowIndex::from(u32::read_from(source)?)) +} + +fn write_memory_element_queue( + queue: &VecDeque<(Felt, Felt, ContextId, RowIndex)>, + target: &mut W, +) { + target.write_usize(queue.len()); + for &(element, addr, ctx, clk) in queue { + element.write_into(target); + addr.write_into(target); + ctx.write_into(target); + write_row_index(clk, target); + } +} + +fn read_memory_element_queue( + source: &mut R, +) -> Result, DeserializationError> { + let len = source.read_usize()?; + let element_size = + Felt::min_serialized_size() * 2 + u32::min_serialized_size() + u32::min_serialized_size(); + let max_len = source.max_alloc(element_size); + if len > max_len { + return Err(DeserializationError::InvalidValue(format!( + "memory element replay length {len} exceeds reader allocation bound {max_len}" + ))); + } + + let mut values = VecDeque::with_capacity(len); + for _ in 0..len { + values.push_back(( + Felt::read_from(source)?, + Felt::read_from(source)?, + ContextId::read_from(source)?, + read_row_index(source)?, + )); + } + Ok(values) +} + +fn write_memory_word_queue( + queue: &VecDeque<(Word, Felt, ContextId, RowIndex)>, + target: &mut W, +) { + target.write_usize(queue.len()); + for &(word, addr, ctx, clk) in queue { + word.write_into(target); + addr.write_into(target); + ctx.write_into(target); + write_row_index(clk, target); + } +} + +fn read_memory_word_queue( + source: &mut R, +) -> Result, DeserializationError> { + let len = source.read_usize()?; + let element_size = + Word::min_serialized_size() + Felt::min_serialized_size() + u32::min_serialized_size() * 2; + let max_len = source.max_alloc(element_size); + if len > max_len { + return Err(DeserializationError::InvalidValue(format!( + "memory word replay length {len} exceeds reader allocation bound {max_len}" + ))); + } + + let mut values = VecDeque::with_capacity(len); + for _ in 0..len { + values.push_back(( + Word::read_from(source)?, + Felt::read_from(source)?, + ContextId::read_from(source)?, + read_row_index(source)?, + )); + } + Ok(values) +} + +impl Serializable for SystemState { + fn write_into(&self, target: &mut W) { + write_row_index(self.clk, target); + self.ctx.write_into(target); + self.fn_hash.write_into(target); + self.pc_transcript_state.write_into(target); + } +} + +impl Deserializable for SystemState { + fn read_from(source: &mut R) -> Result { + Ok(Self { + clk: read_row_index(source)?, + ctx: ContextId::read_from(source)?, + fn_hash: Word::read_from(source)?, + pc_transcript_state: Word::read_from(source)?, + }) + } +} + +impl Serializable for DecoderState { + fn write_into(&self, target: &mut W) { + self.current_addr.write_into(target); + self.parent_addr.write_into(target); + } +} + +impl Deserializable for DecoderState { + fn read_from(source: &mut R) -> Result { + Ok(Self { + current_addr: Felt::read_from(source)?, + parent_addr: Felt::read_from(source)?, + }) + } +} + +impl Serializable for StackState { + fn write_into(&self, target: &mut W) { + self.stack_top.write_into(target); + self.stack_depth.write_into(target); + self.last_overflow_addr.write_into(target); + } +} + +impl Deserializable for StackState { + fn read_from(source: &mut R) -> Result { + let stack_top = <[Felt; MIN_STACK_DEPTH]>::read_from(source)?; + let stack_depth = usize::read_from(source)?; + if stack_depth < MIN_STACK_DEPTH { + return Err(DeserializationError::InvalidValue(format!( + "stack depth {stack_depth} is below minimum {MIN_STACK_DEPTH}" + ))); + } + Ok(Self { + stack_top, + stack_depth, + last_overflow_addr: Felt::read_from(source)?, + }) + } +} + +impl Serializable for CoreTraceState { + fn write_into(&self, target: &mut W) { + self.system.write_into(target); + self.decoder.write_into(target); + self.stack.write_into(target); + } +} + +impl Deserializable for CoreTraceState { + fn read_from(source: &mut R) -> Result { + Ok(Self { + system: SystemState::read_from(source)?, + decoder: DecoderState::read_from(source)?, + stack: StackState::read_from(source)?, + }) + } +} + +impl Serializable for CoreTraceFragmentContext { + fn write_into(&self, target: &mut W) { + self.state.write_into(target); + self.replay.write_into(target); + self.continuation.write_into(target); + self.initial_mast_forest_id.write_into(target); + } +} + +impl Deserializable for CoreTraceFragmentContext { + fn read_from(source: &mut R) -> Result { + Ok(Self { + state: CoreTraceState::read_from(source)?, + replay: ExecutionReplay::read_from(source)?, + continuation: ContinuationStack::::read_from(source)?, + initial_mast_forest_id: MastForestId::read_from(source)?, + }) + } +} + +impl Serializable for NodeEndData { + fn write_into(&self, target: &mut W) { + self.ended_node_addr.write_into(target); + self.prev_addr.write_into(target); + self.prev_parent_addr.write_into(target); + } +} + +impl Deserializable for NodeEndData { + fn read_from(source: &mut R) -> Result { + Ok(Self { + ended_node_addr: Felt::read_from(source)?, + prev_addr: Felt::read_from(source)?, + prev_parent_addr: Felt::read_from(source)?, + }) + } +} + +impl Serializable for ExecutionContextSystemInfo { + fn write_into(&self, target: &mut W) { + self.parent_ctx.write_into(target); + self.parent_fn_hash.write_into(target); + } +} + +impl Deserializable for ExecutionContextSystemInfo { + fn read_from(source: &mut R) -> Result { + Ok(Self { + parent_ctx: ContextId::read_from(source)?, + parent_fn_hash: Word::read_from(source)?, + }) + } +} + +impl Serializable for ExecutionContextReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.execution_contexts).write_into(target); + } +} + +impl Deserializable for ExecutionContextReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + execution_contexts: read_vec_deque(source)?, + }) + } +} + +impl Serializable for BlockStackReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.node_start_parent_addr).write_into(target); + SerializableVecDeque(&self.node_end).write_into(target); + } +} + +impl Deserializable for BlockStackReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + node_start_parent_addr: read_vec_deque(source)?, + node_end: read_vec_deque(source)?, + }) + } +} + +impl Serializable for MastForestResolutionReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.mast_forest_resolutions).write_into(target); + } +} + +impl Deserializable for MastForestResolutionReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + mast_forest_resolutions: read_vec_deque(source)?, + }) + } +} + +impl Serializable for MemoryReadsReplay { + fn write_into(&self, target: &mut W) { + write_memory_element_queue(&self.elements_read, target); + write_memory_word_queue(&self.words_read, target); + } +} + +impl Deserializable for MemoryReadsReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + elements_read: read_memory_element_queue(source)?, + words_read: read_memory_word_queue(source)?, + }) + } +} + +impl Serializable for MemoryWritesReplay { + fn write_into(&self, target: &mut W) { + write_memory_element_queue(&self.elements_written, target); + write_memory_word_queue(&self.words_written, target); + } +} + +impl Deserializable for MemoryWritesReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + elements_written: read_memory_element_queue(source)?, + words_written: read_memory_word_queue(source)?, + }) + } +} + +impl Serializable for AdviceReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.stack_pops).write_into(target); + SerializableVecDeque(&self.stack_word_pops).write_into(target); + SerializableVecDeque(&self.stack_dword_pops).write_into(target); + } +} + +impl Deserializable for AdviceReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + stack_pops: read_vec_deque(source)?, + stack_word_pops: read_vec_deque(source)?, + stack_dword_pops: read_vec_deque(source)?, + }) + } +} + +impl Serializable for BitwiseOp { + fn write_into(&self, target: &mut W) { + match self { + Self::U32And => 0u8, + Self::U32Xor => 1u8, + } + .write_into(target); + } +} + +impl Deserializable for BitwiseOp { + fn read_from(source: &mut R) -> Result { + match u8::read_from(source)? { + 0 => Ok(Self::U32And), + 1 => Ok(Self::U32Xor), + tag => Err(DeserializationError::InvalidValue(format!( + "invalid bitwise replay op tag {tag}" + ))), + } + } +} + +impl Serializable for BitwiseReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.u32op_with_operands).write_into(target); + } +} + +impl Deserializable for BitwiseReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + u32op_with_operands: read_vec_deque(source)?, + }) + } +} + +impl Serializable for KernelReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.kernel_proc_accesses).write_into(target); + } +} + +impl Deserializable for KernelReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + kernel_proc_accesses: read_vec_deque(source)?, + }) + } +} + +fn write_ace_queue(queue: &VecDeque<(RowIndex, CircuitEvaluation)>, target: &mut W) { + target.write_usize(queue.len()); + for &(row, ref evaluation) in queue { + write_row_index(row, target); + evaluation.write_into(target); + } +} + +fn read_ace_queue( + source: &mut R, +) -> Result, DeserializationError> { + let len = source.read_usize()?; + let max_len = + source.max_alloc(u32::min_serialized_size() + CircuitEvaluation::min_serialized_size()); + if len > max_len { + return Err(DeserializationError::InvalidValue(format!( + "ACE replay length {len} exceeds reader allocation bound {max_len}" + ))); + } + + let mut values = VecDeque::with_capacity(len); + for _ in 0..len { + values.push_back((read_row_index(source)?, CircuitEvaluation::read_from(source)?)); + } + Ok(values) +} + +impl Serializable for AceReplay { + fn write_into(&self, target: &mut W) { + write_ace_queue(&self.circuit_evaluations, target); + } +} + +impl Deserializable for AceReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + circuit_evaluations: read_ace_queue(source)?, + }) + } +} + +impl Serializable for RangeCheckerReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.range_checks_u32_ops).write_into(target); + } +} + +impl Deserializable for RangeCheckerReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + range_checks_u32_ops: read_vec_deque(source)?, + }) + } +} + +impl Serializable for BlockAddressReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.block_addresses).write_into(target); + } +} + +impl Deserializable for BlockAddressReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { block_addresses: read_vec_deque(source)? }) + } +} + +impl Serializable for HasherResponseReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.permutation_operations).write_into(target); + SerializableVecDeque(&self.build_merkle_root_operations).write_into(target); + SerializableVecDeque(&self.mrupdate_operations).write_into(target); + } +} + +impl Deserializable for HasherResponseReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + permutation_operations: read_vec_deque(source)?, + build_merkle_root_operations: read_vec_deque(source)?, + mrupdate_operations: read_vec_deque(source)?, + }) + } +} + +impl Serializable for HasherOp { + fn write_into(&self, target: &mut W) { + match self { + Self::Permute(state) => { + 0u8.write_into(target); + state.write_into(target); + }, + Self::HashControlBlock((h1, h2, domain, expected_hash)) => { + 1u8.write_into(target); + h1.write_into(target); + h2.write_into(target); + domain.write_into(target); + expected_hash.write_into(target); + }, + Self::HashBasicBlock((forest_id, node_id, expected_hash)) => { + 2u8.write_into(target); + forest_id.write_into(target); + node_id.write_into(target); + expected_hash.write_into(target); + }, + Self::BuildMerkleRoot((leaf, path, index)) => { + 3u8.write_into(target); + leaf.write_into(target); + path.write_into(target); + index.write_into(target); + }, + Self::UpdateMerkleRoot((old_value, new_value, path, index)) => { + 4u8.write_into(target); + old_value.write_into(target); + new_value.write_into(target); + path.write_into(target); + index.write_into(target); + }, + } + } +} + +impl Deserializable for HasherOp { + fn read_from(source: &mut R) -> Result { + match u8::read_from(source)? { + 0 => Ok(Self::Permute(<[Felt; STATE_WIDTH]>::read_from(source)?)), + 1 => Ok(Self::HashControlBlock(( + Word::read_from(source)?, + Word::read_from(source)?, + Felt::read_from(source)?, + Word::read_from(source)?, + ))), + 2 => Ok(Self::HashBasicBlock(( + MastForestId::read_from(source)?, + MastNodeId::read_from(source)?, + Word::read_from(source)?, + ))), + 3 => Ok(Self::BuildMerkleRoot(( + Word::read_from(source)?, + MerklePath::read_from(source)?, + Felt::read_from(source)?, + ))), + 4 => Ok(Self::UpdateMerkleRoot(( + Word::read_from(source)?, + Word::read_from(source)?, + MerklePath::read_from(source)?, + Felt::read_from(source)?, + ))), + tag => Err(DeserializationError::InvalidValue(format!( + "invalid hasher replay op tag {tag}" + ))), + } + } +} + +impl Serializable for HasherRequestReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.hasher_ops).write_into(target); + } +} + +impl Deserializable for HasherRequestReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { hasher_ops: read_vec_deque(source)? }) + } +} + +impl Serializable for StackOverflowReplay { + fn write_into(&self, target: &mut W) { + SerializableVecDeque(&self.overflow_values).write_into(target); + SerializableVecDeque(&self.restore_context_info).write_into(target); + } +} + +impl Deserializable for StackOverflowReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + overflow_values: read_vec_deque(source)?, + restore_context_info: read_vec_deque(source)?, + }) + } +} + +impl Serializable for ExecutionReplay { + fn write_into(&self, target: &mut W) { + self.block_stack.write_into(target); + self.execution_context.write_into(target); + self.stack_overflow.write_into(target); + self.memory_reads.write_into(target); + self.advice.write_into(target); + self.hasher.write_into(target); + self.block_address.write_into(target); + self.mast_forest_resolution.write_into(target); + } +} + +impl Deserializable for ExecutionReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { + block_stack: BlockStackReplay::read_from(source)?, + execution_context: ExecutionContextReplay::read_from(source)?, + stack_overflow: StackOverflowReplay::read_from(source)?, + memory_reads: MemoryReadsReplay::read_from(source)?, + advice: AdviceReplay::read_from(source)?, + hasher: HasherResponseReplay::read_from(source)?, + block_address: BlockAddressReplay::read_from(source)?, + mast_forest_resolution: MastForestResolutionReplay::read_from(source)?, + }) + } +} + +#[cfg(test)] +mod serialization_tests { + use alloc::vec::Vec; + + use miden_core::serde::{BudgetedReader, SliceReader}; + + use super::*; + use crate::mast::MastForestId; + + #[test] + fn memory_replay_queues_reject_oversized_lengths_before_allocation() { + let mut bytes = Vec::new(); + usize::MAX.write_into(&mut bytes); + + let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); + let err = read_memory_element_queue(&mut reader).unwrap_err(); + assert!(err.to_string().contains("exceeds reader allocation bound")); + + let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); + let err = read_memory_word_queue(&mut reader).unwrap_err(); + assert!(err.to_string().contains("exceeds reader allocation bound")); + } + + #[test] + fn stack_state_read_rejects_depth_below_minimum() { + let mut bytes = Vec::new(); + [ZERO; MIN_STACK_DEPTH].write_into(&mut bytes); + (MIN_STACK_DEPTH - 1).write_into(&mut bytes); + ZERO.write_into(&mut bytes); + + let err = StackState::read_from_bytes(&bytes).unwrap_err(); + assert!(err.to_string().contains("below minimum")); + } + + #[test] + fn execution_replay_round_trip_preserves_replay_order() { + let mut replay = ExecutionReplay::default(); + replay.block_stack.record_node_start_parent_addr(Felt::from_u32(7)); + replay + .block_stack + .record_node_end(Felt::from_u32(9), Felt::from_u32(8), Felt::from_u32(7)); + replay.execution_context.record_execution_context(ExecutionContextSystemInfo { + parent_ctx: ContextId::from(3), + parent_fn_hash: Word::from([ONE, ZERO, ZERO, ZERO]), + }); + replay + .stack_overflow + .record_pop_overflow(Felt::from_u32(11), Felt::from_u32(10)); + replay + .stack_overflow + .record_restore_context_overflow_addr(17, Felt::from_u32(16)); + replay.memory_reads.record_read_element( + Felt::from_u32(21), + Felt::from_u32(20), + ContextId::from(4), + RowIndex::from(5_u32), + ); + replay.memory_reads.record_read_word( + Word::from([ + Felt::from_u32(1), + Felt::from_u32(2), + Felt::from_u32(3), + Felt::from_u32(4), + ]), + Felt::from_u32(24), + ContextId::from(4), + RowIndex::from(6_u32), + ); + replay.advice.record_pop_stack(Felt::from_u32(31)); + replay + .advice + .record_pop_stack_word(Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO])); + replay.advice.record_pop_stack_dword([ + Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), + Word::default(), + ]); + replay.hasher.record_permute(Felt::from_u32(40), [Felt::from_u32(41); 12]); + replay.block_address.record_block_address(Felt::from_u32(50)); + replay + .mast_forest_resolution + .record_resolution(MastNodeId::from(2), MastForestId::from(1)); + + let bytes = replay.to_bytes(); + let mut restored = ExecutionReplay::read_from_bytes(&bytes).unwrap(); + + assert_eq!( + restored.block_stack.replay_node_start_parent_addr().unwrap(), + Felt::from_u32(7) + ); + let node_end = restored.block_stack.replay_node_end().unwrap(); + assert_eq!(node_end.ended_node_addr, Felt::from_u32(9)); + assert_eq!(node_end.prev_addr, Felt::from_u32(8)); + assert_eq!(node_end.prev_parent_addr, Felt::from_u32(7)); + let ctx = restored.execution_context.replay_execution_context().unwrap(); + assert_eq!(ctx.parent_ctx, ContextId::from(3)); + assert_eq!(ctx.parent_fn_hash, Word::from([ONE, ZERO, ZERO, ZERO])); + assert_eq!( + restored.stack_overflow.replay_pop_overflow().unwrap(), + (Felt::from_u32(11), Felt::from_u32(10)) + ); + assert_eq!( + restored.stack_overflow.replay_restore_context_overflow_addr().unwrap(), + (17, Felt::from_u32(16)) + ); + assert_eq!( + restored.memory_reads.replay_read_element(Felt::from_u32(20)).unwrap(), + Felt::from_u32(21) + ); + assert_eq!( + restored.memory_reads.replay_read_word(Felt::from_u32(24)).unwrap(), + Word::from([ + Felt::from_u32(1), + Felt::from_u32(2), + Felt::from_u32(3), + Felt::from_u32(4) + ]) + ); + assert_eq!(restored.advice.replay_pop_stack().unwrap(), Felt::from_u32(31)); + assert_eq!( + restored.advice.replay_pop_stack_word().unwrap(), + Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO]) + ); + assert_eq!( + restored.advice.replay_pop_stack_dword().unwrap(), + [Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), Word::default()] + ); + assert_eq!( + restored.hasher.replay_permute().unwrap(), + (Felt::from_u32(40), [Felt::from_u32(41); 12]) + ); + assert_eq!(restored.block_address.replay_block_address().unwrap(), Felt::from_u32(50)); + assert_eq!( + restored.mast_forest_resolution.replay_resolution().unwrap(), + (MastNodeId::from(2), MastForestId::from(1)) + ); + } +} diff --git a/prover/src/lib.rs b/prover/src/lib.rs index b80f88d8f6..7696507226 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -9,7 +9,15 @@ use alloc::{string::ToString, vec, vec::Vec}; use ::serde::Serialize; use miden_air::{MidenMultiAir, ProverStatement, Statement}; -use miden_core::{Felt, field::QuadFelt, utils::RowMajorMatrix}; +use miden_core::{ + Felt, + field::QuadFelt, + serde::{ + BudgetedReader, ByteReader, ByteWriter, Deserializable, + DeserializationError as SerdeDeserializationError, Serializable, SliceReader, + }, + utils::RowMajorMatrix, +}; use miden_crypto::stark::{ ProverInstance, StarkConfig, lmcs::Lmcs, @@ -36,6 +44,11 @@ pub use miden_processor::{ pub use proving_options::ProvingOptions; /// Inputs required to prove from pre-executed trace data. +/// +/// Its binary form is a VM-owned remote proving input containing trace replay data and +/// proof-generation options. Deserialization checks malformed structure and bounded allocation; +/// semantic replay inconsistencies may still surface later while building the trace. Correct +/// execution is established by the proof generated from these inputs. #[derive(Debug)] pub struct TraceProvingInputs { trace_inputs: TraceBuildInputs, @@ -52,6 +65,54 @@ impl TraceProvingInputs { pub fn into_parts(self) -> (TraceBuildInputs, ProvingOptions) { (self.trace_inputs, self.options) } + + /// Deserializes remote proving inputs using the supplied untrusted byte budget. + pub fn read_from_bytes_with_budget( + bytes: &[u8], + budget: usize, + ) -> Result { + if budget < bytes.len() { + return Err(SerdeDeserializationError::InvalidValue( + "TraceProvingInputs byte budget is smaller than payload length".into(), + )); + } + let allocation_budget = budget.min(bytes.len().saturating_mul(4)); + let mut reader = BudgetedReader::new(SliceReader::new(bytes), allocation_budget); + let inputs = Self::read_from(&mut reader)?; + if reader.has_more_bytes() { + return Err(SerdeDeserializationError::InvalidValue( + "TraceProvingInputs payload has trailing bytes".into(), + )); + } + Ok(inputs) + } +} + +impl Serializable for TraceProvingInputs { + fn write_into(&self, target: &mut W) { + self.trace_inputs.write_into(target); + self.options.write_into(target); + } +} + +impl Deserializable for TraceProvingInputs { + fn read_from(source: &mut R) -> Result { + Ok(Self { + trace_inputs: TraceBuildInputs::read_from(source)?, + options: ProvingOptions::read_from(source)?, + }) + } + + fn read_from_bytes(bytes: &[u8]) -> Result { + TraceProvingInputs::read_from_bytes_with_budget(bytes, bytes.len().saturating_mul(4)) + } + + fn read_from_bytes_with_budget( + bytes: &[u8], + budget: usize, + ) -> Result { + TraceProvingInputs::read_from_bytes_with_budget(bytes, budget) + } } // PROVER diff --git a/prover/src/proving_options.rs b/prover/src/proving_options.rs index fd345f6252..497800a2f6 100644 --- a/prover/src/proving_options.rs +++ b/prover/src/proving_options.rs @@ -1,4 +1,7 @@ -use miden_core::proof::HashFunction; +use miden_core::{ + proof::HashFunction, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; // PROVING OPTIONS // ================================================================================================ @@ -44,3 +47,15 @@ impl Default for ProvingOptions { Self::new(HashFunction::Blake3_256) } } + +impl Serializable for ProvingOptions { + fn write_into(&self, target: &mut W) { + self.hash_fn.write_into(target); + } +} + +impl Deserializable for ProvingOptions { + fn read_from(source: &mut R) -> Result { + Ok(Self::new(HashFunction::read_from(source)?)) + } +} diff --git a/prover/tests/trace_proving_inputs_seeds.rs b/prover/tests/trace_proving_inputs_seeds.rs new file mode 100644 index 0000000000..b65d2f32ed --- /dev/null +++ b/prover/tests/trace_proving_inputs_seeds.rs @@ -0,0 +1,42 @@ +use std::{fs, path::Path}; + +use miden_assembly::Assembler; +use miden_processor::{DefaultHost, FastProcessor}; +use miden_prover::{ + AdviceInputs, ExecutionOptions, ProvingOptions, StackInputs, TraceProvingInputs, + serde::Serializable, +}; + +#[test] +#[ignore = "writes fuzz corpus seeds"] +fn generate_trace_proving_inputs_fuzz_seed() { + let program = Assembler::default() + .assemble_program( + "trace_proving_inputs_seed", + " + begin + push.1 drop + end + ", + ) + .expect("failed to assemble seed program") + .unwrap_program(); + + let processor = FastProcessor::new_with_options( + StackInputs::default(), + AdviceInputs::default(), + ExecutionOptions::default().with_core_trace_fragment_size(64).unwrap(), + ) + .expect("processor advice inputs should fit advice map limits"); + let mut host = DefaultHost::default(); + let trace_inputs = processor + .execute_trace_inputs_sync(&program, &mut host) + .expect("seed program execution failed"); + + let seed = TraceProvingInputs::new(trace_inputs, ProvingOptions::default()).to_bytes(); + let corpus_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../miden-core-fuzz/corpus/trace_proving_inputs_deserialize"); + fs::create_dir_all(&corpus_dir).expect("failed to create trace proving inputs corpus dir"); + fs::write(corpus_dir.join("valid-small.bin"), seed) + .expect("failed to write trace proving inputs seed"); +} From 35922a091dce44e2f6b4c2e3954e552d5ff7e393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 10:24:28 -0400 Subject: [PATCH 02/12] Add arbitrary roundtrips for trace inputs --- Cargo.lock | 3 + core/src/mast/mod.rs | 5 +- core/src/mast/sparse.rs | 27 ++ core/src/precompile.rs | 18 + processor/Cargo.toml | 3 + processor/src/continuation_stack.rs | 85 +++- processor/src/lib.rs | 30 +- processor/src/trace/chiplets/ace/trace.rs | 138 +++++- processor/src/trace/trace_state.rs | 553 +++++++++++++++++++++- prover/Cargo.toml | 3 + prover/src/proving_options.rs | 24 + 11 files changed, 853 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f01c5708fc..2397f3522d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1684,6 +1684,7 @@ dependencies = [ "miden-core", "miden-debug-types", "miden-mast-package", + "miden-test-serde-macros", "miden-test-utils", "miden-utils-diagnostics", "miden-utils-indexing", @@ -1726,6 +1727,8 @@ dependencies = [ "miden-crypto", "miden-debug-types", "miden-processor", + "miden-test-serde-macros", + "proptest", "serde", "serde-wincode", "tokio", diff --git a/core/src/mast/mod.rs b/core/src/mast/mod.rs index 5d1f095274..dbe9e1c03c 100644 --- a/core/src/mast/mod.rs +++ b/core/src/mast/mod.rs @@ -839,7 +839,10 @@ impl ExecutableMastForest for Arc { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] -#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true)) +)] pub struct MastNodeId(u32); /// Operations that mutate a MAST often produce this mapping between old and new NodeIds. diff --git a/core/src/mast/sparse.rs b/core/src/mast/sparse.rs index c555f5ea9b..7b3eff0e3c 100644 --- a/core/src/mast/sparse.rs +++ b/core/src/mast/sparse.rs @@ -40,6 +40,17 @@ impl crate::serde::Deserializable for MastForestId { } } +#[cfg(feature = "arbitrary")] +impl proptest::prelude::Arbitrary for MastForestId { + type Parameters = (); + type Strategy = proptest::prelude::BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use proptest::prelude::*; + any::().prop_map(Self::from).boxed() + } +} + // SPARSE MAST FOREST // ================================================================================================ @@ -306,6 +317,22 @@ impl ExecutableMastForest for SparseMastForest { } } +#[cfg(all(feature = "arbitrary", test))] +mod serde_tests { + use proptest::prelude::*; + + use super::*; + use crate::serde::{Deserializable, Serializable}; + + proptest! { + #[test] + fn mast_forest_id_binary_serde_roundtrip(id in any::()) { + let bytes = id.to_bytes(); + prop_assert_eq!(id, MastForestId::read_from_bytes(&bytes).unwrap()); + } + } +} + // SPARSE MAST FOREST BUILDER // ================================================================================================ diff --git a/core/src/precompile.rs b/core/src/precompile.rs index 1a963d3672..ef23e16360 100644 --- a/core/src/precompile.rs +++ b/core/src/precompile.rs @@ -334,6 +334,10 @@ pub trait PrecompileVerifier: Send + Sync { /// statement into the rolling state via the 2-to-1 hash /// `state' = Poseidon2::merge(state, STMNT)`. The state is exposed directly as the transcript /// digest — no finalization step is required. +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] pub struct PrecompileTranscript { /// The rolling transcript digest. @@ -378,6 +382,20 @@ impl Deserializable for PrecompileTranscript { } } +#[cfg(feature = "arbitrary")] +impl proptest::prelude::Arbitrary for PrecompileTranscript { + type Parameters = (); + type Strategy = proptest::prelude::BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use proptest::prelude::*; + prop::array::uniform4(any::().prop_map(Felt::from_u32)) + .prop_map(Word::new) + .prop_map(Self::from_state) + .boxed() + } +} + // PRECOMPILE ERROR // ================================================================================================ diff --git a/processor/Cargo.toml b/processor/Cargo.toml index 07539c20fd..5b16c4311f 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -19,6 +19,7 @@ bench = false doctest = false [features] +arbitrary = ["dep:proptest", "miden-core/arbitrary"] concurrent = ["std", "miden-air/concurrent"] default = ["std"] std = [ @@ -45,12 +46,14 @@ miden-utils-indexing.workspace = true # External dependencies itertools.workspace = true paste.workspace = true +proptest = { workspace = true, optional = true } rayon = { version = "1.10", default-features = false } tracing.workspace = true thiserror.workspace = true [dev-dependencies] miden-assembly = { workspace = true, features = ["testing"] } +miden-test-serde-macros.workspace = true miden-utils-testing.workspace = true insta.workspace = true pretty_assertions = { workspace = true, features = ["std"] } diff --git a/processor/src/continuation_stack.rs b/processor/src/continuation_stack.rs index b7ea4337b0..24e31cb126 100644 --- a/processor/src/continuation_stack.rs +++ b/processor/src/continuation_stack.rs @@ -22,7 +22,15 @@ const CONTINUATION_STACK_SIZE_HINT: usize = 64; /// [`Continuation::EnterForest`] variant. For live execution this is `Arc`; for the /// snapshotted continuation stack inside a trace fragment it is a `usize` index into the /// `mast_forest_store` of the trace generation context. -#[derive(Debug, Clone)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test( + binary_serde(true), + serde_test(false), + types(MastForestId) + ) +)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Continuation { /// Start processing a node in the MAST forest. StartNode(MastNodeId), @@ -119,7 +127,15 @@ impl Continuation { /// This allows the processor to execute a program iteratively in a loop rather than recursively /// traversing the nodes. It also allows the processor to pass the state of execution to another /// processor for further processing, which is useful for parallel execution of MAST forests. -#[derive(Debug, Clone)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test( + binary_serde(true), + serde_test(false), + types(MastForestId) + ) +)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ContinuationStack { stack: Vec>, source_node_ids: Option>>, @@ -461,6 +477,71 @@ impl Deserializable for ContinuationStack { } } +#[cfg(feature = "arbitrary")] +mod arbitrary { + use proptest::{collection, prelude::*}; + + use super::*; + use crate::mast::MastForestId; + + const MAX_CONTINUATIONS: usize = 16; + + fn arb_source_node_id() -> impl Strategy { + any::().prop_map(DebugSourceNodeId::from) + } + + impl Arbitrary for Continuation { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + any::().prop_map(Self::StartNode), + any::().prop_map(Self::FinishJoin), + any::().prop_map(Self::FinishSplit), + any::().prop_map(Self::FinishLoop), + any::().prop_map(Self::FinishCall), + any::().prop_map(Self::FinishDyn), + (any::(), 0usize..=8, 0usize..=8).prop_map( + |(node_id, batch_index, op_idx_in_batch)| Self::ResumeBasicBlock { + node_id, + batch_index, + op_idx_in_batch, + }, + ), + (any::(), 0usize..=8) + .prop_map(|(node_id, batch_index)| Self::Respan { node_id, batch_index }), + any::().prop_map(Self::FinishBasicBlock), + any::() + .prop_map(|forest| Self::EnterForest { forest, package_debug_info: None }), + ] + .boxed() + } + } + + impl Arbitrary for ContinuationStack { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + collection::vec(any::>(), 0..=MAX_CONTINUATIONS) + .prop_flat_map(|stack| { + let len = stack.len(); + ( + Just(stack), + prop_oneof![ + Just(None), + collection::vec(proptest::option::of(arb_source_node_id()), len..=len,) + .prop_map(Some), + ], + ) + }) + .prop_map(|(stack, source_node_ids)| Self { stack, source_node_ids }) + .boxed() + } + } +} + // TESTS // ================================================================================================ diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 3d0dc6ef2f..c463e166b2 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -26,7 +26,10 @@ mod host; mod processor; mod tracer; -use miden_core::mast::ExecutableMastForest; +use miden_core::{ + mast::ExecutableMastForest, + serde::{Deserializable, Serializable}, +}; use crate::{ advice::{AdviceInputs, AdviceProvider}, @@ -308,6 +311,10 @@ pub trait Stopper { // ================================================================================================ /// Represents the ID of an execution context +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct ContextId(u32); @@ -353,21 +360,32 @@ impl From for Felt { } } -impl serde::Serializable for ContextId { +impl Serializable for ContextId { fn write_into(&self, target: &mut W) { - serde::Serializable::write_into(&self.0, target); + Serializable::write_into(&self.0, target); } } -impl serde::Deserializable for ContextId { +impl Deserializable for ContextId { fn read_from( source: &mut R, ) -> Result { - Ok(Self(::read_from(source)?)) + Ok(Self(::read_from(source)?)) } fn min_serialized_size() -> usize { - ::min_serialized_size() + ::min_serialized_size() + } +} + +#[cfg(feature = "arbitrary")] +impl proptest::prelude::Arbitrary for ContextId { + type Parameters = (); + type Strategy = proptest::prelude::BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use proptest::prelude::*; + any::().prop_map(Self).boxed() } } diff --git a/processor/src/trace/chiplets/ace/trace.rs b/processor/src/trace/chiplets/ace/trace.rs index 8adaac88fc..c824ee5df6 100644 --- a/processor/src/trace/chiplets/ace/trace.rs +++ b/processor/src/trace/chiplets/ace/trace.rs @@ -19,7 +19,11 @@ use crate::{ContextId, errors::AceError}; /// One row of the ACE chiplet trace in `READ` mode: two memory-loaded wires per row, plus the /// pointer of the word that was loaded. -#[derive(Debug, Clone, Copy)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] struct ReadNode { ptr: Felt, id_0: Felt, @@ -31,7 +35,11 @@ struct ReadNode { /// One row of the ACE chiplet trace in `EVAL` mode: a single arithmetic gate `(id_0, v_0)` with /// two inputs `(id_1, v_1)` (left) and `(id_2, v_2)` (right), the instruction pointer that /// produced it, and the gate's `eval_op` selector. -#[derive(Debug, Clone, Copy)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] struct EvalNode { ptr: Felt, eval_op: Felt, @@ -47,7 +55,11 @@ struct EvalNode { /// The output value is checked to be equal to 0. /// /// The set of nodes is used to fill the ACE chiplet trace. -#[derive(Debug, Clone)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CircuitEvaluation { ctx: ContextId, clk: RowIndex, @@ -244,7 +256,11 @@ fn quad_to_expr(v: QuadFelt) -> QuadFeltExpr { /// gate, to "receive" the values of the input wires from the bus and to "send" the value of /// the value of the output wire back with multiplicity equal to the fan-out of the respective gate. /// Note that the messages include extra data in order to avoid collisions. -#[derive(Debug, Clone)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Clone, PartialEq, Eq)] struct WireBus { // Circuit ID as Felt of the next wire to be inserted id_next: Felt, @@ -495,6 +511,120 @@ impl CircuitEvaluation { } } +#[cfg(feature = "arbitrary")] +mod arbitrary { + use proptest::{collection, prelude::*}; + + use super::*; + + const MAX_TEST_NODES: usize = 8; + + fn arb_felt() -> impl Strategy { + any::().prop_map(Felt::from_u32) + } + + fn arb_quad_felt() -> impl Strategy { + (arb_felt(), arb_felt()).prop_map(|(c0, c1)| QuadFelt::new([c0, c1])) + } + + impl Arbitrary for ReadNode { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_felt(), arb_felt(), arb_quad_felt(), arb_felt(), arb_quad_felt()) + .prop_map(|(ptr, id_0, v_0, id_1, v_1)| Self { ptr, id_0, v_0, id_1, v_1 }) + .boxed() + } + } + + impl Arbitrary for EvalNode { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_felt(), + arb_felt(), + arb_felt(), + arb_quad_felt(), + arb_felt(), + arb_quad_felt(), + arb_felt(), + arb_quad_felt(), + ) + .prop_map(|(ptr, eval_op, id_0, v_0, id_1, v_1, id_2, v_2)| Self { + ptr, + eval_op, + id_0, + v_0, + id_1, + v_1, + id_2, + v_2, + }) + .boxed() + } + } + + impl Arbitrary for WireBus { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (1usize..=MAX_TEST_NODES) + .prop_flat_map(|wire_count| { + ( + arb_felt(), + collection::vec((arb_quad_felt(), any::()), wire_count), + Just(wire_count as u32), + ) + }) + .prop_map(|(id_next, wires, num_wires)| Self { id_next, wires, num_wires }) + .boxed() + } + } + + impl Arbitrary for CircuitEvaluation { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (0usize..=3, 1usize..=MAX_TEST_NODES) + .prop_filter( + "ACE test circuit must fit the wire limit", + |(read_count, eval_count)| { + read_count.saturating_mul(2).saturating_add(*eval_count) + <= MAX_NUM_ACE_WIRES as usize + }, + ) + .prop_flat_map(|(read_count, eval_count)| { + let wire_count = read_count * 2 + eval_count; + ( + any::(), + any::().prop_map(RowIndex::from), + collection::vec(any::(), read_count), + collection::vec(any::(), eval_count), + collection::vec((arb_quad_felt(), any::()), wire_count), + Just(wire_count as u32), + ) + }) + .prop_map(|(ctx, clk, read_nodes, eval_nodes, wires, num_wires)| Self { + ctx, + clk, + wire_bus: WireBus { + id_next: Felt::from_u32(num_wires), + wires, + num_wires, + }, + read_nodes, + eval_nodes, + }) + .boxed() + } + } +} + #[cfg(test)] mod serialization_tests { use alloc::vec; diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 36df64abe8..079014a8a4 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -43,7 +43,11 @@ use crate::{ /// 4. initial MAST forest: the MAST forest being executed at the start of the fragment (which can /// change during execution when encountering an [`miden_core::mast::ExternalNode`] or /// [`miden_core::mast::DynNode`]). -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct CoreTraceFragmentContext { pub state: CoreTraceState, pub replay: ExecutionReplay, @@ -59,7 +63,11 @@ pub struct CoreTraceFragmentContext { /// Subset of the processor state used to build the core trace (system, decoder and stack sets of /// columns). -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct CoreTraceState { pub system: SystemState, pub decoder: DecoderState, @@ -73,6 +81,10 @@ pub struct CoreTraceState { /// /// This struct captures the complete state of the system at a specific clock cycle, allowing for /// reconstruction of the system trace during concurrent execution. +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SystemState { /// Current clock cycle (row index in the trace) @@ -109,7 +121,11 @@ impl SystemState { // ================================================================================================ /// The subset of the decoder state required to build the trace. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct DecoderState { /// The value of the decoder's `addr` column. pub current_addr: Felt, @@ -158,7 +174,11 @@ impl DecoderState { /// The stack trace consists of 19 columns total: 16 stack columns + 3 helper columns. The helper /// columns (stack_depth, overflow_addr, and overflow_helper) are computed from the stack_depth and /// last_overflow_addr fields. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct StackState { /// Top 16 stack slots (s0 to s15). These represent the top elements of the stack that are /// directly accessible. @@ -284,7 +304,11 @@ impl StackState { /// components needed to produce those values, such as the memory chiplet, advice provider, etc. It /// also packages up all the necessary data for trace generators to generate trace fragments, which /// can be done on separate machines in parallel, for example. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct ExecutionReplay { pub block_stack: BlockStackReplay, pub execution_context: ExecutionContextReplay, @@ -299,7 +323,11 @@ pub struct ExecutionReplay { // EXECUTION CONTEXT REPLAY // ================================================================================================ -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct ExecutionContextReplay { /// Extra data needed to recover the state on an END operation specifically for /// CALL/SYSCALL/DYNCALL nodes (which start/end a new execution context). @@ -326,7 +354,11 @@ impl ExecutionContextReplay { // ================================================================================================ /// Replay data for the block stack. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct BlockStackReplay { /// The parent address, recorded when a new node is started (JOIN, SPLIT, etc). node_start_parent_addr: VecDeque, @@ -431,7 +463,11 @@ impl NodeFlags { /// We record `ended_node_addr` in order to be able to properly populate the trace row for the /// node operation. Additionally, we record `prev_addr` and `prev_parent_addr` to allow emulating /// peeking into the block stack, which is needed when processing REPEAT or RESPAN nodes. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct NodeEndData { /// the address of the node that is ending pub ended_node_addr: Felt, @@ -445,7 +481,11 @@ pub struct NodeEndData { /// Data required to recover the state of an execution context when restoring it during an END /// operation. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct ExecutionContextSystemInfo { pub parent_ctx: ContextId, pub parent_fn_hash: Word, @@ -464,7 +504,11 @@ pub struct ExecutionContextSystemInfo { /// [`crate::TraceGenerationContext`] that owns this replay. This avoids holding a strong /// `Arc` reference per resolution, allowing the trace generation context to deduplicate /// forests across fragments. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct MastForestResolutionReplay { mast_forest_resolutions: VecDeque<(MastNodeId, MastForestId)>, } @@ -503,7 +547,11 @@ impl MastForestResolutionReplay { /// addresses that they were recorded at. This works naturally since the fast processor has exactly /// the same access patterns as the main trace generators (which re-executes part of the program). /// The read methods include debug assertions to verify address consistency. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct MemoryReadsReplay { elements_read: VecDeque<(Felt, Felt, ContextId, RowIndex)>, words_read: VecDeque<(Word, Felt, ContextId, RowIndex)>, @@ -568,7 +616,11 @@ impl MemoryReadsReplay { /// /// This is separated from [MemoryReadsReplay] since writes are not needed for core trace generation /// (as reads are), but only to be able to fully build the memory chiplet trace. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct MemoryWritesReplay { elements_written: VecDeque<(Felt, Felt, ContextId, RowIndex)>, words_written: VecDeque<(Word, Felt, ContextId, RowIndex)>, @@ -654,7 +706,11 @@ impl MemoryInterface for MemoryReadsReplay { /// that return the pre-recorded results. This works naturally since the fast processor has exactly /// the same access patterns as the main trace generators (which re-executes part of the program). /// The read methods include debug assertions to verify parameter consistency. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct AdviceReplay { // Stack operations stack_pops: VecDeque, @@ -745,14 +801,22 @@ impl AdviceProviderInterface for AdviceReplay { // ================================================================================================ /// Enum representing the different bitwise operations that can be recorded. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum BitwiseOp { U32And, U32Xor, } /// Replay data for bitwise operations. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct BitwiseReplay { u32op_with_operands: VecDeque<(BitwiseOp, Felt, Felt)>, } @@ -786,7 +850,11 @@ impl IntoIterator for BitwiseReplay { // ================================================================================================ /// Replay data for kernel operations. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct KernelReplay { kernel_proc_accesses: VecDeque, } @@ -815,7 +883,11 @@ impl IntoIterator for KernelReplay { // ================================================================================================ /// Replay data for ACE operations. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct AceReplay { circuit_evaluations: VecDeque<(RowIndex, CircuitEvaluation)>, } @@ -847,7 +919,11 @@ impl IntoIterator for AceReplay { /// Replay data for range checking operations. /// /// This currently only records -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct RangeCheckerReplay { range_checks_u32_ops: VecDeque<[u16; 4]>, } @@ -875,7 +951,11 @@ impl IntoIterator for RangeCheckerReplay { // BLOCK ADDRESS REPLAY // ================================================================================================ -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct BlockAddressReplay { /// Recorded hasher addresses from operations like hash_control_block, hash_basic_block, etc. block_addresses: VecDeque, @@ -905,7 +985,11 @@ impl BlockAddressReplay { /// /// The hasher responses are recorded during fast processor execution and then replayed during core /// trace generation. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct HasherResponseReplay { /// Recorded hasher operations from permutation operations (HPerm). /// @@ -1012,7 +1096,11 @@ impl HasherInterface for HasherResponseReplay { /// Enum representing the different hasher operations that can be recorded, along with their /// operands. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub enum HasherOp { Permute([Felt; STATE_WIDTH]), HashControlBlock((Word, Word, Felt, Word)), @@ -1028,7 +1116,11 @@ pub enum HasherOp { /// /// The hasher requests are recorded during fast processor execution and then replayed during hasher /// chiplet trace generation. -#[derive(Debug, Default)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct HasherRequestReplay { hasher_ops: VecDeque, } @@ -1113,7 +1205,11 @@ impl IntoIterator for HasherRequestReplay { /// the clock cycle of the last overflow update) and provides replay methods that return the /// pre-recorded values. This works naturally since the fast processor has exactly the same /// access patterns as the main trace generators. -#[derive(Debug)] +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] +#[derive(Debug, PartialEq, Eq)] pub struct StackOverflowReplay { /// Recorded overflow values and overflow addresses from pop_overflow operations. Each entry /// represents a value that was popped from the overflow stack, and the overflow address of the @@ -1775,6 +1871,417 @@ impl Deserializable for ExecutionReplay { } } +#[cfg(feature = "arbitrary")] +mod arbitrary { + use alloc::collections::VecDeque; + + use miden_air::trace::chiplets::hasher::STATE_WIDTH; + use proptest::{collection, prelude::*}; + + use super::*; + + const MAX_REPLAY_ITEMS: usize = 8; + + fn arb_felt() -> impl Strategy { + any::().prop_map(Felt::from_u32) + } + + fn arb_word() -> impl Strategy { + any::<[u32; 4]>().prop_map(|values| values.map(Felt::from_u32).into()) + } + + fn arb_row_index() -> impl Strategy { + any::().prop_map(RowIndex::from) + } + + fn arb_merkle_path() -> impl Strategy { + collection::vec(arb_word(), 0..=MAX_REPLAY_ITEMS).prop_map(MerklePath::new) + } + + fn arb_vec_deque(strategy: S) -> impl Strategy> + where + T: core::fmt::Debug + 'static, + S: Strategy + 'static, + { + collection::vec(strategy, 0..=MAX_REPLAY_ITEMS).prop_map(VecDeque::from) + } + + fn arb_felt_array() -> impl Strategy { + any::<[u32; N]>().prop_map(|values| values.map(Felt::from_u32)) + } + + fn arb_word_pair() -> impl Strategy { + (arb_word(), arb_word()).prop_map(|(first, second)| [first, second]) + } + + impl Arbitrary for SystemState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_row_index(), any::(), arb_word(), arb_word()) + .prop_map(|(clk, ctx, fn_hash, pc_transcript_state)| Self { + clk, + ctx, + fn_hash, + pc_transcript_state, + }) + .boxed() + } + } + + impl Arbitrary for DecoderState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_felt(), arb_felt()) + .prop_map(|(current_addr, parent_addr)| Self { current_addr, parent_addr }) + .boxed() + } + } + + impl Arbitrary for StackState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_felt_array::(), + MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, + arb_felt(), + ) + .prop_map(|(stack_top, stack_depth, last_overflow_addr)| Self { + stack_top, + stack_depth, + last_overflow_addr, + }) + .boxed() + } + } + + impl Arbitrary for CoreTraceState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), any::(), any::()) + .prop_map(|(system, decoder, stack)| Self { system, decoder, stack }) + .boxed() + } + } + + impl Arbitrary for CoreTraceFragmentContext { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::>(), + any::(), + ) + .prop_map(|(state, replay, continuation, initial_mast_forest_id)| Self { + state, + replay, + continuation, + initial_mast_forest_id, + }) + .boxed() + } + } + + impl Arbitrary for NodeEndData { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_felt(), arb_felt(), arb_felt()) + .prop_map(|(ended_node_addr, prev_addr, prev_parent_addr)| Self { + ended_node_addr, + prev_addr, + prev_parent_addr, + }) + .boxed() + } + } + + impl Arbitrary for ExecutionContextSystemInfo { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), arb_word()) + .prop_map(|(parent_ctx, parent_fn_hash)| Self { parent_ctx, parent_fn_hash }) + .boxed() + } + } + + impl Arbitrary for ExecutionContextReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::()) + .prop_map(|execution_contexts| Self { execution_contexts }) + .boxed() + } + } + + impl Arbitrary for BlockStackReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_vec_deque(arb_felt()), arb_vec_deque(any::())) + .prop_map(|(node_start_parent_addr, node_end)| Self { + node_start_parent_addr, + node_end, + }) + .boxed() + } + } + + impl Arbitrary for MastForestResolutionReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque((any::(), any::())) + .prop_map(|mast_forest_resolutions| Self { mast_forest_resolutions }) + .boxed() + } + } + + impl Arbitrary for MemoryReadsReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((arb_felt(), arb_felt(), any::(), arb_row_index())), + arb_vec_deque((arb_word(), arb_felt(), any::(), arb_row_index())), + ) + .prop_map(|(elements_read, words_read)| Self { elements_read, words_read }) + .boxed() + } + } + + impl Arbitrary for MemoryWritesReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((arb_felt(), arb_felt(), any::(), arb_row_index())), + arb_vec_deque((arb_word(), arb_felt(), any::(), arb_row_index())), + ) + .prop_map(|(elements_written, words_written)| Self { + elements_written, + words_written, + }) + .boxed() + } + } + + impl Arbitrary for AdviceReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque(arb_felt()), + arb_vec_deque(arb_word()), + arb_vec_deque(arb_word_pair()), + ) + .prop_map(|(stack_pops, stack_word_pops, stack_dword_pops)| Self { + stack_pops, + stack_word_pops, + stack_dword_pops, + }) + .boxed() + } + } + + impl Arbitrary for BitwiseOp { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![Just(Self::U32And), Just(Self::U32Xor)].boxed() + } + } + + impl Arbitrary for BitwiseReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque((any::(), arb_felt(), arb_felt())) + .prop_map(|u32op_with_operands| Self { u32op_with_operands }) + .boxed() + } + } + + impl Arbitrary for KernelReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(arb_word()) + .prop_map(|kernel_proc_accesses| Self { kernel_proc_accesses }) + .boxed() + } + } + + impl Arbitrary for RangeCheckerReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::<[u16; 4]>()) + .prop_map(|range_checks_u32_ops| Self { range_checks_u32_ops }) + .boxed() + } + } + + impl Arbitrary for BlockAddressReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(arb_felt()) + .prop_map(|block_addresses| Self { block_addresses }) + .boxed() + } + } + + impl Arbitrary for HasherResponseReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((arb_felt(), arb_felt_array::())), + arb_vec_deque((arb_felt(), arb_word())), + arb_vec_deque((arb_felt(), arb_word(), arb_word())), + ) + .prop_map( + |( + permutation_operations, + build_merkle_root_operations, + mrupdate_operations, + )| { + Self { + permutation_operations, + build_merkle_root_operations, + mrupdate_operations, + } + }, + ) + .boxed() + } + } + + impl Arbitrary for HasherOp { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + arb_felt_array::().prop_map(Self::Permute), + (arb_word(), arb_word(), arb_felt(), arb_word()).prop_map(Self::HashControlBlock), + (any::(), any::(), arb_word()) + .prop_map(Self::HashBasicBlock), + (arb_word(), arb_merkle_path(), arb_felt()).prop_map(Self::BuildMerkleRoot), + (arb_word(), arb_word(), arb_merkle_path(), arb_felt()) + .prop_map(Self::UpdateMerkleRoot), + ] + .boxed() + } + } + + impl Arbitrary for HasherRequestReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::()) + .prop_map(|hasher_ops| Self { hasher_ops }) + .boxed() + } + } + + impl Arbitrary for StackOverflowReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((arb_felt(), arb_felt())), + arb_vec_deque((MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, arb_felt())), + ) + .prop_map(|(overflow_values, restore_context_info)| Self { + overflow_values, + restore_context_info, + }) + .boxed() + } + } + + impl Arbitrary for ExecutionReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::(), + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map( + |( + block_stack, + execution_context, + stack_overflow, + memory_reads, + advice, + hasher, + block_address, + mast_forest_resolution, + )| Self { + block_stack, + execution_context, + stack_overflow, + memory_reads, + advice, + hasher, + block_address, + mast_forest_resolution, + }, + ) + .boxed() + } + } + + impl Arbitrary for AceReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque((arb_row_index(), any::())) + .prop_map(|circuit_evaluations| Self { circuit_evaluations }) + .boxed() + } + } +} + #[cfg(test)] mod serialization_tests { use alloc::vec::Vec; diff --git a/prover/Cargo.toml b/prover/Cargo.toml index 95f8844302..1ef17b8541 100644 --- a/prover/Cargo.toml +++ b/prover/Cargo.toml @@ -14,6 +14,7 @@ rust-version.workspace = true edition.workspace = true [features] +arbitrary = ["dep:proptest"] default = ["std"] concurrent = ["std", "miden-air/concurrent", "miden-crypto/concurrent", "miden-processor/concurrent"] std = ["miden-air/std", "miden-debug-types/std", "miden-processor/std"] @@ -26,6 +27,7 @@ miden-processor.workspace = true miden-crypto.workspace = true # External dependencies +proptest = { workspace = true, optional = true } serde.workspace = true serde-wincode.workspace = true tracing.workspace = true @@ -34,4 +36,5 @@ wincode.workspace = true [dev-dependencies] miden-assembly.workspace = true miden-debug-types.workspace = true +miden-test-serde-macros.workspace = true tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/prover/src/proving_options.rs b/prover/src/proving_options.rs index 497800a2f6..309d6b289e 100644 --- a/prover/src/proving_options.rs +++ b/prover/src/proving_options.rs @@ -11,6 +11,10 @@ use miden_core::{ /// This struct stores the proof-generation hash function only. The actual STARK proving parameters /// (FRI config, security level, etc.) are determined by the hash function and hardcoded in the /// prover's config module. +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] #[derive(Debug, Clone, Eq, PartialEq)] pub struct ProvingOptions { hash_fn: HashFunction, @@ -59,3 +63,23 @@ impl Deserializable for ProvingOptions { Ok(Self::new(HashFunction::read_from(source)?)) } } + +#[cfg(feature = "arbitrary")] +impl proptest::prelude::Arbitrary for ProvingOptions { + type Parameters = (); + type Strategy = proptest::prelude::BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use proptest::prelude::*; + (0u8..5) + .prop_map(|tag| match tag { + 0 => HashFunction::Blake3_256, + 1 => HashFunction::Rpo256, + 2 => HashFunction::Rpx256, + 3 => HashFunction::Poseidon2, + _ => HashFunction::Keccak, + }) + .prop_map(Self::new) + .boxed() + } +} From cd6c0deeb969cae33b1a69cb87932daa61233dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 12:31:26 -0400 Subject: [PATCH 03/12] Wire crypto testing for arbitrary impls --- Cargo.lock | 2 ++ core/Cargo.toml | 8 +++++++- processor/src/trace/trace_state.rs | 11 +++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2397f3522d..c3f5ad4f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1468,6 +1468,7 @@ dependencies = [ "p3-maybe-rayon", "p3-symmetric", "p3-util", + "proptest", "rand 0.10.1", "rand_chacha 0.10.0", "rayon", @@ -1521,6 +1522,7 @@ dependencies = [ "p3-goldilocks", "p3-util", "paste", + "proptest", "rand 0.10.1", "serde", "subtle", diff --git a/core/Cargo.toml b/core/Cargo.toml index 8af4bc43d4..175c55141f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -44,7 +44,13 @@ serde = [ "miden-debug-types/serde", "miden-utils-indexing/serde", ] -arbitrary = ["dep:proptest"] +arbitrary = [ + "dep:proptest", + # TODO: switch to miden-crypto/arbitrary once it exists. For now, the crypto-owned + # Arbitrary impls for Felt and Word are exposed by testing: + # https://github.com/0xMiden/crypto/issues/1071 + "miden-crypto/testing", +] testing = ["arbitrary"] fuzzing = [] diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 079014a8a4..5e1e87dc28 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -1514,6 +1514,10 @@ impl Deserializable for ExecutionContextSystemInfo { parent_fn_hash: Word::read_from(source)?, }) } + + fn min_serialized_size() -> usize { + ContextId::min_serialized_size() + Word::min_serialized_size() + } } impl Serializable for ExecutionContextReplay { @@ -1813,6 +1817,13 @@ impl Deserializable for HasherOp { ))), } } + + fn min_serialized_size() -> usize { + u8::min_serialized_size() + + (MastForestId::min_serialized_size() + + MastNodeId::min_serialized_size() + + Word::min_serialized_size()) + } } impl Serializable for HasherRequestReplay { From 948432dedc77115d8cfe7e79e82b2b565493956e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 16:56:37 -0400 Subject: [PATCH 04/12] Use upstream arbitrary strategies in trace tests --- processor/src/trace/trace_state.rs | 75 ++++++++++++------------------ 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 5e1e87dc28..eb7dc84151 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -1893,20 +1893,14 @@ mod arbitrary { const MAX_REPLAY_ITEMS: usize = 8; - fn arb_felt() -> impl Strategy { - any::().prop_map(Felt::from_u32) - } - - fn arb_word() -> impl Strategy { - any::<[u32; 4]>().prop_map(|values| values.map(Felt::from_u32).into()) - } - fn arb_row_index() -> impl Strategy { any::().prop_map(RowIndex::from) } + // TODO: use any::() once miden-crypto exposes its impl outside test code: + // https://github.com/0xMiden/crypto/issues/1072 fn arb_merkle_path() -> impl Strategy { - collection::vec(arb_word(), 0..=MAX_REPLAY_ITEMS).prop_map(MerklePath::new) + collection::vec(any::(), 0..=MAX_REPLAY_ITEMS).prop_map(MerklePath::new) } fn arb_vec_deque(strategy: S) -> impl Strategy> @@ -1917,20 +1911,12 @@ mod arbitrary { collection::vec(strategy, 0..=MAX_REPLAY_ITEMS).prop_map(VecDeque::from) } - fn arb_felt_array() -> impl Strategy { - any::<[u32; N]>().prop_map(|values| values.map(Felt::from_u32)) - } - - fn arb_word_pair() -> impl Strategy { - (arb_word(), arb_word()).prop_map(|(first, second)| [first, second]) - } - impl Arbitrary for SystemState { type Parameters = (); type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (arb_row_index(), any::(), arb_word(), arb_word()) + (arb_row_index(), any::(), any::(), any::()) .prop_map(|(clk, ctx, fn_hash, pc_transcript_state)| Self { clk, ctx, @@ -1946,7 +1932,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (arb_felt(), arb_felt()) + (any::(), any::()) .prop_map(|(current_addr, parent_addr)| Self { current_addr, parent_addr }) .boxed() } @@ -1958,9 +1944,9 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - arb_felt_array::(), + any::<[Felt; MIN_STACK_DEPTH]>(), MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, - arb_felt(), + any::(), ) .prop_map(|(stack_top, stack_depth, last_overflow_addr)| Self { stack_top, @@ -2008,7 +1994,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (arb_felt(), arb_felt(), arb_felt()) + (any::(), any::(), any::()) .prop_map(|(ended_node_addr, prev_addr, prev_parent_addr)| Self { ended_node_addr, prev_addr, @@ -2023,7 +2009,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), arb_word()) + (any::(), any::()) .prop_map(|(parent_ctx, parent_fn_hash)| Self { parent_ctx, parent_fn_hash }) .boxed() } @@ -2045,7 +2031,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (arb_vec_deque(arb_felt()), arb_vec_deque(any::())) + (arb_vec_deque(any::()), arb_vec_deque(any::())) .prop_map(|(node_start_parent_addr, node_end)| Self { node_start_parent_addr, node_end, @@ -2071,8 +2057,8 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - arb_vec_deque((arb_felt(), arb_felt(), any::(), arb_row_index())), - arb_vec_deque((arb_word(), arb_felt(), any::(), arb_row_index())), + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), ) .prop_map(|(elements_read, words_read)| Self { elements_read, words_read }) .boxed() @@ -2085,8 +2071,8 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - arb_vec_deque((arb_felt(), arb_felt(), any::(), arb_row_index())), - arb_vec_deque((arb_word(), arb_felt(), any::(), arb_row_index())), + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), ) .prop_map(|(elements_written, words_written)| Self { elements_written, @@ -2102,9 +2088,9 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - arb_vec_deque(arb_felt()), - arb_vec_deque(arb_word()), - arb_vec_deque(arb_word_pair()), + arb_vec_deque(any::()), + arb_vec_deque(any::()), + arb_vec_deque(any::<[Word; 2]>()), ) .prop_map(|(stack_pops, stack_word_pops, stack_dword_pops)| Self { stack_pops, @@ -2129,7 +2115,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque((any::(), arb_felt(), arb_felt())) + arb_vec_deque((any::(), any::(), any::())) .prop_map(|u32op_with_operands| Self { u32op_with_operands }) .boxed() } @@ -2140,7 +2126,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(arb_word()) + arb_vec_deque(any::()) .prop_map(|kernel_proc_accesses| Self { kernel_proc_accesses }) .boxed() } @@ -2162,7 +2148,7 @@ mod arbitrary { type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(arb_felt()) + arb_vec_deque(any::()) .prop_map(|block_addresses| Self { block_addresses }) .boxed() } @@ -2174,9 +2160,9 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - arb_vec_deque((arb_felt(), arb_felt_array::())), - arb_vec_deque((arb_felt(), arb_word())), - arb_vec_deque((arb_felt(), arb_word(), arb_word())), + arb_vec_deque((any::(), any::<[Felt; STATE_WIDTH]>())), + arb_vec_deque((any::(), any::())), + arb_vec_deque((any::(), any::(), any::())), ) .prop_map( |( @@ -2201,12 +2187,13 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { prop_oneof![ - arb_felt_array::().prop_map(Self::Permute), - (arb_word(), arb_word(), arb_felt(), arb_word()).prop_map(Self::HashControlBlock), - (any::(), any::(), arb_word()) + any::<[Felt; STATE_WIDTH]>().prop_map(Self::Permute), + (any::(), any::(), any::(), any::()) + .prop_map(Self::HashControlBlock), + (any::(), any::(), any::()) .prop_map(Self::HashBasicBlock), - (arb_word(), arb_merkle_path(), arb_felt()).prop_map(Self::BuildMerkleRoot), - (arb_word(), arb_word(), arb_merkle_path(), arb_felt()) + (any::(), arb_merkle_path(), any::()).prop_map(Self::BuildMerkleRoot), + (any::(), any::(), arb_merkle_path(), any::()) .prop_map(Self::UpdateMerkleRoot), ] .boxed() @@ -2230,8 +2217,8 @@ mod arbitrary { fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { ( - arb_vec_deque((arb_felt(), arb_felt())), - arb_vec_deque((MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, arb_felt())), + arb_vec_deque((any::(), any::())), + arb_vec_deque((MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, any::())), ) .prop_map(|(overflow_values, restore_context_info)| Self { overflow_values, From 0139d6b910fc3b419236fae4acaefe5e5eaa4c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 18:01:35 -0400 Subject: [PATCH 05/12] Split trace state tests --- processor/src/trace/trace_state.rs | 534 +----------------- processor/src/trace/trace_state/arbitrary.rs | 387 +++++++++++++ .../trace/trace_state/serialization_tests.rs | 121 ++++ 3 files changed, 510 insertions(+), 532 deletions(-) create mode 100644 processor/src/trace/trace_state/arbitrary.rs create mode 100644 processor/src/trace/trace_state/serialization_tests.rs diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index eb7dc84151..17cd2eb708 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -1883,537 +1883,7 @@ impl Deserializable for ExecutionReplay { } #[cfg(feature = "arbitrary")] -mod arbitrary { - use alloc::collections::VecDeque; - - use miden_air::trace::chiplets::hasher::STATE_WIDTH; - use proptest::{collection, prelude::*}; - - use super::*; - - const MAX_REPLAY_ITEMS: usize = 8; - - fn arb_row_index() -> impl Strategy { - any::().prop_map(RowIndex::from) - } - - // TODO: use any::() once miden-crypto exposes its impl outside test code: - // https://github.com/0xMiden/crypto/issues/1072 - fn arb_merkle_path() -> impl Strategy { - collection::vec(any::(), 0..=MAX_REPLAY_ITEMS).prop_map(MerklePath::new) - } - - fn arb_vec_deque(strategy: S) -> impl Strategy> - where - T: core::fmt::Debug + 'static, - S: Strategy + 'static, - { - collection::vec(strategy, 0..=MAX_REPLAY_ITEMS).prop_map(VecDeque::from) - } - - impl Arbitrary for SystemState { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (arb_row_index(), any::(), any::(), any::()) - .prop_map(|(clk, ctx, fn_hash, pc_transcript_state)| Self { - clk, - ctx, - fn_hash, - pc_transcript_state, - }) - .boxed() - } - } - - impl Arbitrary for DecoderState { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), any::()) - .prop_map(|(current_addr, parent_addr)| Self { current_addr, parent_addr }) - .boxed() - } - } - - impl Arbitrary for StackState { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - any::<[Felt; MIN_STACK_DEPTH]>(), - MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, - any::(), - ) - .prop_map(|(stack_top, stack_depth, last_overflow_addr)| Self { - stack_top, - stack_depth, - last_overflow_addr, - }) - .boxed() - } - } - - impl Arbitrary for CoreTraceState { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), any::(), any::()) - .prop_map(|(system, decoder, stack)| Self { system, decoder, stack }) - .boxed() - } - } - - impl Arbitrary for CoreTraceFragmentContext { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - any::(), - any::(), - any::>(), - any::(), - ) - .prop_map(|(state, replay, continuation, initial_mast_forest_id)| Self { - state, - replay, - continuation, - initial_mast_forest_id, - }) - .boxed() - } - } - - impl Arbitrary for NodeEndData { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), any::(), any::()) - .prop_map(|(ended_node_addr, prev_addr, prev_parent_addr)| Self { - ended_node_addr, - prev_addr, - prev_parent_addr, - }) - .boxed() - } - } - - impl Arbitrary for ExecutionContextSystemInfo { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (any::(), any::()) - .prop_map(|(parent_ctx, parent_fn_hash)| Self { parent_ctx, parent_fn_hash }) - .boxed() - } - } - - impl Arbitrary for ExecutionContextReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(any::()) - .prop_map(|execution_contexts| Self { execution_contexts }) - .boxed() - } - } - - impl Arbitrary for BlockStackReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - (arb_vec_deque(any::()), arb_vec_deque(any::())) - .prop_map(|(node_start_parent_addr, node_end)| Self { - node_start_parent_addr, - node_end, - }) - .boxed() - } - } - - impl Arbitrary for MastForestResolutionReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque((any::(), any::())) - .prop_map(|mast_forest_resolutions| Self { mast_forest_resolutions }) - .boxed() - } - } - - impl Arbitrary for MemoryReadsReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - arb_vec_deque((any::(), any::(), any::(), arb_row_index())), - arb_vec_deque((any::(), any::(), any::(), arb_row_index())), - ) - .prop_map(|(elements_read, words_read)| Self { elements_read, words_read }) - .boxed() - } - } - - impl Arbitrary for MemoryWritesReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - arb_vec_deque((any::(), any::(), any::(), arb_row_index())), - arb_vec_deque((any::(), any::(), any::(), arb_row_index())), - ) - .prop_map(|(elements_written, words_written)| Self { - elements_written, - words_written, - }) - .boxed() - } - } - - impl Arbitrary for AdviceReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - arb_vec_deque(any::()), - arb_vec_deque(any::()), - arb_vec_deque(any::<[Word; 2]>()), - ) - .prop_map(|(stack_pops, stack_word_pops, stack_dword_pops)| Self { - stack_pops, - stack_word_pops, - stack_dword_pops, - }) - .boxed() - } - } - - impl Arbitrary for BitwiseOp { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - prop_oneof![Just(Self::U32And), Just(Self::U32Xor)].boxed() - } - } - - impl Arbitrary for BitwiseReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque((any::(), any::(), any::())) - .prop_map(|u32op_with_operands| Self { u32op_with_operands }) - .boxed() - } - } - - impl Arbitrary for KernelReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(any::()) - .prop_map(|kernel_proc_accesses| Self { kernel_proc_accesses }) - .boxed() - } - } - - impl Arbitrary for RangeCheckerReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(any::<[u16; 4]>()) - .prop_map(|range_checks_u32_ops| Self { range_checks_u32_ops }) - .boxed() - } - } - - impl Arbitrary for BlockAddressReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(any::()) - .prop_map(|block_addresses| Self { block_addresses }) - .boxed() - } - } - - impl Arbitrary for HasherResponseReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - arb_vec_deque((any::(), any::<[Felt; STATE_WIDTH]>())), - arb_vec_deque((any::(), any::())), - arb_vec_deque((any::(), any::(), any::())), - ) - .prop_map( - |( - permutation_operations, - build_merkle_root_operations, - mrupdate_operations, - )| { - Self { - permutation_operations, - build_merkle_root_operations, - mrupdate_operations, - } - }, - ) - .boxed() - } - } - - impl Arbitrary for HasherOp { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - prop_oneof![ - any::<[Felt; STATE_WIDTH]>().prop_map(Self::Permute), - (any::(), any::(), any::(), any::()) - .prop_map(Self::HashControlBlock), - (any::(), any::(), any::()) - .prop_map(Self::HashBasicBlock), - (any::(), arb_merkle_path(), any::()).prop_map(Self::BuildMerkleRoot), - (any::(), any::(), arb_merkle_path(), any::()) - .prop_map(Self::UpdateMerkleRoot), - ] - .boxed() - } - } - - impl Arbitrary for HasherRequestReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque(any::()) - .prop_map(|hasher_ops| Self { hasher_ops }) - .boxed() - } - } - - impl Arbitrary for StackOverflowReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - arb_vec_deque((any::(), any::())), - arb_vec_deque((MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, any::())), - ) - .prop_map(|(overflow_values, restore_context_info)| Self { - overflow_values, - restore_context_info, - }) - .boxed() - } - } - - impl Arbitrary for ExecutionReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - ( - any::(), - any::(), - any::(), - any::(), - any::(), - any::(), - any::(), - any::(), - ) - .prop_map( - |( - block_stack, - execution_context, - stack_overflow, - memory_reads, - advice, - hasher, - block_address, - mast_forest_resolution, - )| Self { - block_stack, - execution_context, - stack_overflow, - memory_reads, - advice, - hasher, - block_address, - mast_forest_resolution, - }, - ) - .boxed() - } - } - - impl Arbitrary for AceReplay { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - arb_vec_deque((arb_row_index(), any::())) - .prop_map(|circuit_evaluations| Self { circuit_evaluations }) - .boxed() - } - } -} +mod arbitrary; #[cfg(test)] -mod serialization_tests { - use alloc::vec::Vec; - - use miden_core::serde::{BudgetedReader, SliceReader}; - - use super::*; - use crate::mast::MastForestId; - - #[test] - fn memory_replay_queues_reject_oversized_lengths_before_allocation() { - let mut bytes = Vec::new(); - usize::MAX.write_into(&mut bytes); - - let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); - let err = read_memory_element_queue(&mut reader).unwrap_err(); - assert!(err.to_string().contains("exceeds reader allocation bound")); - - let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); - let err = read_memory_word_queue(&mut reader).unwrap_err(); - assert!(err.to_string().contains("exceeds reader allocation bound")); - } - - #[test] - fn stack_state_read_rejects_depth_below_minimum() { - let mut bytes = Vec::new(); - [ZERO; MIN_STACK_DEPTH].write_into(&mut bytes); - (MIN_STACK_DEPTH - 1).write_into(&mut bytes); - ZERO.write_into(&mut bytes); - - let err = StackState::read_from_bytes(&bytes).unwrap_err(); - assert!(err.to_string().contains("below minimum")); - } - - #[test] - fn execution_replay_round_trip_preserves_replay_order() { - let mut replay = ExecutionReplay::default(); - replay.block_stack.record_node_start_parent_addr(Felt::from_u32(7)); - replay - .block_stack - .record_node_end(Felt::from_u32(9), Felt::from_u32(8), Felt::from_u32(7)); - replay.execution_context.record_execution_context(ExecutionContextSystemInfo { - parent_ctx: ContextId::from(3), - parent_fn_hash: Word::from([ONE, ZERO, ZERO, ZERO]), - }); - replay - .stack_overflow - .record_pop_overflow(Felt::from_u32(11), Felt::from_u32(10)); - replay - .stack_overflow - .record_restore_context_overflow_addr(17, Felt::from_u32(16)); - replay.memory_reads.record_read_element( - Felt::from_u32(21), - Felt::from_u32(20), - ContextId::from(4), - RowIndex::from(5_u32), - ); - replay.memory_reads.record_read_word( - Word::from([ - Felt::from_u32(1), - Felt::from_u32(2), - Felt::from_u32(3), - Felt::from_u32(4), - ]), - Felt::from_u32(24), - ContextId::from(4), - RowIndex::from(6_u32), - ); - replay.advice.record_pop_stack(Felt::from_u32(31)); - replay - .advice - .record_pop_stack_word(Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO])); - replay.advice.record_pop_stack_dword([ - Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), - Word::default(), - ]); - replay.hasher.record_permute(Felt::from_u32(40), [Felt::from_u32(41); 12]); - replay.block_address.record_block_address(Felt::from_u32(50)); - replay - .mast_forest_resolution - .record_resolution(MastNodeId::from(2), MastForestId::from(1)); - - let bytes = replay.to_bytes(); - let mut restored = ExecutionReplay::read_from_bytes(&bytes).unwrap(); - - assert_eq!( - restored.block_stack.replay_node_start_parent_addr().unwrap(), - Felt::from_u32(7) - ); - let node_end = restored.block_stack.replay_node_end().unwrap(); - assert_eq!(node_end.ended_node_addr, Felt::from_u32(9)); - assert_eq!(node_end.prev_addr, Felt::from_u32(8)); - assert_eq!(node_end.prev_parent_addr, Felt::from_u32(7)); - let ctx = restored.execution_context.replay_execution_context().unwrap(); - assert_eq!(ctx.parent_ctx, ContextId::from(3)); - assert_eq!(ctx.parent_fn_hash, Word::from([ONE, ZERO, ZERO, ZERO])); - assert_eq!( - restored.stack_overflow.replay_pop_overflow().unwrap(), - (Felt::from_u32(11), Felt::from_u32(10)) - ); - assert_eq!( - restored.stack_overflow.replay_restore_context_overflow_addr().unwrap(), - (17, Felt::from_u32(16)) - ); - assert_eq!( - restored.memory_reads.replay_read_element(Felt::from_u32(20)).unwrap(), - Felt::from_u32(21) - ); - assert_eq!( - restored.memory_reads.replay_read_word(Felt::from_u32(24)).unwrap(), - Word::from([ - Felt::from_u32(1), - Felt::from_u32(2), - Felt::from_u32(3), - Felt::from_u32(4) - ]) - ); - assert_eq!(restored.advice.replay_pop_stack().unwrap(), Felt::from_u32(31)); - assert_eq!( - restored.advice.replay_pop_stack_word().unwrap(), - Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO]) - ); - assert_eq!( - restored.advice.replay_pop_stack_dword().unwrap(), - [Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), Word::default()] - ); - assert_eq!( - restored.hasher.replay_permute().unwrap(), - (Felt::from_u32(40), [Felt::from_u32(41); 12]) - ); - assert_eq!(restored.block_address.replay_block_address().unwrap(), Felt::from_u32(50)); - assert_eq!( - restored.mast_forest_resolution.replay_resolution().unwrap(), - (MastNodeId::from(2), MastForestId::from(1)) - ); - } -} +mod serialization_tests; diff --git a/processor/src/trace/trace_state/arbitrary.rs b/processor/src/trace/trace_state/arbitrary.rs new file mode 100644 index 0000000000..0615a2315f --- /dev/null +++ b/processor/src/trace/trace_state/arbitrary.rs @@ -0,0 +1,387 @@ +use alloc::collections::VecDeque; + +use miden_air::trace::chiplets::hasher::STATE_WIDTH; +use proptest::{collection, prelude::*}; + +use super::*; + +const MAX_REPLAY_ITEMS: usize = 8; + +fn arb_row_index() -> impl Strategy { + any::().prop_map(RowIndex::from) +} + +// TODO: use any::() once miden-crypto exposes its impl outside test code: +// https://github.com/0xMiden/crypto/issues/1072 +fn arb_merkle_path() -> impl Strategy { + collection::vec(any::(), 0..=MAX_REPLAY_ITEMS).prop_map(MerklePath::new) +} + +fn arb_vec_deque(strategy: S) -> impl Strategy> +where + T: core::fmt::Debug + 'static, + S: Strategy + 'static, +{ + collection::vec(strategy, 0..=MAX_REPLAY_ITEMS).prop_map(VecDeque::from) +} + +impl Arbitrary for SystemState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_row_index(), any::(), any::(), any::()) + .prop_map(|(clk, ctx, fn_hash, pc_transcript_state)| Self { + clk, + ctx, + fn_hash, + pc_transcript_state, + }) + .boxed() + } +} + +impl Arbitrary for DecoderState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), any::()) + .prop_map(|(current_addr, parent_addr)| Self { current_addr, parent_addr }) + .boxed() + } +} + +impl Arbitrary for StackState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::<[Felt; MIN_STACK_DEPTH]>(), + MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, + any::(), + ) + .prop_map(|(stack_top, stack_depth, last_overflow_addr)| Self { + stack_top, + stack_depth, + last_overflow_addr, + }) + .boxed() + } +} + +impl Arbitrary for CoreTraceState { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), any::(), any::()) + .prop_map(|(system, decoder, stack)| Self { system, decoder, stack }) + .boxed() + } +} + +impl Arbitrary for CoreTraceFragmentContext { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::>(), + any::(), + ) + .prop_map(|(state, replay, continuation, initial_mast_forest_id)| Self { + state, + replay, + continuation, + initial_mast_forest_id, + }) + .boxed() + } +} + +impl Arbitrary for NodeEndData { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), any::(), any::()) + .prop_map(|(ended_node_addr, prev_addr, prev_parent_addr)| Self { + ended_node_addr, + prev_addr, + prev_parent_addr, + }) + .boxed() + } +} + +impl Arbitrary for ExecutionContextSystemInfo { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (any::(), any::()) + .prop_map(|(parent_ctx, parent_fn_hash)| Self { parent_ctx, parent_fn_hash }) + .boxed() + } +} + +impl Arbitrary for ExecutionContextReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::()) + .prop_map(|execution_contexts| Self { execution_contexts }) + .boxed() + } +} + +impl Arbitrary for BlockStackReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (arb_vec_deque(any::()), arb_vec_deque(any::())) + .prop_map(|(node_start_parent_addr, node_end)| Self { + node_start_parent_addr, + node_end, + }) + .boxed() + } +} + +impl Arbitrary for MastForestResolutionReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque((any::(), any::())) + .prop_map(|mast_forest_resolutions| Self { mast_forest_resolutions }) + .boxed() + } +} + +impl Arbitrary for MemoryReadsReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), + ) + .prop_map(|(elements_read, words_read)| Self { elements_read, words_read }) + .boxed() + } +} + +impl Arbitrary for MemoryWritesReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), + arb_vec_deque((any::(), any::(), any::(), arb_row_index())), + ) + .prop_map(|(elements_written, words_written)| Self { elements_written, words_written }) + .boxed() + } +} + +impl Arbitrary for AdviceReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque(any::()), + arb_vec_deque(any::()), + arb_vec_deque(any::<[Word; 2]>()), + ) + .prop_map(|(stack_pops, stack_word_pops, stack_dword_pops)| Self { + stack_pops, + stack_word_pops, + stack_dword_pops, + }) + .boxed() + } +} + +impl Arbitrary for BitwiseOp { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![Just(Self::U32And), Just(Self::U32Xor)].boxed() + } +} + +impl Arbitrary for BitwiseReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque((any::(), any::(), any::())) + .prop_map(|u32op_with_operands| Self { u32op_with_operands }) + .boxed() + } +} + +impl Arbitrary for KernelReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::()) + .prop_map(|kernel_proc_accesses| Self { kernel_proc_accesses }) + .boxed() + } +} + +impl Arbitrary for RangeCheckerReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::<[u16; 4]>()) + .prop_map(|range_checks_u32_ops| Self { range_checks_u32_ops }) + .boxed() + } +} + +impl Arbitrary for BlockAddressReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::()) + .prop_map(|block_addresses| Self { block_addresses }) + .boxed() + } +} + +impl Arbitrary for HasherResponseReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((any::(), any::<[Felt; STATE_WIDTH]>())), + arb_vec_deque((any::(), any::())), + arb_vec_deque((any::(), any::(), any::())), + ) + .prop_map( + |(permutation_operations, build_merkle_root_operations, mrupdate_operations)| { + Self { + permutation_operations, + build_merkle_root_operations, + mrupdate_operations, + } + }, + ) + .boxed() + } +} + +impl Arbitrary for HasherOp { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + any::<[Felt; STATE_WIDTH]>().prop_map(Self::Permute), + (any::(), any::(), any::(), any::()) + .prop_map(Self::HashControlBlock), + (any::(), any::(), any::()) + .prop_map(Self::HashBasicBlock), + (any::(), arb_merkle_path(), any::()).prop_map(Self::BuildMerkleRoot), + (any::(), any::(), arb_merkle_path(), any::()) + .prop_map(Self::UpdateMerkleRoot), + ] + .boxed() + } +} + +impl Arbitrary for HasherRequestReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque(any::()) + .prop_map(|hasher_ops| Self { hasher_ops }) + .boxed() + } +} + +impl Arbitrary for StackOverflowReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + arb_vec_deque((any::(), any::())), + arb_vec_deque((MIN_STACK_DEPTH..=MIN_STACK_DEPTH + 64, any::())), + ) + .prop_map(|(overflow_values, restore_context_info)| Self { + overflow_values, + restore_context_info, + }) + .boxed() + } +} + +impl Arbitrary for ExecutionReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::(), + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map( + |( + block_stack, + execution_context, + stack_overflow, + memory_reads, + advice, + hasher, + block_address, + mast_forest_resolution, + )| Self { + block_stack, + execution_context, + stack_overflow, + memory_reads, + advice, + hasher, + block_address, + mast_forest_resolution, + }, + ) + .boxed() + } +} + +impl Arbitrary for AceReplay { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + arb_vec_deque((arb_row_index(), any::())) + .prop_map(|circuit_evaluations| Self { circuit_evaluations }) + .boxed() + } +} diff --git a/processor/src/trace/trace_state/serialization_tests.rs b/processor/src/trace/trace_state/serialization_tests.rs new file mode 100644 index 0000000000..3b89e46ca9 --- /dev/null +++ b/processor/src/trace/trace_state/serialization_tests.rs @@ -0,0 +1,121 @@ +use alloc::vec::Vec; + +use miden_core::serde::{BudgetedReader, SliceReader}; + +use super::*; +use crate::mast::MastForestId; + +#[test] +fn memory_replay_queues_reject_oversized_lengths_before_allocation() { + let mut bytes = Vec::new(); + usize::MAX.write_into(&mut bytes); + + let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); + let err = read_memory_element_queue(&mut reader).unwrap_err(); + assert!(err.to_string().contains("exceeds reader allocation bound")); + + let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); + let err = read_memory_word_queue(&mut reader).unwrap_err(); + assert!(err.to_string().contains("exceeds reader allocation bound")); +} + +#[test] +fn stack_state_read_rejects_depth_below_minimum() { + let mut bytes = Vec::new(); + [ZERO; MIN_STACK_DEPTH].write_into(&mut bytes); + (MIN_STACK_DEPTH - 1).write_into(&mut bytes); + ZERO.write_into(&mut bytes); + + let err = StackState::read_from_bytes(&bytes).unwrap_err(); + assert!(err.to_string().contains("below minimum")); +} + +#[test] +fn execution_replay_round_trip_preserves_replay_order() { + let mut replay = ExecutionReplay::default(); + replay.block_stack.record_node_start_parent_addr(Felt::from_u32(7)); + replay + .block_stack + .record_node_end(Felt::from_u32(9), Felt::from_u32(8), Felt::from_u32(7)); + replay.execution_context.record_execution_context(ExecutionContextSystemInfo { + parent_ctx: ContextId::from(3), + parent_fn_hash: Word::from([ONE, ZERO, ZERO, ZERO]), + }); + replay + .stack_overflow + .record_pop_overflow(Felt::from_u32(11), Felt::from_u32(10)); + replay + .stack_overflow + .record_restore_context_overflow_addr(17, Felt::from_u32(16)); + replay.memory_reads.record_read_element( + Felt::from_u32(21), + Felt::from_u32(20), + ContextId::from(4), + RowIndex::from(5_u32), + ); + replay.memory_reads.record_read_word( + Word::from([Felt::from_u32(1), Felt::from_u32(2), Felt::from_u32(3), Felt::from_u32(4)]), + Felt::from_u32(24), + ContextId::from(4), + RowIndex::from(6_u32), + ); + replay.advice.record_pop_stack(Felt::from_u32(31)); + replay + .advice + .record_pop_stack_word(Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO])); + replay.advice.record_pop_stack_dword([ + Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), + Word::default(), + ]); + replay.hasher.record_permute(Felt::from_u32(40), [Felt::from_u32(41); 12]); + replay.block_address.record_block_address(Felt::from_u32(50)); + replay + .mast_forest_resolution + .record_resolution(MastNodeId::from(2), MastForestId::from(1)); + + let bytes = replay.to_bytes(); + let mut restored = ExecutionReplay::read_from_bytes(&bytes).unwrap(); + + assert_eq!(restored.block_stack.replay_node_start_parent_addr().unwrap(), Felt::from_u32(7)); + let node_end = restored.block_stack.replay_node_end().unwrap(); + assert_eq!(node_end.ended_node_addr, Felt::from_u32(9)); + assert_eq!(node_end.prev_addr, Felt::from_u32(8)); + assert_eq!(node_end.prev_parent_addr, Felt::from_u32(7)); + let ctx = restored.execution_context.replay_execution_context().unwrap(); + assert_eq!(ctx.parent_ctx, ContextId::from(3)); + assert_eq!(ctx.parent_fn_hash, Word::from([ONE, ZERO, ZERO, ZERO])); + assert_eq!( + restored.stack_overflow.replay_pop_overflow().unwrap(), + (Felt::from_u32(11), Felt::from_u32(10)) + ); + assert_eq!( + restored.stack_overflow.replay_restore_context_overflow_addr().unwrap(), + (17, Felt::from_u32(16)) + ); + assert_eq!( + restored.memory_reads.replay_read_element(Felt::from_u32(20)).unwrap(), + Felt::from_u32(21) + ); + assert_eq!( + restored.memory_reads.replay_read_word(Felt::from_u32(24)).unwrap(), + Word::from([Felt::from_u32(1), Felt::from_u32(2), Felt::from_u32(3), Felt::from_u32(4)]) + ); + assert_eq!(restored.advice.replay_pop_stack().unwrap(), Felt::from_u32(31)); + assert_eq!( + restored.advice.replay_pop_stack_word().unwrap(), + Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO]) + ); + assert_eq!( + restored.advice.replay_pop_stack_dword().unwrap(), + [Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), Word::default()] + ); + assert_eq!( + restored.hasher.replay_permute().unwrap(), + (Felt::from_u32(40), [Felt::from_u32(41); 12]) + ); + assert_eq!(restored.block_address.replay_block_address().unwrap(), Felt::from_u32(50)); + assert_eq!( + restored.mast_forest_resolution.replay_resolution().unwrap(), + (MastNodeId::from(2), MastForestId::from(1)) + ); +} From 254aac92e42b43913945739ebc0ed62e62e5d44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 18:11:00 -0400 Subject: [PATCH 06/12] Clarify sparse replay serialization scope --- core/src/mast/serialization/mod.rs | 5 +- core/src/mast/serialization/sparse.rs | 5 +- core/src/mast/serialization/tests.rs | 104 ++++++++++++++++++++++ processor/src/trace/execution_tracer.rs | 109 ++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 3 deletions(-) diff --git a/core/src/mast/serialization/mod.rs b/core/src/mast/serialization/mod.rs index a239023e87..3950df3521 100644 --- a/core/src/mast/serialization/mod.rs +++ b/core/src/mast/serialization/mod.rs @@ -81,8 +81,9 @@ //! - [`MastForestWireView::new`]: trusted wire-backed cache access; rejects hashless and legacy //! debug-bearing payloads, and rejects sparse payloads. //! - [`crate::mast::SparseMastForest::read_from_bytes`] / -//! [`crate::mast::SparseMastForest::read_from_bytes_with_options`]: sparse replay payloads for -//! serialized trace-generation inputs. +//! [`crate::mast::SparseMastForest::read_from_bytes_with_options`]: trusted sparse replay +//! payloads for serialized trace-generation inputs. Sparse payloads currently carry full-node +//! digests and do not recompute them on read. //! - [`crate::mast::UntrustedMastForest::read_from_bytes`] / //! [`crate::mast::UntrustedMastForest::read_from_bytes_with_options`]: untrusted parsing plus //! later validation before use. diff --git a/core/src/mast/serialization/sparse.rs b/core/src/mast/serialization/sparse.rs index a296df31fb..e24fabae9e 100644 --- a/core/src/mast/serialization/sparse.rs +++ b/core/src/mast/serialization/sparse.rs @@ -26,7 +26,10 @@ fn sparse_mast_forest_min_serialized_size() -> usize { + usize::min_serialized_size() } -/// Serializes a [`SparseMastForest`] in sparse replay form. +/// Serializes a [`SparseMastForest`] in trusted sparse replay form. +/// +/// This format carries the digest for each full node and accepts those digests on read. It is +/// suitable for trusted remote proving inputs, not as an untrusted hashless validation path. pub(super) fn write_sparse_into(forest: &SparseMastForest, target: &mut W) { let mut basic_block_data_builder = BasicBlockDataBuilder::new(); let mut full_ids = Vec::with_capacity(forest.nodes().len()); diff --git a/core/src/mast/serialization/tests.rs b/core/src/mast/serialization/tests.rs index f1049eded3..097356fe34 100644 --- a/core/src/mast/serialization/tests.rs +++ b/core/src/mast/serialization/tests.rs @@ -1028,6 +1028,110 @@ fn sparse_serialized_parts_reject_duplicate_full_ids() { ); } +#[test] +fn sparse_serialized_parts_reject_duplicate_digest_only_ids() { + let (_source, sparse, _true_branch, false_branch, _root) = sparse_split_fixture(); + let nodes = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + let mut digests: Vec<_> = + sparse.digest_entries().iter().map(|(&id, &digest)| (id, digest)).collect(); + let digest = sparse.get_digest_by_id(false_branch).unwrap(); + digests.push((false_branch, digest)); + + let result = SparseMastForest::from_serialized_parts( + nodes, + digests, + sparse.num_nodes(), + sparse.procedure_roots().to_vec(), + sparse.advice_map().clone(), + sparse.commitment(), + ); + + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("duplicate sparse digest-only id") + ); +} + +#[test] +fn sparse_serialized_parts_reject_full_digest_overlap() { + let (_source, sparse, true_branch, _false_branch, _root) = sparse_split_fixture(); + let nodes = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + let mut digests: Vec<_> = + sparse.digest_entries().iter().map(|(&id, &digest)| (id, digest)).collect(); + digests.push((true_branch, sparse.get_digest_by_id(true_branch).unwrap())); + + let result = SparseMastForest::from_serialized_parts( + nodes, + digests, + sparse.num_nodes(), + sparse.procedure_roots().to_vec(), + sparse.advice_map().clone(), + sparse.commitment(), + ); + + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("overlaps a digest-only entry") + ); +} + +#[test] +fn sparse_serialized_parts_reject_out_of_range_full_digest_and_root_ids() { + let (_source, sparse, true_branch, false_branch, _root) = sparse_split_fixture(); + let out_of_range = MastNodeId::from(sparse.num_nodes() as u32); + + let mut nodes: Vec<_> = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + nodes.push((out_of_range, sparse.get_node_by_id(true_branch).unwrap().clone())); + let digests: Vec<_> = + sparse.digest_entries().iter().map(|(&id, &digest)| (id, digest)).collect(); + let result = SparseMastForest::from_serialized_parts( + nodes, + digests.clone(), + sparse.num_nodes(), + sparse.procedure_roots().to_vec(), + sparse.advice_map().clone(), + sparse.commitment(), + ); + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("full node id") + && msg.contains("out of range") + ); + + let nodes = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + let mut out_of_range_digests = digests; + out_of_range_digests.push((out_of_range, sparse.get_digest_by_id(false_branch).unwrap())); + let result = SparseMastForest::from_serialized_parts( + nodes, + out_of_range_digests, + sparse.num_nodes(), + sparse.procedure_roots().to_vec(), + sparse.advice_map().clone(), + sparse.commitment(), + ); + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("digest-only node id") + && msg.contains("out of range") + ); + + let nodes = sparse.nodes().iter().map(|(&id, node)| (id, node.clone())).collect(); + let digests = sparse.digest_entries().iter().map(|(&id, &digest)| (id, digest)).collect(); + let result = SparseMastForest::from_serialized_parts( + nodes, + digests, + sparse.num_nodes(), + vec![out_of_range], + sparse.advice_map().clone(), + sparse.commitment(), + ); + assert_matches!( + result, + Err(DeserializationError::InvalidValue(msg)) if msg.contains("procedure root id") + && msg.contains("out of range") + ); +} + /// Test that a forest with a node whose child ids are larger than its own id serializes and /// deserializes successfully. #[test] diff --git a/processor/src/trace/execution_tracer.rs b/processor/src/trace/execution_tracer.rs index 9fdd13e818..f6cff2ee01 100644 --- a/processor/src/trace/execution_tracer.rs +++ b/processor/src/trace/execution_tracer.rs @@ -1218,6 +1218,7 @@ impl Default for HasherChipletShim { #[cfg(test)] mod serialization_tests { use super::*; + use crate::mast::{BasicBlockNodeBuilder, MastForestContributor}; fn empty_trace_generation_context( fragment_size: usize, @@ -1237,6 +1238,64 @@ mod serialization_tests { } } + fn one_node_sparse_forest() -> Arc { + let mut forest = MastForest::new(); + let root = BasicBlockNodeBuilder::new(vec![Operation::Noop]) + .add_to_forest(&mut forest) + .unwrap(); + forest.make_root(root); + + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(root, VisitKind::FullVisit); + Arc::new(builder.finalize()) + } + + fn core_trace_state() -> CoreTraceState { + CoreTraceState { + system: SystemState { + clk: RowIndex::from(0u32), + ctx: ContextId::root(), + fn_hash: Word::default(), + pc_transcript_state: Word::default(), + }, + decoder: DecoderState { current_addr: ZERO, parent_addr: ZERO }, + stack: StackState::new([ZERO; MIN_STACK_DEPTH], MIN_STACK_DEPTH, ZERO), + } + } + + fn valid_trace_generation_context() -> TraceGenerationContext { + TraceGenerationContext { + mast_forest_store: vec![one_node_sparse_forest()], + core_trace_contexts: vec![CoreTraceFragmentContext { + state: core_trace_state(), + replay: ExecutionReplay::default(), + continuation: ContinuationStack::default(), + initial_mast_forest_id: MastForestId::from(0u32), + }], + range_checker_replay: RangeCheckerReplay::default(), + memory_writes: MemoryWritesReplay::default(), + bitwise_replay: BitwiseReplay::default(), + hasher_for_chiplet: HasherRequestReplay::default(), + kernel_replay: KernelReplay::default(), + ace_replay: AceReplay::default(), + fragment_size: 1, + max_stack_depth: MIN_STACK_DEPTH, + } + } + + fn assert_context_read_rejects_bad_forest_id( + context: TraceGenerationContext, + expected_label: &str, + ) { + let err = TraceGenerationContext::read_from_bytes(&context.to_bytes()).unwrap_err(); + let DeserializationError::InvalidValue(message) = err else { + panic!("expected invalid forest id error"); + }; + assert!(message.contains(expected_label), "{message}"); + assert!(message.contains("out of range for mast_forest_store length 1"), "{message}"); + } + #[test] fn trace_generation_context_read_rejects_zero_fragment_size() { let context = empty_trace_generation_context(0, MIN_STACK_DEPTH); @@ -1258,4 +1317,54 @@ mod serialization_tests { }; assert!(message.contains("max_stack_depth")); } + + #[test] + fn trace_generation_context_read_rejects_bad_initial_forest_id() { + let mut context = valid_trace_generation_context(); + context.core_trace_contexts[0].initial_mast_forest_id = MastForestId::from(1u32); + + assert_context_read_rejects_bad_forest_id( + context, + "core trace fragment initial_mast_forest_id", + ); + } + + #[test] + fn trace_generation_context_read_rejects_bad_continuation_forest_id() { + let mut context = valid_trace_generation_context(); + context.core_trace_contexts[0] + .continuation + .push_enter_forest(MastForestId::from(1u32)); + + assert_context_read_rejects_bad_forest_id( + context, + "core trace fragment continuation EnterForest", + ); + } + + #[test] + fn trace_generation_context_read_rejects_bad_resolution_replay_forest_id() { + let mut context = valid_trace_generation_context(); + context.core_trace_contexts[0] + .replay + .mast_forest_resolution + .record_resolution(MastNodeId::from(0), MastForestId::from(1u32)); + + assert_context_read_rejects_bad_forest_id( + context, + "core trace fragment MastForestResolutionReplay", + ); + } + + #[test] + fn trace_generation_context_read_rejects_bad_hasher_replay_forest_id() { + let mut context = valid_trace_generation_context(); + context.hasher_for_chiplet.record_hash_basic_block( + MastForestId::from(1u32), + MastNodeId::from(0), + Word::default(), + ); + + assert_context_read_rejects_bad_forest_id(context, "hasher HashBasicBlock replay"); + } } From 9dff27c7a1f4fa58ed20d4becace05b457e523ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 18:55:58 -0400 Subject: [PATCH 07/12] Strengthen trace input round-trip tests --- core/src/mast/serialization/tests.rs | 31 ++++++++++++++++++++++ miden-vm/tests/integration/prove_verify.rs | 11 +++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/core/src/mast/serialization/tests.rs b/core/src/mast/serialization/tests.rs index 097356fe34..fcb1b20630 100644 --- a/core/src/mast/serialization/tests.rs +++ b/core/src/mast/serialization/tests.rs @@ -725,6 +725,37 @@ fn sparse_mast_round_trip_preserves_sparse_replay_ids() { assert_eq!(restored.get_digest_by_id(root), Some(source[root].digest())); } +#[test] +fn sparse_mast_round_trip_preserves_external_full_node() { + let mut forest = MastForest::new(); + let unvisited = BasicBlockNodeBuilder::new(vec![Operation::Add]) + .add_to_forest(&mut forest) + .unwrap(); + let external_digest = Word::new([ + Felt::new_unchecked(17), + Felt::new_unchecked(18), + Felt::new_unchecked(19), + Felt::new_unchecked(20), + ]); + let external = ExternalNodeBuilder::new(external_digest).add_to_forest(&mut forest).unwrap(); + forest.make_root(external); + + let forest = Arc::new(forest); + let mut builder = SparseMastForestBuilder::new(Arc::clone(&forest)); + builder.record_visit(external, VisitKind::FullVisit); + let sparse = builder.finalize(); + + let restored = SparseMastForest::read_from_bytes(&sparse.to_bytes()).unwrap(); + + assert_eq!(restored.num_nodes(), forest.num_nodes() as usize); + assert_eq!(restored.procedure_roots(), &[external]); + assert_eq!(restored.commitment(), forest.commitment()); + assert_eq!(restored.get_node_by_id(external).unwrap().digest(), external_digest); + assert_eq!(restored.get_digest_by_id(external), Some(external_digest)); + assert!(restored.get_node_by_id(unvisited).is_none()); + assert_eq!(restored.get_digest_by_id(unvisited), None); +} + fn write_sparse_test_payload( source_node_count: usize, roots: &[MastNodeId], diff --git a/miden-vm/tests/integration/prove_verify.rs b/miden-vm/tests/integration/prove_verify.rs index cc39512d33..8f18daa457 100644 --- a/miden-vm/tests/integration/prove_verify.rs +++ b/miden-vm/tests/integration/prove_verify.rs @@ -415,13 +415,22 @@ mod fast_parallel { execute_parallel_trace_inputs(&program, stack_inputs, advice_inputs, &mut host); let trace_inputs_bytes = trace_inputs.to_bytes(); + let original_trace = build_trace(trace_inputs).expect("original trace inputs build trace"); let restored_trace_inputs = TraceBuildInputs::read_from_bytes(&trace_inputs_bytes) .expect("trace inputs round trip"); assert!( restored_trace_inputs.trace_generation_context().mast_forest_store.len() > 1, "expected dynamic library execution to serialize multiple MAST forests" ); - let _trace = build_trace(restored_trace_inputs).expect("restored trace inputs build trace"); + let restored_trace = + build_trace(restored_trace_inputs).expect("restored trace inputs build trace"); + assert_eq!(restored_trace.stack_outputs(), original_trace.stack_outputs()); + assert_eq!(restored_trace.program_info(), original_trace.program_info()); + assert_eq!(restored_trace.trace_len_summary(), original_trace.trace_len_summary()); + assert_eq!( + restored_trace.public_inputs().to_air_inputs(), + original_trace.public_inputs().to_air_inputs() + ); let trace_inputs = TraceBuildInputs::read_from_bytes(&trace_inputs_bytes) .expect("trace inputs round trip"); From 893bcd5c1e6dda9e5d1fb5b1f50dbe97d5e38f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 23 Jun 2026 19:01:12 -0400 Subject: [PATCH 08/12] chore: Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40efe5330e..068715ba09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - [BREAKING] Removed the stripped `MastForest` serialization mode. Normal forest bytes now describe execution data only ([#3268](https://github.com/0xMiden/miden-vm/pull/3268)). - [BREAKING] Bump Plonky3 related dependencies to fix NEON arithmetic bug ([#3272](https://github.com/0xMiden/miden-vm/pull/3272)). - [BREAKING] Bump Plonky3 and miden-crypto related dependencies ([#3275](https://github.com/0xMiden/miden-vm/pull/3275)). +- Added trusted binary serialization for `TraceProvingInputs` and sparse MAST replay data so pre-executed trace inputs can be sent to a trusted prover ([#3284](https://github.com/0xMiden/miden-vm/pull/3284)). #### Fixes From b4d632ac425775d76a693e951fa7c30fe4536f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Thu, 25 Jun 2026 10:29:48 -0400 Subject: [PATCH 09/12] Address trace input review feedback --- air/Cargo.toml | 2 +- core/src/mast/serialization/tests.rs | 4 ++-- crates/assembly/Cargo.toml | 2 +- crates/debug-types/Cargo.toml | 1 + crates/mast-package/Cargo.toml | 1 + crates/package-registry/Cargo.toml | 1 + crates/project/Cargo.toml | 1 + crates/utils-indexing/Cargo.toml | 1 + miden-vm/tests/integration/prove_verify.rs | 17 +++++++++-------- processor/Cargo.toml | 2 +- prover/Cargo.toml | 1 + 11 files changed, 20 insertions(+), 13 deletions(-) diff --git a/air/Cargo.toml b/air/Cargo.toml index ce08803eaa..dfca6bae5f 100644 --- a/air/Cargo.toml +++ b/air/Cargo.toml @@ -22,7 +22,7 @@ default = ["std"] arbitrary = ["std", "dep:proptest"] std = ["miden-core/std", "proptest?/std", "thiserror/std"] concurrent = ["std"] -testing = [] +testing = ["arbitrary"] [dependencies] # Miden dependencies diff --git a/core/src/mast/serialization/tests.rs b/core/src/mast/serialization/tests.rs index fcb1b20630..91001f3e5f 100644 --- a/core/src/mast/serialization/tests.rs +++ b/core/src/mast/serialization/tests.rs @@ -966,10 +966,10 @@ fn sparse_reader_rejects_trailing_bytes_with_exact_prefix_budget() { bytes_with_trailing.push(0); let err = SparseMastForest::read_from_bytes_with_options( &bytes_with_trailing, - SparseMastForestReadOptions::new().with_wire_byte_budget(bytes.len()), + SparseMastForestReadOptions::new().with_wire_byte_budget(bytes_with_trailing.len()), ) .unwrap_err(); - assert!(err.to_string().contains("budget is smaller than payload length")); + assert!(err.to_string().contains("extra bytes after SparseMastForest payload")); } #[test] diff --git a/crates/assembly/Cargo.toml b/crates/assembly/Cargo.toml index 9f48ca31ba..7d3523fe8a 100644 --- a/crates/assembly/Cargo.toml +++ b/crates/assembly/Cargo.toml @@ -30,7 +30,7 @@ std = [ "tempfile/getrandom", "thiserror/std", ] -testing = ["logging", "miden-assembly-syntax/testing"] +testing = ["arbitrary", "logging", "miden-assembly-syntax/testing"] logging = ["dep:env_logger"] [dependencies] diff --git a/crates/debug-types/Cargo.toml b/crates/debug-types/Cargo.toml index bc48388d71..86b58a1c61 100644 --- a/crates/debug-types/Cargo.toml +++ b/crates/debug-types/Cargo.toml @@ -30,6 +30,7 @@ std = [ ] arbitrary = ["std", "dep:proptest"] serde = ["dep:serde", "dep:serde_spanned", "serde_spanned?/serde"] +testing = ["arbitrary"] [dependencies] # Miden dependencies diff --git a/crates/mast-package/Cargo.toml b/crates/mast-package/Cargo.toml index bac1e30242..9de820cbbb 100644 --- a/crates/mast-package/Cargo.toml +++ b/crates/mast-package/Cargo.toml @@ -22,6 +22,7 @@ default = ["std"] arbitrary = ["std", "dep:proptest", "dep:proptest-derive", "miden-assembly-syntax/arbitrary", "miden-core/arbitrary"] std = ["miden-assembly-syntax/std", "miden-core/std", "serde?/std", "thiserror/std"] serde = ["dep:serde", "miden-assembly-syntax/serde", "miden-core/serde"] +testing = ["arbitrary"] [dependencies] # Miden dependencies diff --git a/crates/package-registry/Cargo.toml b/crates/package-registry/Cargo.toml index ce71be88d7..c9126f2d75 100644 --- a/crates/package-registry/Cargo.toml +++ b/crates/package-registry/Cargo.toml @@ -22,6 +22,7 @@ arbitrary = ["std", "dep:proptest", "miden-mast-package/arbitrary"] resolver = ["std", "dep:pubgrub", "dep:smallvec"] std = ["miden-assembly-syntax/std", "miden-core/std", "proptest?/std", "serde?/std"] serde = ["dep:serde", "miden-assembly-syntax/serde", "miden-core/serde", "miden-mast-package/serde"] +testing = ["arbitrary"] [dependencies] miden-assembly-syntax.workspace = true diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 636a795c1b..122d5efa2b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -19,6 +19,7 @@ arbitrary = ["std", "dep:proptest", "dep:proptest-derive", "miden-assembly-synta resolver = ["std", "miden-package-registry/resolver"] std = ["miden-assembly-syntax/std", "miden-package-registry/std", "miden-package-registry/resolver", "proptest?/std", "serde?/std", "tempfile/getrandom", "thiserror/std", "toml/std"] serde = ["dep:serde", "dep:serde-untagged", "miden-assembly-syntax/serde", "miden-core/serde", "miden-package-registry/serde", "toml/serde", "toml/parse", "toml/display"] +testing = ["arbitrary"] [dependencies] miden-assembly-syntax.workspace = true diff --git a/crates/utils-indexing/Cargo.toml b/crates/utils-indexing/Cargo.toml index 4b76274f80..934cbde4a8 100644 --- a/crates/utils-indexing/Cargo.toml +++ b/crates/utils-indexing/Cargo.toml @@ -20,6 +20,7 @@ arbitrary = ["std", "dep:proptest"] default = ["std"] std = ["proptest?/std"] serde = ["dep:serde"] +testing = ["arbitrary"] [dependencies] # Required dependencies diff --git a/miden-vm/tests/integration/prove_verify.rs b/miden-vm/tests/integration/prove_verify.rs index 8f18daa457..a37384aa1d 100644 --- a/miden-vm/tests/integration/prove_verify.rs +++ b/miden-vm/tests/integration/prove_verify.rs @@ -443,14 +443,15 @@ mod fast_parallel { proving_inputs_bytes.len().checked_mul(4).expect("test input budget overflow"); let mut proving_inputs_with_trailing_byte = proving_inputs_bytes.clone(); proving_inputs_with_trailing_byte.push(0); - assert!( - TraceProvingInputs::read_from_bytes_with_budget( - &proving_inputs_with_trailing_byte, - proving_inputs_bytes.len(), - ) - .is_err(), - "TraceProvingInputs should reject trailing bytes even when the budget matches the valid prefix" - ); + let err = TraceProvingInputs::read_from_bytes_with_budget( + &proving_inputs_with_trailing_byte, + proving_inputs_with_trailing_byte + .len() + .checked_mul(4) + .expect("test input budget overflow"), + ) + .unwrap_err(); + assert!(err.to_string().contains("TraceProvingInputs payload has trailing bytes")); let restored_proving_inputs = TraceProvingInputs::read_from_bytes_with_budget( &proving_inputs_bytes, proving_inputs_budget, diff --git a/processor/Cargo.toml b/processor/Cargo.toml index 5b16c4311f..cb35b2bb84 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -28,7 +28,7 @@ std = [ "miden-utils-diagnostics/std", "thiserror/std", ] -testing = ["miden-air/testing"] +testing = ["arbitrary", "miden-air/testing"] # Pulls in the LogUp debug surface from miden-air (under `#[cfg(feature = "std")]`). # NOTE: the real-trace bus debugger is not yet wired into the prover/processor paths, # so today this feature only compiles in the LookupAir shape-validation walker. diff --git a/prover/Cargo.toml b/prover/Cargo.toml index 1ef17b8541..53f4021b91 100644 --- a/prover/Cargo.toml +++ b/prover/Cargo.toml @@ -18,6 +18,7 @@ arbitrary = ["dep:proptest"] default = ["std"] concurrent = ["std", "miden-air/concurrent", "miden-crypto/concurrent", "miden-processor/concurrent"] std = ["miden-air/std", "miden-debug-types/std", "miden-processor/std"] +testing = ["arbitrary"] [dependencies] # Miden dependencies From e9669b9e618d46d4b5ca222e81ae477cb81923e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Thu, 25 Jun 2026 10:35:45 -0400 Subject: [PATCH 10/12] chore: Refresh miden-core-fuzz lockfile --- miden-core-fuzz/Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/miden-core-fuzz/Cargo.lock b/miden-core-fuzz/Cargo.lock index 028444ef02..66b4567bac 100644 --- a/miden-core-fuzz/Cargo.lock +++ b/miden-core-fuzz/Cargo.lock @@ -733,7 +733,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miden-ace-codegen" -version = "0.24.0" +version = "0.25.0" dependencies = [ "miden-core", "miden-crypto", @@ -742,7 +742,7 @@ dependencies = [ [[package]] name = "miden-air" -version = "0.24.0" +version = "0.25.0" dependencies = [ "miden-ace-codegen", "miden-core", @@ -1026,7 +1026,7 @@ dependencies = [ [[package]] name = "miden-processor" -version = "0.24.0" +version = "0.25.0" dependencies = [ "itertools", "miden-air", @@ -1058,7 +1058,7 @@ dependencies = [ [[package]] name = "miden-prover" -version = "0.24.0" +version = "0.25.0" dependencies = [ "miden-air", "miden-core", From 26ea7f73f044d94eb45eaf6339ed7562e03fa77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Thu, 25 Jun 2026 10:44:58 -0400 Subject: [PATCH 11/12] fix: Remove redundant sparse test clone --- core/src/mast/serialization/tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/mast/serialization/tests.rs b/core/src/mast/serialization/tests.rs index 91001f3e5f..3b0d818bff 100644 --- a/core/src/mast/serialization/tests.rs +++ b/core/src/mast/serialization/tests.rs @@ -952,7 +952,7 @@ fn sparse_reader_rejects_trailing_bytes_with_exact_prefix_budget() { let basic_block_data = basic_block_data.finalize(); let root = MastNodeId::from(0); - let bytes = write_sparse_test_payload( + let mut bytes_with_trailing = write_sparse_test_payload( 1, &[root], &[root], @@ -962,7 +962,6 @@ fn sparse_reader_rejects_trailing_bytes_with_exact_prefix_budget() { block.digest(), ); - let mut bytes_with_trailing = bytes.clone(); bytes_with_trailing.push(0); let err = SparseMastForest::read_from_bytes_with_options( &bytes_with_trailing, From 0c4c6f49f35505758850eb70419c485cee540d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Mon, 29 Jun 2026 10:41:12 -0400 Subject: [PATCH 12/12] docs: Clarify trusted trace input readers --- Cargo.lock | 2 +- core/src/mast/serialization/sparse.rs | 19 +++++++++++++++++-- processor/src/trace/execution_tracer.rs | 3 +++ processor/src/trace/mod.rs | 4 ++++ prover/src/lib.rs | 14 +++++++++----- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3f5ad4f27..bab112ac9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1899,7 +1899,7 @@ dependencies = [ [[package]] name = "miden-vm-blake3-bench" -version = "0.24.0" +version = "0.25.0" dependencies = [ "clap", "codspeed-criterion-compat", diff --git a/core/src/mast/serialization/sparse.rs b/core/src/mast/serialization/sparse.rs index e24fabae9e..63d032061a 100644 --- a/core/src/mast/serialization/sparse.rs +++ b/core/src/mast/serialization/sparse.rs @@ -30,6 +30,8 @@ fn sparse_mast_forest_min_serialized_size() -> usize { /// /// This format carries the digest for each full node and accepts those digests on read. It is /// suitable for trusted remote proving inputs, not as an untrusted hashless validation path. +/// +/// See for the planned untrusted reader. pub(super) fn write_sparse_into(forest: &SparseMastForest, target: &mut W) { let mut basic_block_data_builder = BasicBlockDataBuilder::new(); let mut full_ids = Vec::with_capacity(forest.nodes().len()); @@ -100,6 +102,10 @@ impl Serializable for SparseMastForest { } impl Deserializable for SparseMastForest { + /// Reads a trusted sparse replay payload. + /// + /// Full-node digests are accepted from the payload. This is not the untrusted hash-validation + /// path from . fn read_from(source: &mut R) -> Result { read_sparse_from(source) } @@ -108,11 +114,16 @@ impl Deserializable for SparseMastForest { sparse_mast_forest_min_serialized_size() } + /// Reads trusted sparse replay bytes and rejects trailing bytes. fn read_from_bytes(bytes: &[u8]) -> Result { SparseMastForest::read_from_bytes(bytes) } } +/// Reads a trusted sparse replay payload. +/// +/// The payload carries full-node digests and digest-only entries as replay data. It does not +/// rebuild those hashes from node structure. pub(super) fn read_sparse_from( source: &mut R, ) -> Result { @@ -335,12 +346,16 @@ fn materialize_sparse_nodes( } impl SparseMastForest { - /// Deserializes sparse MAST bytes using default untrusted budgets. + /// Deserializes trusted sparse MAST replay bytes using default parse budgets. + /// + /// This reader bounds parsing, but accepts sparse MAST hashes from the payload. pub fn read_from_bytes(bytes: &[u8]) -> Result { Self::read_from_bytes_with_options(bytes, SparseMastForestReadOptions::default()) } - /// Deserializes sparse MAST bytes using explicit read options. + /// Deserializes trusted sparse MAST replay bytes using explicit read options. + /// + /// See for the planned untrusted reader. pub fn read_from_bytes_with_options( bytes: &[u8], options: SparseMastForestReadOptions, diff --git a/processor/src/trace/execution_tracer.rs b/processor/src/trace/execution_tracer.rs index f6cff2ee01..3b617a2fa0 100644 --- a/processor/src/trace/execution_tracer.rs +++ b/processor/src/trace/execution_tracer.rs @@ -63,6 +63,9 @@ pub struct TraceGenerationContext { /// original [`MastNodeId`]s of the source forest. References from `CoreTraceFragmentContext`, /// `MastForestResolutionReplay`, and `HasherOp::HashBasicBlock` are encoded as /// [`MastForestId`]s into this vector. + /// + /// Serialized entries are trusted sparse replay data. Their sparse MAST hashes are not + /// recomputed on read; see . pub mast_forest_store: Vec>, // Replays that contain additional data needed to generate the range checker and chiplets diff --git a/processor/src/trace/mod.rs b/processor/src/trace/mod.rs index d897e0752e..bcc00aa5e7 100644 --- a/processor/src/trace/mod.rs +++ b/processor/src/trace/mod.rs @@ -43,6 +43,10 @@ pub use parallel::{CORE_TRACE_WIDTH, build_trace, build_trace_with_max_len}; pub use utils::{ChipletsLengths, TraceLenSummary}; /// Inputs required to build an execution trace from pre-executed data. +/// +/// Its binary form is trusted replay data. Sparse MAST hashes inside the trace generation context +/// are not validated against untrusted senders; see +/// . #[derive(Debug)] pub struct TraceBuildInputs { trace_output: TraceBuildOutput, diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 7696507226..2fbe6019a4 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -45,10 +45,11 @@ pub use proving_options::ProvingOptions; /// Inputs required to prove from pre-executed trace data. /// -/// Its binary form is a VM-owned remote proving input containing trace replay data and -/// proof-generation options. Deserialization checks malformed structure and bounded allocation; -/// semantic replay inconsistencies may still surface later while building the trace. Correct -/// execution is established by the proof generated from these inputs. +/// Its binary form is a VM-owned trusted remote proving input containing trace replay data and +/// proof-generation options. Deserialization checks malformed structure and bounded allocation, but +/// sparse MAST hashes are accepted as replay data. +/// +/// See for the planned untrusted reader. #[derive(Debug)] pub struct TraceProvingInputs { trace_inputs: TraceBuildInputs, @@ -66,7 +67,10 @@ impl TraceProvingInputs { (self.trace_inputs, self.options) } - /// Deserializes remote proving inputs using the supplied untrusted byte budget. + /// Deserializes trusted remote proving inputs using the supplied byte budget. + /// + /// The budget bounds parsing. It does not validate sparse MAST hashes from untrusted senders. + /// See . pub fn read_from_bytes_with_budget( bytes: &[u8], budget: usize,