From 0f02d64001f982c8a00e33d268c38c3a916f33e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 30 Jun 2026 19:07:47 -0400 Subject: [PATCH 01/10] Serialize trace proving inputs --- core/src/lib.rs | 49 ++ core/src/mast/sparse.rs | 45 ++ processor/src/continuation_stack.rs | 252 ++++++- processor/src/lib.rs | 34 + processor/src/trace/chiplets/ace/trace.rs | 417 +++++++++++- processor/src/trace/execution_tracer.rs | 293 ++++++++- processor/src/trace/mod.rs | 55 +- processor/src/trace/trace_state.rs | 761 +++++++++++++++++++++- prover/src/lib.rs | 67 +- prover/src/proving_options.rs | 41 +- 10 files changed, 1979 insertions(+), 35 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index ee6723d6b7..3fbc77ceb3 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/sparse.rs b/core/src/mast/sparse.rs index c2897bfd34..c9327ce153 100644 --- a/core/src/mast/sparse.rs +++ b/core/src/mast/sparse.rs @@ -24,6 +24,35 @@ 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() + } +} + +#[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 // ================================================================================================ @@ -267,6 +296,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/processor/src/continuation_stack.rs b/processor/src/continuation_stack.rs index d91bf6d007..531bf6de9f 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. @@ -18,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), @@ -114,7 +126,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>>, @@ -357,6 +377,187 @@ 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 }) + } +} + +#[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 // ================================================================================================ @@ -425,4 +626,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 6ea781e913..1a9a29b126 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -29,6 +29,7 @@ mod tracer; use miden_core::{ deferred::{Digest, Node, PrecompileError}, mast::ExecutableMastForest, + serde::{Deserializable, Serializable}, }; use crate::{ @@ -345,6 +346,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); @@ -390,6 +395,35 @@ impl From for Felt { } } +impl Serializable for ContextId { + fn write_into(&self, target: &mut W) { + Serializable::write_into(&self.0, target); + } +} + +impl Deserializable for ContextId { + fn read_from( + source: &mut R, + ) -> Result { + Ok(Self(::read_from(source)?)) + } + + fn min_serialized_size() -> usize { + ::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() + } +} + 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..c824ee5df6 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::{ @@ -18,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, @@ -30,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, @@ -46,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, @@ -243,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, @@ -289,3 +306,395 @@ 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(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; + + 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 1dd741f6bb..9b15037530 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}, @@ -60,6 +64,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 @@ -80,6 +87,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 @@ -1101,3 +1238,157 @@ impl Default for HasherChipletShim { Self::new() } } + +#[cfg(test)] +mod serialization_tests { + use super::*; + use crate::mast::{BasicBlockNodeBuilder, MastForestContributor}; + + 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, + } + } + + 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(), + deferred_root: 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); + + 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")); + } + + #[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"); + } +} diff --git a/processor/src/trace/mod.rs b/processor/src/trace/mod.rs index 504cdd3d68..18c896f246 100644 --- a/processor/src/trace/mod.rs +++ b/processor/src/trace/mod.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{format, sync::Arc, vec::Vec}; #[cfg(any(test, feature = "testing"))] use core::ops::Range; @@ -6,7 +6,10 @@ use miden_air::{ MidenMultiAir, ProverStatement, PublicInputs, StarkConfig, Statement, config, debug, trace::{MainTrace, decoder::NUM_USER_OP_HELPERS}, }; -use miden_core::deferred::DeferredState; +use miden_core::{ + deferred::{DEFAULT_MAX_DEFERRED_ELEMENTS, DeferredState, DeferredStateWire}, + serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, +}; use crate::{ Felt, MIN_STACK_DEPTH, Program, ProgramInfo, StackInputs, StackOutputs, Word, ZERO, @@ -44,6 +47,10 @@ pub(crate) use trace_state::ResolvedHasherOp; 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, @@ -81,6 +88,32 @@ impl TraceBuildOutput { } } +impl Serializable for TraceBuildOutput { + fn write_into(&self, target: &mut W) { + self.stack_outputs.write_into(target); + let deferred_wire = self + .deferred_state + .to_wire() + .expect("deferred state must serialize to canonical wire"); + deferred_wire.write_into(target); + } +} + +impl Deserializable for TraceBuildOutput { + fn read_from(source: &mut R) -> Result { + let stack_outputs = StackOutputs::read_from(source)?; + let deferred_wire = DeferredStateWire::read_from(source)?; + let deferred_state = DeferredState::from_wire( + Arc::new(miden_precompiles::registry()), + &deferred_wire, + DEFAULT_MAX_DEFERRED_ELEMENTS, + ) + .map_err(|err| DeserializationError::InvalidValue(format!("invalid deferred state: {err}")))?; + + Ok(Self { stack_outputs, deferred_state }) + } +} + impl TraceBuildInputs { pub(crate) fn from_execution( program: &Program, @@ -125,6 +158,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 c0b726f707..506bfaecaa 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -4,7 +4,13 @@ use miden_air::trace::{ RowIndex, chiplets::hasher::{HasherState, RATE_LEN, STATE_WIDTH}, }; -use miden_core::mast::{BasicBlockNode, ExecutableMastForest, MastNode, MastNodeExt, OpBatch}; +use miden_core::{ + mast::{BasicBlockNode, ExecutableMastForest, MastNode, MastNodeExt, OpBatch}, + serde::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, + SerializableVecDeque, read_vec_deque, + }, +}; use crate::{ ContextId, ExecutionError, Felt, MIN_STACK_DEPTH, MemoryError, ONE, Word, ZERO, @@ -40,7 +46,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, @@ -56,7 +66,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, @@ -70,6 +84,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) @@ -106,7 +124,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, @@ -155,7 +177,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. @@ -281,7 +307,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, @@ -296,7 +326,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). @@ -323,7 +357,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, @@ -428,7 +466,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, @@ -442,7 +484,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, @@ -461,7 +507,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)>, } @@ -480,6 +530,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 @@ -496,7 +550,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)>, @@ -566,7 +624,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)>, @@ -658,7 +720,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, @@ -749,14 +815,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)>, } @@ -795,7 +869,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, } @@ -824,7 +902,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)>, } @@ -864,7 +946,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]>, } @@ -892,7 +978,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, @@ -922,7 +1012,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). /// @@ -1029,7 +1123,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)), @@ -1086,6 +1184,10 @@ pub enum ResolvedHasherOp<'a> { /// ([`HasherRequestReplay::streamed`]) each request is resolved at record time and forwarded to /// a hasher-chiplet builder running concurrently with execution; see /// `FastProcessor::execute_and_build_trace_sync`. +#[cfg_attr( + all(feature = "arbitrary", test), + miden_test_serde_macros::serde_test(binary_serde(true), serde_test(false)) +)] #[derive(Debug, Default)] pub struct HasherRequestReplay { sink: HasherOpSink, @@ -1212,6 +1314,23 @@ impl HasherRequestReplay { self.record(HasherOp::UpdateMerkleRoot((old_value, new_value, path, index))); } + pub(crate) fn iter_hash_basic_block_forest_ids( + &self, + ) -> impl Iterator + '_ { + self.buffered_ops().into_iter().flat_map(|ops| ops.iter()).filter_map(|op| match op { + HasherOp::HashBasicBlock((forest_id, _node_id, _expected_hash)) => Some(*forest_id), + _ => None, + }) + } + + fn buffered_ops(&self) -> Option<&VecDeque> { + match &self.sink { + HasherOpSink::Buffered(ops) => Some(ops), + #[cfg(feature = "std")] + HasherOpSink::Streamed(_) => None, + } + } + /// Drains the buffered requests as resolved ops, looking basic blocks up in the finalized /// forest store. /// @@ -1272,7 +1391,11 @@ impl 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 @@ -1359,6 +1482,598 @@ impl StackOverflowReplay { } } +// 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.deferred_root.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)?, + deferred_root: 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)?, + }) + } + + fn min_serialized_size() -> usize { + ContextId::min_serialized_size() + Word::min_serialized_size() + } +} + +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}" + ))), + } + } + + 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 { + fn write_into(&self, target: &mut W) { + match &self.sink { + HasherOpSink::Buffered(ops) => SerializableVecDeque(ops).write_into(target), + #[cfg(feature = "std")] + HasherOpSink::Streamed(_) => { + panic!("cannot serialize streamed hasher request replay") + }, + } + } +} + +impl Deserializable for HasherRequestReplay { + fn read_from(source: &mut R) -> Result { + Ok(Self { sink: HasherOpSink::Buffered(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(all(test, feature = "std"))] mod tests { use super::*; diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 7c89786f4a..eb16305d99 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,12 @@ 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 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, @@ -52,6 +66,57 @@ impl TraceProvingInputs { pub fn into_parts(self) -> (TraceBuildInputs, ProvingOptions) { (self.trace_inputs, self.options) } + + /// 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, + ) -> 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..309d6b289e 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 // ================================================================================================ @@ -8,6 +11,10 @@ use miden_core::proof::HashFunction; /// 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, @@ -44,3 +51,35 @@ 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)?)) + } +} + +#[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 898f192c34eb043d203e1ebe5323e88335ba63bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 30 Jun 2026 19:23:35 -0400 Subject: [PATCH 02/10] chore: Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925d7f32fc..eec9b65ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,6 +144,7 @@ - Reduced optimized benchmark build time by relaxing forced inlining in processor execution helpers ([#3292](https://github.com/0xMiden/miden-vm/pull/3292)). - Added no-op handlers for readonly debugger events to `CoreLibrary::handlers`, so hosts that load the core library can execute programs emitting those events without registering no-op handlers manually ([#3305](https://github.com/0xMiden/miden-vm/pull/3305)). - Added trusted sparse MAST forest serialization for trace replay payloads ([#3313](https://github.com/0xMiden/miden-vm/pull/3313)). +- Added trusted trace proving input serialization for remote proving ([#3314](https://github.com/0xMiden/miden-vm/pull/3314)). ## miden-vm v0.24.0 (2026-06-24) From bcb144647f6f5e517add927ed93e33efe9b6e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 30 Jun 2026 19:46:18 -0400 Subject: [PATCH 03/10] fix: Make trace input serialization features standalone --- Cargo.lock | 3 + core/Cargo.toml | 8 +- processor/Cargo.toml | 7 +- processor/src/trace/trace_state.rs | 17 + processor/src/trace/trace_state/arbitrary.rs | 387 +++++++++++++++++++ prover/Cargo.toml | 4 + 6 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 processor/src/trace/trace_state/arbitrary.rs diff --git a/Cargo.lock b/Cargo.lock index 32d7a5ac07..0635f6221b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2553,6 +2553,7 @@ dependencies = [ "miden-debug-types", "miden-mast-package", "miden-precompiles", + "miden-test-serde-macros", "miden-test-utils", "miden-utils-diagnostics", "miden-utils-indexing", @@ -2597,6 +2598,8 @@ dependencies = [ "miden-debug-types", "miden-precompiles-prover", "miden-processor", + "miden-test-serde-macros", + "proptest", "serde", "serde-wincode", "tokio", diff --git a/core/Cargo.toml b/core/Cargo.toml index 8db1161d9c..d8b782d701 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/Cargo.toml b/processor/Cargo.toml index 118bf2da9e..b280209f27 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 = [ @@ -27,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. @@ -47,12 +48,14 @@ miden-utils-indexing.workspace = true hashbrown.workspace = true itertools.workspace = true paste.workspace = true -rayon.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 tracing = { workspace = true, features = ["std"] } tracing-subscriber.workspace = true miden-utils-testing.workspace = true diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 506bfaecaa..39a1995b95 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -1200,6 +1200,20 @@ enum HasherOpSink { Streamed(std::sync::mpsc::Sender>), } +impl PartialEq for HasherRequestReplay { + fn eq(&self, other: &Self) -> bool { + match (&self.sink, &other.sink) { + (HasherOpSink::Buffered(lhs), HasherOpSink::Buffered(rhs)) => lhs == rhs, + #[cfg(feature = "std")] + (HasherOpSink::Streamed(_), HasherOpSink::Streamed(_)) => true, + #[cfg(feature = "std")] + _ => false, + } + } +} + +impl Eq for HasherRequestReplay {} + impl Default for HasherOpSink { fn default() -> Self { Self::Buffered(VecDeque::new()) @@ -2089,3 +2103,6 @@ mod tests { assert!(ops.next().is_none()); } } + +#[cfg(feature = "arbitrary")] +mod arbitrary; diff --git a/processor/src/trace/trace_state/arbitrary.rs b/processor/src/trace/trace_state/arbitrary.rs new file mode 100644 index 0000000000..26d2f03faf --- /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, deferred_root)| Self { + clk, + ctx, + fn_hash, + deferred_root, + }) + .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(|ops| Self { sink: HasherOpSink::Buffered(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/prover/Cargo.toml b/prover/Cargo.toml index fa70089b49..60c4455fc5 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", @@ -28,6 +29,7 @@ std = [ "miden-precompiles-prover/std", "miden-processor/std", ] +testing = ["arbitrary"] [dependencies] # Miden dependencies @@ -38,6 +40,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 @@ -45,4 +48,5 @@ tracing.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"] } From f7024ca0f20ad19feac5a0c2a8dbceb11dbab923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 30 Jun 2026 21:19:58 -0400 Subject: [PATCH 04/10] fix: Address trace input serialization review comments --- core/src/lib.rs | 49 ---------- core/src/utils/mod.rs | 60 +++++++++++- processor/src/continuation_stack.rs | 59 +++++++----- processor/src/trace/chiplets/ace/trace.rs | 3 + processor/src/trace/execution_tracer.rs | 106 +++++++++++----------- processor/src/trace/mod.rs | 24 ++++- processor/src/trace/trace_state.rs | 27 +++++- prover/src/lib.rs | 17 +++- 8 files changed, 209 insertions(+), 136 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 3fbc77ceb3..ee6723d6b7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -31,8 +31,6 @@ pub mod field { } pub mod serde { - use alloc::collections::VecDeque; - pub use miden_crypto::utils::{ BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader, @@ -75,53 +73,6 @@ 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/utils/mod.rs b/core/src/utils/mod.rs index c90cf2c2ee..6ca1508de8 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -1,8 +1,19 @@ -use alloc::vec::Vec; +use alloc::{collections::VecDeque, vec::Vec}; use core::ops::{Bound, Range}; +use crate::{ + Felt, Word, + crypto::hash::Blake3_256, + field::PrimeCharacteristicRing, + serde::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, + read_bounded_len, + }, +}; + // RE-EXPORTS // ================================================================================================ + #[cfg(feature = "std")] pub use miden_crypto::utils::ReadAdapter; pub use miden_crypto::{ @@ -18,8 +29,6 @@ pub use miden_utils_indexing::{ newtype_id, }; -use crate::{Felt, Word, crypto::hash::Blake3_256, field::PrimeCharacteristicRing}; - // TO ELEMENTS // ================================================================================================ @@ -101,6 +110,35 @@ where } } +// VECDEQUE SERIALIZATION +// ================================================================================================ + +/// 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) +} + // BYTE CONVERSIONS // ================================================================================================ @@ -176,9 +214,12 @@ pub fn packed_u32_elements_to_bytes(elements: &[Felt]) -> Vec { #[cfg(test)] mod tests { + use alloc::vec::Vec; + use proptest::prelude::*; use super::*; + use crate::serde::{Deserializable, Serializable, SliceReader}; proptest! { #[test] @@ -207,4 +248,17 @@ mod tests { // https://github.com/0xMiden/miden-vm/issues/433 debug_assert!(false); } + + #[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 SliceReader::new(&bytes)).unwrap(); + assert_eq!(values, restored); + + let vec = Vec::::read_from_bytes(&bytes).unwrap(); + assert_eq!(vec, [1, 2, 3]); + } } diff --git a/processor/src/continuation_stack.rs b/processor/src/continuation_stack.rs index 531bf6de9f..657d82271e 100644 --- a/processor/src/continuation_stack.rs +++ b/processor/src/continuation_stack.rs @@ -10,6 +10,17 @@ use miden_mast_package::debug_info::{DebugSourceNodeId, PackageDebugInfo}; /// A hint for the initial size of the continuation stack. const CONTINUATION_STACK_SIZE_HINT: usize = 64; +const TAG_START_NODE: u8 = 0; +const TAG_FINISH_JOIN: u8 = 1; +const TAG_FINISH_SPLIT: u8 = 2; +const TAG_FINISH_LOOP: u8 = 3; +const TAG_FINISH_CALL: u8 = 4; +const TAG_FINISH_DYN: u8 = 5; +const TAG_RESUME_BASIC_BLOCK: u8 = 6; +const TAG_RESPAN: u8 = 7; +const TAG_FINISH_BASIC_BLOCK: u8 = 8; +const TAG_ENTER_FOREST: u8 = 9; + // CONTINUATION // ================================================================================================ @@ -393,46 +404,46 @@ impl Serializable for Continuation { fn write_into(&self, target: &mut W) { match self { Self::StartNode(node_id) => { - 0u8.write_into(target); + TAG_START_NODE.write_into(target); node_id.write_into(target); }, Self::FinishJoin(node_id) => { - 1u8.write_into(target); + TAG_FINISH_JOIN.write_into(target); node_id.write_into(target); }, Self::FinishSplit(node_id) => { - 2u8.write_into(target); + TAG_FINISH_SPLIT.write_into(target); node_id.write_into(target); }, Self::FinishLoop(node_id) => { - 3u8.write_into(target); + TAG_FINISH_LOOP.write_into(target); node_id.write_into(target); }, Self::FinishCall(node_id) => { - 4u8.write_into(target); + TAG_FINISH_CALL.write_into(target); node_id.write_into(target); }, Self::FinishDyn(node_id) => { - 5u8.write_into(target); + TAG_FINISH_DYN.write_into(target); node_id.write_into(target); }, Self::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => { - 6u8.write_into(target); + TAG_RESUME_BASIC_BLOCK.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); + TAG_RESPAN.write_into(target); node_id.write_into(target); batch_index.write_into(target); }, Self::FinishBasicBlock(node_id) => { - 8u8.write_into(target); + TAG_FINISH_BASIC_BLOCK.write_into(target); node_id.write_into(target); }, Self::EnterForest { forest, package_debug_info: _ } => { - 9u8.write_into(target); + TAG_ENTER_FOREST.write_into(target); forest.write_into(target); }, } @@ -442,23 +453,23 @@ impl Serializable for Continuation { 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)?, + TAG_START_NODE => Ok(Self::StartNode(read_mast_node_id(source)?)), + TAG_FINISH_JOIN => Ok(Self::FinishJoin(read_mast_node_id(source)?)), + TAG_FINISH_SPLIT => Ok(Self::FinishSplit(read_mast_node_id(source)?)), + TAG_FINISH_LOOP => Ok(Self::FinishLoop(read_mast_node_id(source)?)), + TAG_FINISH_CALL => Ok(Self::FinishCall(read_mast_node_id(source)?)), + TAG_FINISH_DYN => Ok(Self::FinishDyn(read_mast_node_id(source)?)), + TAG_RESUME_BASIC_BLOCK => Ok(Self::ResumeBasicBlock { + node_id: read_mast_node_id(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)?, + TAG_RESPAN => Ok(Self::Respan { + node_id: read_mast_node_id(source)?, batch_index: usize::read_from(source)?, }), - 8 => Ok(Self::FinishBasicBlock(MastNodeId::read_from(source)?)), - 9 => Ok(Self::EnterForest { + TAG_FINISH_BASIC_BLOCK => Ok(Self::FinishBasicBlock(read_mast_node_id(source)?)), + TAG_ENTER_FOREST => Ok(Self::EnterForest { forest: MastForestId::read_from(source)?, package_debug_info: None, }), @@ -469,6 +480,10 @@ impl Deserializable for Continuation { } } +fn read_mast_node_id(source: &mut R) -> Result { + Ok(MastNodeId::from(u32::read_from(source)?)) +} + impl Serializable for ContinuationStack { fn write_into(&self, target: &mut W) { self.stack.write_into(target); diff --git a/processor/src/trace/chiplets/ace/trace.rs b/processor/src/trace/chiplets/ace/trace.rs index c824ee5df6..9294ed8e5e 100644 --- a/processor/src/trace/chiplets/ace/trace.rs +++ b/processor/src/trace/chiplets/ace/trace.rs @@ -511,6 +511,9 @@ impl CircuitEvaluation { } } +// ARBITRARY TEST SUPPORT +// ================================================================================================ + #[cfg(feature = "arbitrary")] mod arbitrary { use proptest::{collection, prelude::*}; diff --git a/processor/src/trace/execution_tracer.rs b/processor/src/trace/execution_tracer.rs index 9b15037530..9fe04b5bf3 100644 --- a/processor/src/trace/execution_tracer.rs +++ b/processor/src/trace/execution_tracer.rs @@ -132,75 +132,73 @@ impl Deserializable for TraceGenerationContext { 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)?; + context.validate_invariants()?; + context.validate_forest_ids()?; 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 { +impl TraceGenerationContext { + fn validate_invariants(&self) -> Result<(), DeserializationError> { + if self.fragment_size == 0 { + return Err(DeserializationError::InvalidValue( + "trace generation fragment_size must be non-zero".into(), + )); + } + if self.max_stack_depth < MIN_STACK_DEPTH { return Err(DeserializationError::InvalidValue(format!( - "fragment {fragment_index}: stack depth {stack_depth} exceeds max_stack_depth {}", - context.max_stack_depth + "trace generation max_stack_depth {} is below minimum {MIN_STACK_DEPTH}", + self.max_stack_depth ))); } + for (fragment_index, fragment) in self.core_trace_contexts.iter().enumerate() { + let stack_depth = fragment.state.stack.stack_depth(); + if stack_depth > self.max_stack_depth { + return Err(DeserializationError::InvalidValue(format!( + "fragment {fragment_index}: stack depth {stack_depth} exceeds max_stack_depth {}", + self.max_stack_depth + ))); + } + } + Ok(()) } - 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() { + fn validate_forest_ids(&self) -> Result<(), DeserializationError> { + let store_len = self.mast_forest_store.len(); + for (fragment_index, fragment) in self.core_trace_contexts.iter().enumerate() { validate_mast_forest_id( - forest_id, + fragment.initial_mast_forest_id, store_len, - "core trace fragment continuation EnterForest", - ) - .map_err(|err| { - DeserializationError::InvalidValue(format!("fragment {fragment_index}: {err}")) - })?; + "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 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 self.hasher_for_chiplet.iter_hash_basic_block_forest_ids() { + validate_mast_forest_id(forest_id, store_len, "hasher HashBasicBlock replay")?; } - } - 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(()) } - - Ok(()) } fn validate_mast_forest_id( diff --git a/processor/src/trace/mod.rs b/processor/src/trace/mod.rs index 18c896f246..22c686aa80 100644 --- a/processor/src/trace/mod.rs +++ b/processor/src/trace/mod.rs @@ -70,7 +70,7 @@ impl TraceBuildInputs { } #[derive(Debug)] -pub(crate) struct TraceBuildOutput { +struct TraceBuildOutput { stack_outputs: StackOutputs, deferred_state: DeferredState, } @@ -144,6 +144,13 @@ impl TraceBuildInputs { &self.program_info } + // Kept for mismatch and edge-case tests that mutate replay inputs directly. + #[cfg(any(test, feature = "testing"))] + #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))] + fn into_parts(self) -> (TraceBuildOutput, TraceGenerationContext, ProgramInfo) { + (self.trace_output, self.trace_generation_context, self.program_info) + } + #[cfg(any(test, feature = "testing"))] /// Returns the trace replay context captured during execution. pub fn trace_generation_context(&self) -> &TraceGenerationContext { @@ -156,6 +163,19 @@ impl TraceBuildInputs { pub(crate) fn trace_generation_context_mut(&mut self) -> &mut TraceGenerationContext { &mut self.trace_generation_context } + + #[cfg(test)] + fn from_parts( + trace_output: TraceBuildOutput, + trace_generation_context: TraceGenerationContext, + program_info: ProgramInfo, + ) -> Self { + Self { + trace_output, + trace_generation_context, + program_info, + } + } } impl Serializable for TraceBuildInputs { @@ -199,7 +219,7 @@ impl ExecutionTrace { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- - pub(crate) fn new_from_parts( + fn new_from_parts( program_info: ProgramInfo, trace_output: TraceBuildOutput, main_trace: MainTrace, diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 39a1995b95..1888e251c9 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -8,8 +8,9 @@ use miden_core::{ mast::{BasicBlockNode, ExecutableMastForest, MastNode, MastNodeExt, OpBatch}, serde::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, - SerializableVecDeque, read_vec_deque, + read_bounded_len, }, + utils::{SerializableVecDeque, read_vec_deque}, }; use crate::{ @@ -1759,11 +1760,29 @@ impl Serializable for MastForestResolutionReplay { impl Deserializable for MastForestResolutionReplay { fn read_from(source: &mut R) -> Result { Ok(Self { - mast_forest_resolutions: read_vec_deque(source)?, + mast_forest_resolutions: read_mast_forest_resolution_queue(source)?, }) } } +fn read_mast_forest_resolution_queue( + source: &mut R, +) -> Result, DeserializationError> { + let len = read_bounded_len( + source, + "MastForestResolutionReplay", + u32::min_serialized_size() + MastForestId::min_serialized_size(), + )?; + let mut values = VecDeque::with_capacity(len); + for _ in 0..len { + values.push_back(( + MastNodeId::from(u32::read_from(source)?), + MastForestId::read_from(source)?, + )); + } + Ok(values) +} + impl Serializable for MemoryReadsReplay { fn write_into(&self, target: &mut W) { write_memory_element_queue(&self.elements_read, target); @@ -1998,7 +2017,7 @@ impl Deserializable for HasherOp { ))), 2 => Ok(Self::HashBasicBlock(( MastForestId::read_from(source)?, - MastNodeId::read_from(source)?, + MastNodeId::from(u32::read_from(source)?), Word::read_from(source)?, ))), 3 => Ok(Self::BuildMerkleRoot(( @@ -2021,7 +2040,7 @@ impl Deserializable for HasherOp { fn min_serialized_size() -> usize { u8::min_serialized_size() + (MastForestId::min_serialized_size() - + MastNodeId::min_serialized_size() + + u32::min_serialized_size() + Word::min_serialized_size()) } } diff --git a/prover/src/lib.rs b/prover/src/lib.rs index eb16305d99..efc7868934 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -43,6 +43,8 @@ pub use miden_processor::{ }; pub use proving_options::ProvingOptions; +const TRACE_PROVING_INPUTS_ALLOCATION_BUDGET_MULTIPLIER: usize = 4; + /// Inputs required to prove from pre-executed trace data. /// /// Its binary form is a VM-owned trusted remote proving input containing trace replay data and @@ -70,6 +72,13 @@ impl TraceProvingInputs { /// Deserializes trusted remote proving inputs using the supplied byte budget. /// /// The budget bounds parsing. It does not validate sparse MAST hashes from untrusted senders. + /// This function reads one standalone payload and rejects trailing bytes. Readers for a larger + /// wrapper object should call [`TraceProvingInputs::read_from`] and let the wrapper own the + /// trailing-byte check. + /// + /// The public budget is a byte budget. Length-prefixed replay collections also need a bounded + /// allocation budget, so the reader derives a small preallocation allowance from the actual + /// payload length and caps it by the caller's byte budget. /// See . pub fn read_from_bytes_with_budget( bytes: &[u8], @@ -80,7 +89,8 @@ impl TraceProvingInputs { "TraceProvingInputs byte budget is smaller than payload length".into(), )); } - let allocation_budget = budget.min(bytes.len().saturating_mul(4)); + let allocation_budget = budget + .min(bytes.len().saturating_mul(TRACE_PROVING_INPUTS_ALLOCATION_BUDGET_MULTIPLIER)); let mut reader = BudgetedReader::new(SliceReader::new(bytes), allocation_budget); let inputs = Self::read_from(&mut reader)?; if reader.has_more_bytes() { @@ -108,7 +118,10 @@ impl Deserializable for TraceProvingInputs { } fn read_from_bytes(bytes: &[u8]) -> Result { - TraceProvingInputs::read_from_bytes_with_budget(bytes, bytes.len().saturating_mul(4)) + TraceProvingInputs::read_from_bytes_with_budget( + bytes, + bytes.len().saturating_mul(TRACE_PROVING_INPUTS_ALLOCATION_BUDGET_MULTIPLIER), + ) } fn read_from_bytes_with_budget( From 5ef5bda5e66a3b5d34b118d6a0a17ab13cc7d121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Fri, 3 Jul 2026 19:59:40 -0400 Subject: [PATCH 05/10] fix: Remove sparse MAST error qualification --- core/src/mast/sparse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/mast/sparse.rs b/core/src/mast/sparse.rs index c9327ce153..394deadc72 100644 --- a/core/src/mast/sparse.rs +++ b/core/src/mast/sparse.rs @@ -33,7 +33,7 @@ impl crate::serde::Serializable for MastForestId { impl crate::serde::Deserializable for MastForestId { fn read_from( source: &mut R, - ) -> Result { + ) -> Result { Ok(Self::from(::read_from(source)?)) } From 2a92127bfd6da92c6683956abc61bf6e9f1b13a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Fri, 10 Jul 2026 13:31:45 -0400 Subject: [PATCH 06/10] docs: clarify trace input trust boundary --- Cargo.toml | 2 +- processor/Cargo.toml | 2 +- processor/src/trace/execution_tracer.rs | 7 ++++--- processor/src/trace/mod.rs | 11 +++++++---- prover/src/lib.rs | 6 ++++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e14cc4709b..4650565f22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ paste = { version = "1.0", default-features = false } proptest = { version = "1.8", default-features = false, features = ["no_std", "alloc"] } proptest-derive = { version = "0.7", default-features = false } rand = { version = "0.10", default-features = false } -rayon = "1.10" +rayon = { version = "1.10", default-features = false } rocksdb = { version = "0.24", default-features = false } seq-macro = "0.3" serde = { version = "1.0", default-features = false, features = ["alloc", "derive", "rc"] } diff --git a/processor/Cargo.toml b/processor/Cargo.toml index b280209f27..5d1cb06104 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -49,7 +49,7 @@ hashbrown.workspace = true itertools.workspace = true paste.workspace = true proptest = { workspace = true, optional = true } -rayon = { version = "1.10", default-features = false } +rayon.workspace = true tracing.workspace = true thiserror.workspace = true diff --git a/processor/src/trace/execution_tracer.rs b/processor/src/trace/execution_tracer.rs index 9fe04b5bf3..d71d3d2e3a 100644 --- a/processor/src/trace/execution_tracer.rs +++ b/processor/src/trace/execution_tracer.rs @@ -65,8 +65,9 @@ pub struct TraceGenerationContext { /// `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 . + /// Serialized entries are trusted sparse replay data. Their node and digest maps are not + /// checked against a source [`MastForest`] commitment on read; see + /// . pub mast_forest_store: Vec>, // Replays that contain additional data needed to generate the range checker and chiplets @@ -1240,7 +1241,7 @@ impl Default for HasherChipletShim { #[cfg(test)] mod serialization_tests { use super::*; - use crate::mast::{BasicBlockNodeBuilder, MastForestContributor}; + use crate::mast::BasicBlockNodeBuilder; fn empty_trace_generation_context( fragment_size: usize, diff --git a/processor/src/trace/mod.rs b/processor/src/trace/mod.rs index 22c686aa80..58a399ad3c 100644 --- a/processor/src/trace/mod.rs +++ b/processor/src/trace/mod.rs @@ -48,8 +48,8 @@ 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 +/// Its binary form is trusted replay data. Sparse MAST node and digest maps inside the trace +/// generation context are not checked against a source `MastForest` commitment; see /// . #[derive(Debug)] pub struct TraceBuildInputs { @@ -108,7 +108,9 @@ impl Deserializable for TraceBuildOutput { &deferred_wire, DEFAULT_MAX_DEFERRED_ELEMENTS, ) - .map_err(|err| DeserializationError::InvalidValue(format!("invalid deferred state: {err}")))?; + .map_err(|err| { + DeserializationError::InvalidValue(format!("invalid deferred state: {err}")) + })?; Ok(Self { stack_outputs, deferred_state }) } @@ -146,7 +148,7 @@ impl TraceBuildInputs { // Kept for mismatch and edge-case tests that mutate replay inputs directly. #[cfg(any(test, feature = "testing"))] - #[cfg_attr(all(feature = "testing", not(test)), expect(dead_code))] + #[expect(dead_code, reason = "used only by replay-mutation tests")] fn into_parts(self) -> (TraceBuildOutput, TraceGenerationContext, ProgramInfo) { (self.trace_output, self.trace_generation_context, self.program_info) } @@ -165,6 +167,7 @@ impl TraceBuildInputs { } #[cfg(test)] + #[expect(dead_code, reason = "used only by replay-mutation tests")] fn from_parts( trace_output: TraceBuildOutput, trace_generation_context: TraceGenerationContext, diff --git a/prover/src/lib.rs b/prover/src/lib.rs index efc7868934..b69c77745f 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -49,7 +49,8 @@ const TRACE_PROVING_INPUTS_ALLOCATION_BUDGET_MULTIPLIER: usize = 4; /// /// 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. +/// sparse MAST node and digest maps are accepted as replay data and are not checked against a +/// source [`miden_core::mast::MastForest`] commitment. /// /// See for the planned untrusted reader. #[derive(Debug)] @@ -71,7 +72,8 @@ impl TraceProvingInputs { /// Deserializes trusted remote proving inputs using the supplied byte budget. /// - /// The budget bounds parsing. It does not validate sparse MAST hashes from untrusted senders. + /// The budget bounds parsing. It does not validate sparse MAST replay data from untrusted + /// senders. /// This function reads one standalone payload and rejects trailing bytes. Readers for a larger /// wrapper object should call [`TraceProvingInputs::read_from`] and let the wrapper own the /// trailing-byte check. From 0d44a48b4cbe9a78e36ecd9aa705cffeba645ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Sat, 25 Jul 2026 15:54:06 -0400 Subject: [PATCH 07/10] refactor: reduce trace input serialization diff --- core/src/utils/mod.rs | 58 ++--------------------- processor/src/trace/chiplets/ace/trace.rs | 2 +- processor/src/trace/mod.rs | 21 -------- processor/src/trace/trace_state.rs | 26 +++++++++- 4 files changed, 29 insertions(+), 78 deletions(-) diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index 6ca1508de8..f592d1f0c8 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -1,19 +1,8 @@ -use alloc::{collections::VecDeque, vec::Vec}; +use alloc::vec::Vec; use core::ops::{Bound, Range}; -use crate::{ - Felt, Word, - crypto::hash::Blake3_256, - field::PrimeCharacteristicRing, - serde::{ - ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, - read_bounded_len, - }, -}; - // RE-EXPORTS // ================================================================================================ - #[cfg(feature = "std")] pub use miden_crypto::utils::ReadAdapter; pub use miden_crypto::{ @@ -29,6 +18,8 @@ pub use miden_utils_indexing::{ newtype_id, }; +use crate::{Felt, Word, crypto::hash::Blake3_256, field::PrimeCharacteristicRing}; + // TO ELEMENTS // ================================================================================================ @@ -110,35 +101,6 @@ where } } -// VECDEQUE SERIALIZATION -// ================================================================================================ - -/// 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) -} - // BYTE CONVERSIONS // ================================================================================================ @@ -219,7 +181,6 @@ mod tests { use proptest::prelude::*; use super::*; - use crate::serde::{Deserializable, Serializable, SliceReader}; proptest! { #[test] @@ -248,17 +209,4 @@ mod tests { // https://github.com/0xMiden/miden-vm/issues/433 debug_assert!(false); } - - #[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 SliceReader::new(&bytes)).unwrap(); - assert_eq!(values, restored); - - let vec = Vec::::read_from_bytes(&bytes).unwrap(); - assert_eq!(vec, [1, 2, 3]); - } } diff --git a/processor/src/trace/chiplets/ace/trace.rs b/processor/src/trace/chiplets/ace/trace.rs index 9294ed8e5e..55c96cbb3d 100644 --- a/processor/src/trace/chiplets/ace/trace.rs +++ b/processor/src/trace/chiplets/ace/trace.rs @@ -327,7 +327,7 @@ fn write_quad_felt(value: QuadFelt, target: &mut W) { 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])) + Ok(QuadFelt::new([c0, c1])) } fn quad_felt_min_serialized_size() -> usize { diff --git a/processor/src/trace/mod.rs b/processor/src/trace/mod.rs index 58a399ad3c..7b9a3a03d2 100644 --- a/processor/src/trace/mod.rs +++ b/processor/src/trace/mod.rs @@ -146,13 +146,6 @@ impl TraceBuildInputs { &self.program_info } - // Kept for mismatch and edge-case tests that mutate replay inputs directly. - #[cfg(any(test, feature = "testing"))] - #[expect(dead_code, reason = "used only by replay-mutation tests")] - fn into_parts(self) -> (TraceBuildOutput, TraceGenerationContext, ProgramInfo) { - (self.trace_output, self.trace_generation_context, self.program_info) - } - #[cfg(any(test, feature = "testing"))] /// Returns the trace replay context captured during execution. pub fn trace_generation_context(&self) -> &TraceGenerationContext { @@ -165,20 +158,6 @@ impl TraceBuildInputs { pub(crate) fn trace_generation_context_mut(&mut self) -> &mut TraceGenerationContext { &mut self.trace_generation_context } - - #[cfg(test)] - #[expect(dead_code, reason = "used only by replay-mutation tests")] - fn from_parts( - trace_output: TraceBuildOutput, - trace_generation_context: TraceGenerationContext, - program_info: ProgramInfo, - ) -> Self { - Self { - trace_output, - trace_generation_context, - program_info, - } - } } impl Serializable for TraceBuildInputs { diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 1888e251c9..417aacea54 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -10,7 +10,6 @@ use miden_core::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, read_bounded_len, }, - utils::{SerializableVecDeque, read_vec_deque}, }; use crate::{ @@ -1508,6 +1507,31 @@ fn read_row_index(source: &mut R) -> Result`: a length prefix followed by items in iteration order. +struct SerializableVecDeque<'a, T>(&'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); + } + } +} + +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) +} + fn write_memory_element_queue( queue: &VecDeque<(Felt, Felt, ContextId, RowIndex)>, target: &mut W, From b322aa97999b48f32446bdfb64037e5fcdc51f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Tue, 28 Jul 2026 09:41:09 -0400 Subject: [PATCH 08/10] fix: remove duplicate MastForestId serde impls --- core/src/mast/sparse.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/core/src/mast/sparse.rs b/core/src/mast/sparse.rs index 394deadc72..ff30950d15 100644 --- a/core/src/mast/sparse.rs +++ b/core/src/mast/sparse.rs @@ -24,24 +24,6 @@ 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() - } -} - #[cfg(feature = "arbitrary")] impl proptest::prelude::Arbitrary for MastForestId { type Parameters = (); From e689cb59c4a134ef4e92b553df78641673ba292d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Thu, 30 Jul 2026 15:27:23 -0400 Subject: [PATCH 09/10] fix: adapt trace serialization tests to streamed hasher replay --- processor/src/trace/execution_tracer.rs | 3 ++- processor/src/trace/trace_state.rs | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/processor/src/trace/execution_tracer.rs b/processor/src/trace/execution_tracer.rs index d71d3d2e3a..3eb8c014e9 100644 --- a/processor/src/trace/execution_tracer.rs +++ b/processor/src/trace/execution_tracer.rs @@ -1382,10 +1382,11 @@ mod serialization_tests { #[test] fn trace_generation_context_read_rejects_bad_hasher_replay_forest_id() { let mut context = valid_trace_generation_context(); + let basic_block = BasicBlockNodeBuilder::new(vec![Operation::Noop]).build().unwrap(); context.hasher_for_chiplet.record_hash_basic_block( MastForestId::from(1u32), MastNodeId::from(0), - Word::default(), + &basic_block, ); assert_context_read_rejects_bad_forest_id(context, "hasher HashBasicBlock replay"); diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 417aacea54..9e0fe30caa 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -1331,10 +1331,13 @@ impl HasherRequestReplay { pub(crate) fn iter_hash_basic_block_forest_ids( &self, ) -> impl Iterator + '_ { - self.buffered_ops().into_iter().flat_map(|ops| ops.iter()).filter_map(|op| match op { - HasherOp::HashBasicBlock((forest_id, _node_id, _expected_hash)) => Some(*forest_id), - _ => None, - }) + self.buffered_ops() + .into_iter() + .flat_map(|ops| ops.iter()) + .filter_map(|op| match op { + HasherOp::HashBasicBlock((forest_id, _node_id, _expected_hash)) => Some(*forest_id), + _ => None, + }) } fn buffered_ops(&self) -> Option<&VecDeque> { @@ -2083,7 +2086,9 @@ impl Serializable for HasherRequestReplay { impl Deserializable for HasherRequestReplay { fn read_from(source: &mut R) -> Result { - Ok(Self { sink: HasherOpSink::Buffered(read_vec_deque(source)?) }) + Ok(Self { + sink: HasherOpSink::Buffered(read_vec_deque(source)?), + }) } } From a382dd2de7080ff7d6383b926ec9e8ddf52a032f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Thu, 6 Aug 2026 14:41:11 -0400 Subject: [PATCH 10/10] chore: move trace proving input changelog entry to v0.30.0 section --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eec9b65ab5..f5a0a24a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## v0.30.0 (Unreleased) +#### Features + +- Added trusted trace proving input serialization for remote proving ([#3314](https://github.com/0xMiden/miden-vm/pull/3314)). + ## v0.29.0 (2026-08-04) #### Changes @@ -144,7 +148,6 @@ - Reduced optimized benchmark build time by relaxing forced inlining in processor execution helpers ([#3292](https://github.com/0xMiden/miden-vm/pull/3292)). - Added no-op handlers for readonly debugger events to `CoreLibrary::handlers`, so hosts that load the core library can execute programs emitting those events without registering no-op handlers manually ([#3305](https://github.com/0xMiden/miden-vm/pull/3305)). - Added trusted sparse MAST forest serialization for trace replay payloads ([#3313](https://github.com/0xMiden/miden-vm/pull/3313)). -- Added trusted trace proving input serialization for remote proving ([#3314](https://github.com/0xMiden/miden-vm/pull/3314)). ## miden-vm v0.24.0 (2026-06-24)