diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a0a24a18..aa034aa8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Features - Added trusted trace proving input serialization for remote proving ([#3314](https://github.com/0xMiden/miden-vm/pull/3314)). +- Added round-trip and proptest coverage for trusted trace proving input serialization ([#3315](https://github.com/0xMiden/miden-vm/pull/3315)). ## v0.29.0 (2026-08-04) diff --git a/air/Cargo.toml b/air/Cargo.toml index 356cc4c2d5..669a8ad23a 100644 --- a/air/Cargo.toml +++ b/air/Cargo.toml @@ -22,7 +22,7 @@ default = ["std"] arbitrary = ["std", "dep:proptest"] std = ["miden-core/std", "proptest?/std", "thiserror/std"] concurrent = ["std"] -testing = [] +testing = ["arbitrary"] [dependencies] # Miden dependencies diff --git a/crates/assembly/Cargo.toml b/crates/assembly/Cargo.toml index 7c467608c4..a4ef278d76 100644 --- a/crates/assembly/Cargo.toml +++ b/crates/assembly/Cargo.toml @@ -30,7 +30,7 @@ std = [ "tempfile/getrandom", "thiserror/std", ] -testing = ["logging", "miden-assembly-syntax/testing"] +testing = ["arbitrary", "logging", "miden-assembly-syntax/testing"] logging = ["dep:env_logger"] [dependencies] diff --git a/crates/debug-types/Cargo.toml b/crates/debug-types/Cargo.toml index 606e5a1d53..4d5de40a85 100644 --- a/crates/debug-types/Cargo.toml +++ b/crates/debug-types/Cargo.toml @@ -30,6 +30,7 @@ std = [ ] arbitrary = ["std", "dep:proptest"] serde = ["dep:serde", "dep:serde_spanned", "serde_spanned?/serde"] +testing = ["arbitrary"] [dependencies] # Miden dependencies diff --git a/crates/mast-package/Cargo.toml b/crates/mast-package/Cargo.toml index 943cbc80e7..0831dbb4f1 100644 --- a/crates/mast-package/Cargo.toml +++ b/crates/mast-package/Cargo.toml @@ -22,6 +22,7 @@ default = ["std"] arbitrary = ["std", "dep:proptest", "dep:proptest-derive", "miden-assembly-syntax/arbitrary", "miden-core/arbitrary"] std = ["miden-assembly-syntax/std", "miden-core/std", "miden-utils-indexing/std", "rustc-hash/std", "serde?/std", "thiserror/std"] serde = ["dep:serde", "miden-assembly-syntax/serde", "miden-core/serde", "miden-utils-indexing/serde"] +testing = ["arbitrary"] [dependencies] # Miden dependencies diff --git a/crates/package-registry/Cargo.toml b/crates/package-registry/Cargo.toml index ba21c9eefe..e096d5fb43 100644 --- a/crates/package-registry/Cargo.toml +++ b/crates/package-registry/Cargo.toml @@ -22,6 +22,7 @@ arbitrary = ["std", "dep:proptest", "miden-mast-package/arbitrary"] resolver = ["std", "dep:pubgrub", "dep:smallvec"] std = ["miden-assembly-syntax/std", "miden-core/std", "proptest?/std", "serde?/std"] serde = ["dep:serde", "miden-assembly-syntax/serde", "miden-core/serde", "miden-mast-package/serde"] +testing = ["arbitrary"] [dependencies] miden-assembly-syntax.workspace = true diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 4d239688ba..482db373c8 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -19,6 +19,7 @@ arbitrary = ["std", "dep:proptest", "dep:proptest-derive", "miden-assembly-synta resolver = ["std", "miden-package-registry/resolver"] std = ["miden-assembly-syntax/std", "miden-package-registry/std", "miden-package-registry/resolver", "proptest?/std", "serde?/std", "tempfile/getrandom", "thiserror/std", "toml/std"] serde = ["dep:serde", "dep:serde-untagged", "miden-assembly-syntax/serde", "miden-core/serde", "miden-package-registry/serde", "toml/serde", "toml/parse", "toml/display"] +testing = ["arbitrary"] [dependencies] miden-assembly-syntax.workspace = true diff --git a/crates/utils-indexing/Cargo.toml b/crates/utils-indexing/Cargo.toml index 4ab193a31b..25d2009a2c 100644 --- a/crates/utils-indexing/Cargo.toml +++ b/crates/utils-indexing/Cargo.toml @@ -20,6 +20,7 @@ arbitrary = ["std", "dep:proptest"] default = ["std"] std = ["proptest?/std"] serde = ["dep:serde"] +testing = ["arbitrary"] [dependencies] # Required dependencies diff --git a/miden-vm/tests/integration/prove_verify.rs b/miden-vm/tests/integration/prove_verify.rs index dcd75b4752..5349ef2594 100644 --- a/miden-vm/tests/integration/prove_verify.rs +++ b/miden-vm/tests/integration/prove_verify.rs @@ -237,18 +237,24 @@ mod fast_parallel { use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core::{ + Word, + mast::{ + BasicBlockNodeBuilder, ExternalNodeBuilder, JoinNodeBuilder, MastForest, MastNodeExt, + }, + operations::Operation, program::ExecutionClaim, proof::{DeferredProof, ExecutionProof, HashFunction}, }; use miden_processor::{ - DefaultHost, ExecutionOptions, FastProcessor, StackInputs, advice::AdviceInputs, - trace::build_trace, + DefaultHost, ExecutionOptions, FastProcessor, HostLibrary, StackInputs, + advice::AdviceInputs, trace::build_trace, }; use miden_prover::{ ProvingOptions, TraceProvingInputs, config, prove_from_trace_sync, prove_partial_from_trace_sync, prove_stark, + serde::{Deserializable, Serializable}, }; - use miden_verifier::verify; + use miden_verifier::{VerificationError, Verifier, verify}; use miden_vm::{Program, TraceBuildInputs}; /// Default fragment size for parallel trace generation @@ -276,6 +282,38 @@ mod fast_parallel { DefaultHost::default().with_source_manager(Arc::new(DefaultSourceManager::default())) } + fn create_simple_library() -> HostLibrary { + let mut mast_forest = MastForest::new(); + let swap_block = BasicBlockNodeBuilder::new(vec![Operation::Swap, Operation::Swap]) + .add_to_forest(&mut mast_forest) + .unwrap(); + mast_forest.make_root(swap_block); + HostLibrary::from(Arc::new(mast_forest)) + } + + fn external_lib_proc_digest() -> Word { + let mut forest = MastForest::new(); + let swap_block = BasicBlockNodeBuilder::new(vec![Operation::Swap, Operation::Swap]) + .add_to_forest(&mut forest) + .unwrap(); + forest.get_node_by_id(swap_block).unwrap().digest() + } + + fn external_program() -> Program { + let mut program = MastForest::new(); + let basic_block = BasicBlockNodeBuilder::new(vec![Operation::Pad, Operation::Drop]) + .add_to_forest(&mut program) + .unwrap(); + let external_node = ExternalNodeBuilder::new(external_lib_proc_digest()) + .add_to_forest(&mut program) + .unwrap(); + let root = JoinNodeBuilder::new([basic_block, external_node]) + .add_to_forest(&mut program) + .unwrap(); + program.make_root(root); + Program::new(Arc::new(program), root) + } + /// Test that proves and verifies using the fast processor + parallel trace generation path. /// This verifies the complete code path works end-to-end. /// @@ -372,7 +410,79 @@ mod fast_parallel { } #[test] - fn test_prove_from_trace_sync_preserves_deferred_wire() { + fn test_trace_proving_inputs_round_trip_proves_external_library_program() { + std::thread::Builder::new() + .name("trace-proving-inputs-round-trip".into()) + .stack_size(8 * 1024 * 1024) + .spawn(trace_proving_inputs_round_trip_proves_external_library_program) + .expect("failed to spawn round-trip test thread") + .join() + .expect("round-trip test thread panicked"); + } + + fn trace_proving_inputs_round_trip_proves_external_library_program() { + let program = external_program(); + let stack_inputs = StackInputs::default(); + let advice_inputs = AdviceInputs::default(); + let mut host = default_source_manager_host(); + host.load_library(create_simple_library()) + .expect("failed to load test library into host"); + let trace_inputs = + execute_parallel_trace_inputs(&program, stack_inputs, advice_inputs, &mut host); + + let trace_inputs_bytes = trace_inputs.to_bytes(); + let original_trace = build_trace(trace_inputs).expect("original trace inputs build trace"); + let restored_trace_inputs = TraceBuildInputs::read_from_bytes(&trace_inputs_bytes) + .expect("trace inputs round trip"); + assert!( + restored_trace_inputs.trace_generation_context().mast_forest_store.len() > 1, + "expected dynamic library execution to serialize multiple MAST forests" + ); + let restored_trace = + build_trace(restored_trace_inputs).expect("restored trace inputs build trace"); + assert_eq!(restored_trace.stack_outputs(), original_trace.stack_outputs()); + assert_eq!(restored_trace.program_info(), original_trace.program_info()); + assert_eq!(restored_trace.trace_len_summary(), original_trace.trace_len_summary()); + assert_eq!( + restored_trace.public_inputs().to_air_inputs(), + original_trace.public_inputs().to_air_inputs() + ); + + let trace_inputs = TraceBuildInputs::read_from_bytes(&trace_inputs_bytes) + .expect("trace inputs round trip"); + let proving_inputs = TraceProvingInputs::new( + trace_inputs, + ProvingOptions::with_96_bit_security(HashFunction::Blake3_256), + ); + let proving_inputs_bytes = proving_inputs.to_bytes(); + let proving_inputs_budget = + proving_inputs_bytes.len().checked_mul(4).expect("test input budget overflow"); + let mut proving_inputs_with_trailing_byte = proving_inputs_bytes.clone(); + proving_inputs_with_trailing_byte.push(0); + let err = TraceProvingInputs::read_from_bytes_with_budget( + &proving_inputs_with_trailing_byte, + proving_inputs_with_trailing_byte + .len() + .checked_mul(4) + .expect("test input budget overflow"), + ) + .unwrap_err(); + assert!(err.to_string().contains("TraceProvingInputs payload has trailing bytes")); + let restored_proving_inputs = TraceProvingInputs::read_from_bytes_with_budget( + &proving_inputs_bytes, + proving_inputs_budget, + ) + .expect("trace proving inputs round trip"); + + let (stack_outputs, proof) = + prove_from_trace_sync(restored_proving_inputs).expect("prove_from_trace_sync failed"); + + let claim = ExecutionClaim::from_program_info(program.into(), stack_inputs, stack_outputs); + verify(proof, claim).expect("Verification failed"); + } + + #[test] + fn test_prove_partial_from_trace_sync_preserves_deferred_wire() { let source = "begin log_deferred end"; let program = Assembler::default() .assemble_program("program", source) @@ -383,6 +493,7 @@ mod fast_parallel { let mut host = default_source_manager_host(); let trace_inputs = execute_parallel_trace_inputs(&program, stack_inputs, advice_inputs, &mut host); + let expected_deferred_root = trace_inputs.deferred_state().root(); let expected_wire = trace_inputs .deferred_state() .to_wire() @@ -398,13 +509,25 @@ mod fast_parallel { )) .expect("prove_partial_from_trace_sync failed"); - assert_eq!(proof.deferred_proof().as_wire(), Some(&expected_wire)); + assert!(!proof.is_final()); + assert_eq!(proof.deferred_proof(), &DeferredProof::Wire(expected_wire.clone())); let claim = ExecutionClaim::from_program_info(program.into(), stack_inputs, stack_outputs); - let (_, pending) = miden_verifier::Verifier::new() + let err = Verifier::new().verify(proof.clone(), claim.clone()).unwrap_err(); + assert!( + matches!(err, VerificationError::UnsupportedDeferredProof), + "wire-backed partial proofs should be rejected by final verification, got {err:?}" + ); + + let (security_level, pending) = Verifier::new() .verify_partial(proof, claim) - .expect("partial verification failed"); - assert_ne!(pending.root(), miden_core::deferred::TRUE_DIGEST); - let _state = pending.into_state(); + .expect("wire-backed partial proof should verify and hydrate deferred state"); + assert_eq!(security_level, 96); + assert_eq!(pending.root(), expected_deferred_root); + let hydrated_state = pending.into_state(); + assert_eq!( + hydrated_state.to_wire().expect("hydrated state should serialize to wire"), + expected_wire + ); } } diff --git a/processor/src/trace/trace_state.rs b/processor/src/trace/trace_state.rs index 9e0fe30caa..d739151355 100644 --- a/processor/src/trace/trace_state.rs +++ b/processor/src/trace/trace_state.rs @@ -2154,3 +2154,6 @@ mod tests { #[cfg(feature = "arbitrary")] mod arbitrary; + +#[cfg(test)] +mod serialization_tests; diff --git a/processor/src/trace/trace_state/serialization_tests.rs b/processor/src/trace/trace_state/serialization_tests.rs new file mode 100644 index 0000000000..3b89e46ca9 --- /dev/null +++ b/processor/src/trace/trace_state/serialization_tests.rs @@ -0,0 +1,121 @@ +use alloc::vec::Vec; + +use miden_core::serde::{BudgetedReader, SliceReader}; + +use super::*; +use crate::mast::MastForestId; + +#[test] +fn memory_replay_queues_reject_oversized_lengths_before_allocation() { + let mut bytes = Vec::new(); + usize::MAX.write_into(&mut bytes); + + let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); + let err = read_memory_element_queue(&mut reader).unwrap_err(); + assert!(err.to_string().contains("exceeds reader allocation bound")); + + let mut reader = BudgetedReader::new(SliceReader::new(&bytes), bytes.len()); + let err = read_memory_word_queue(&mut reader).unwrap_err(); + assert!(err.to_string().contains("exceeds reader allocation bound")); +} + +#[test] +fn stack_state_read_rejects_depth_below_minimum() { + let mut bytes = Vec::new(); + [ZERO; MIN_STACK_DEPTH].write_into(&mut bytes); + (MIN_STACK_DEPTH - 1).write_into(&mut bytes); + ZERO.write_into(&mut bytes); + + let err = StackState::read_from_bytes(&bytes).unwrap_err(); + assert!(err.to_string().contains("below minimum")); +} + +#[test] +fn execution_replay_round_trip_preserves_replay_order() { + let mut replay = ExecutionReplay::default(); + replay.block_stack.record_node_start_parent_addr(Felt::from_u32(7)); + replay + .block_stack + .record_node_end(Felt::from_u32(9), Felt::from_u32(8), Felt::from_u32(7)); + replay.execution_context.record_execution_context(ExecutionContextSystemInfo { + parent_ctx: ContextId::from(3), + parent_fn_hash: Word::from([ONE, ZERO, ZERO, ZERO]), + }); + replay + .stack_overflow + .record_pop_overflow(Felt::from_u32(11), Felt::from_u32(10)); + replay + .stack_overflow + .record_restore_context_overflow_addr(17, Felt::from_u32(16)); + replay.memory_reads.record_read_element( + Felt::from_u32(21), + Felt::from_u32(20), + ContextId::from(4), + RowIndex::from(5_u32), + ); + replay.memory_reads.record_read_word( + Word::from([Felt::from_u32(1), Felt::from_u32(2), Felt::from_u32(3), Felt::from_u32(4)]), + Felt::from_u32(24), + ContextId::from(4), + RowIndex::from(6_u32), + ); + replay.advice.record_pop_stack(Felt::from_u32(31)); + replay + .advice + .record_pop_stack_word(Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO])); + replay.advice.record_pop_stack_dword([ + Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), + Word::default(), + ]); + replay.hasher.record_permute(Felt::from_u32(40), [Felt::from_u32(41); 12]); + replay.block_address.record_block_address(Felt::from_u32(50)); + replay + .mast_forest_resolution + .record_resolution(MastNodeId::from(2), MastForestId::from(1)); + + let bytes = replay.to_bytes(); + let mut restored = ExecutionReplay::read_from_bytes(&bytes).unwrap(); + + assert_eq!(restored.block_stack.replay_node_start_parent_addr().unwrap(), Felt::from_u32(7)); + let node_end = restored.block_stack.replay_node_end().unwrap(); + assert_eq!(node_end.ended_node_addr, Felt::from_u32(9)); + assert_eq!(node_end.prev_addr, Felt::from_u32(8)); + assert_eq!(node_end.prev_parent_addr, Felt::from_u32(7)); + let ctx = restored.execution_context.replay_execution_context().unwrap(); + assert_eq!(ctx.parent_ctx, ContextId::from(3)); + assert_eq!(ctx.parent_fn_hash, Word::from([ONE, ZERO, ZERO, ZERO])); + assert_eq!( + restored.stack_overflow.replay_pop_overflow().unwrap(), + (Felt::from_u32(11), Felt::from_u32(10)) + ); + assert_eq!( + restored.stack_overflow.replay_restore_context_overflow_addr().unwrap(), + (17, Felt::from_u32(16)) + ); + assert_eq!( + restored.memory_reads.replay_read_element(Felt::from_u32(20)).unwrap(), + Felt::from_u32(21) + ); + assert_eq!( + restored.memory_reads.replay_read_word(Felt::from_u32(24)).unwrap(), + Word::from([Felt::from_u32(1), Felt::from_u32(2), Felt::from_u32(3), Felt::from_u32(4)]) + ); + assert_eq!(restored.advice.replay_pop_stack().unwrap(), Felt::from_u32(31)); + assert_eq!( + restored.advice.replay_pop_stack_word().unwrap(), + Word::from([Felt::from_u32(5), ZERO, ZERO, ZERO]) + ); + assert_eq!( + restored.advice.replay_pop_stack_dword().unwrap(), + [Word::from([Felt::from_u32(6), ZERO, ZERO, ZERO]), Word::default()] + ); + assert_eq!( + restored.hasher.replay_permute().unwrap(), + (Felt::from_u32(40), [Felt::from_u32(41); 12]) + ); + assert_eq!(restored.block_address.replay_block_address().unwrap(), Felt::from_u32(50)); + assert_eq!( + restored.mast_forest_resolution.replay_resolution().unwrap(), + (MastNodeId::from(2), MastForestId::from(1)) + ); +}