diff --git a/CHANGELOG.md b/CHANGELOG.md index 925d7f32fc..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 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/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/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/core/src/mast/sparse.rs b/core/src/mast/sparse.rs index c2897bfd34..ff30950d15 100644 --- a/core/src/mast/sparse.rs +++ b/core/src/mast/sparse.rs @@ -24,6 +24,17 @@ use crate::{ // `MastNodeId`, which is meaningful only within one forest's node store. newtype_id!(MastForestId); +#[cfg(feature = "arbitrary")] +impl proptest::prelude::Arbitrary for MastForestId { + type Parameters = (); + type Strategy = proptest::prelude::BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + use proptest::prelude::*; + any::().prop_map(Self::from).boxed() + } +} + // SPARSE MAST FOREST // ================================================================================================ @@ -267,6 +278,22 @@ impl ExecutableMastForest for SparseMastForest { } } +#[cfg(all(feature = "arbitrary", test))] +mod serde_tests { + use proptest::prelude::*; + + use super::*; + use crate::serde::{Deserializable, Serializable}; + + proptest! { + #[test] + fn mast_forest_id_binary_serde_roundtrip(id in any::()) { + let bytes = id.to_bytes(); + prop_assert_eq!(id, MastForestId::read_from_bytes(&bytes).unwrap()); + } + } +} + // SPARSE MAST FOREST BUILDER // ================================================================================================ diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index c90cf2c2ee..f592d1f0c8 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -176,6 +176,8 @@ pub fn packed_u32_elements_to_bytes(elements: &[Felt]) -> Vec { #[cfg(test)] mod tests { + use alloc::vec::Vec; + use proptest::prelude::*; use super::*; diff --git a/processor/Cargo.toml b/processor/Cargo.toml index 118bf2da9e..5d1cb06104 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 +proptest = { workspace = true, optional = true } rayon.workspace = true 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/continuation_stack.rs b/processor/src/continuation_stack.rs index d91bf6d007..657d82271e 100644 --- a/processor/src/continuation_stack.rs +++ b/processor/src/continuation_stack.rs @@ -1,11 +1,26 @@ 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. 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 // ================================================================================================ @@ -18,7 +33,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 +137,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 +388,191 @@ 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) => { + TAG_START_NODE.write_into(target); + node_id.write_into(target); + }, + Self::FinishJoin(node_id) => { + TAG_FINISH_JOIN.write_into(target); + node_id.write_into(target); + }, + Self::FinishSplit(node_id) => { + TAG_FINISH_SPLIT.write_into(target); + node_id.write_into(target); + }, + Self::FinishLoop(node_id) => { + TAG_FINISH_LOOP.write_into(target); + node_id.write_into(target); + }, + Self::FinishCall(node_id) => { + TAG_FINISH_CALL.write_into(target); + node_id.write_into(target); + }, + Self::FinishDyn(node_id) => { + TAG_FINISH_DYN.write_into(target); + node_id.write_into(target); + }, + Self::ResumeBasicBlock { node_id, batch_index, op_idx_in_batch } => { + 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 } => { + TAG_RESPAN.write_into(target); + node_id.write_into(target); + batch_index.write_into(target); + }, + Self::FinishBasicBlock(node_id) => { + TAG_FINISH_BASIC_BLOCK.write_into(target); + node_id.write_into(target); + }, + Self::EnterForest { forest, package_debug_info: _ } => { + TAG_ENTER_FOREST.write_into(target); + forest.write_into(target); + }, + } + } +} + +impl Deserializable for Continuation { + fn read_from(source: &mut R) -> Result { + match u8::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)?, + }), + TAG_RESPAN => Ok(Self::Respan { + node_id: read_mast_node_id(source)?, + batch_index: usize::read_from(source)?, + }), + 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, + }), + tag => { + Err(DeserializationError::InvalidValue(format!("invalid continuation tag {tag}"))) + }, + } + } +} + +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); + 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 +641,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..55c96cbb3d 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,398 @@ 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::new([c0, c1])) +} + +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(()) + } +} + +// ARBITRARY TEST SUPPORT +// ================================================================================================ + +#[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..3eb8c014e9 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,10 @@ 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 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 @@ -80,6 +88,134 @@ 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)?, + }; + context.validate_invariants()?; + context.validate_forest_ids()?; + Ok(context) + } +} + +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!( + "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(()) + } + + 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( + 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 self.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 +1237,158 @@ impl Default for HasherChipletShim { Self::new() } } + +#[cfg(test)] +mod serialization_tests { + use super::*; + use crate::mast::BasicBlockNodeBuilder; + + 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(); + let basic_block = BasicBlockNodeBuilder::new(vec![Operation::Noop]).build().unwrap(); + context.hasher_for_chiplet.record_hash_basic_block( + MastForestId::from(1u32), + MastNodeId::from(0), + &basic_block, + ); + + 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..7b9a3a03d2 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 node and digest maps inside the trace +/// generation context are not checked against a source `MastForest` commitment; see +/// . #[derive(Debug)] pub struct TraceBuildInputs { trace_output: TraceBuildOutput, @@ -63,7 +70,7 @@ impl TraceBuildInputs { } #[derive(Debug)] -pub(crate) struct TraceBuildOutput { +struct TraceBuildOutput { stack_outputs: StackOutputs, deferred_state: DeferredState, } @@ -81,6 +88,34 @@ 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 +160,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 // ================================================================================================ @@ -148,7 +201,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 c0b726f707..9e0fe30caa 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, + read_bounded_len, + }, +}; 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, @@ -1098,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()) @@ -1212,6 +1328,26 @@ 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 +1408,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 +1499,643 @@ 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)?)) +} + +/// Serializable view over a [`VecDeque`]. +/// +/// This uses the same wire shape as `Vec`: 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, +) { + 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_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); + 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::from(u32::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() + + u32::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::*; @@ -1374,3 +2151,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"] } diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 7c89786f4a..b69c77745f 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, @@ -35,7 +43,16 @@ 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 +/// proof-generation options. Deserialization checks malformed structure and bounded allocation, but +/// 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)] pub struct TraceProvingInputs { trace_inputs: TraceBuildInputs, @@ -52,6 +69,69 @@ 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 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. + /// + /// 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], + 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(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() { + 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(TRACE_PROVING_INPUTS_ALLOCATION_BUDGET_MULTIPLIER), + ) + } + + 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() + } +}