use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use miden_core::{
mast::MastForest,
program::StackInputs,
serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
};
use miden_mast_package::{Package, debug_info::PackageDebugInfo};
use miden_processor::{
ExecutionOptions, LoadedMastForest,
advice::{AdviceInputs, AdviceMutation},
};
use super::advice::{read_event_log, write_event_log};
#[derive(Clone, Default)]
pub struct MastForestRecorder {
forests: Arc<Mutex<Vec<LoadedMastForest>>>,
}
impl MastForestRecorder {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> Vec<LoadedMastForest> {
self.forests.lock().expect("mast forest log poisoned").clone()
}
pub(crate) fn record(&self, forest: LoadedMastForest) {
let mut guard = self.forests.lock().expect("mast forest log poisoned");
if !guard
.iter()
.any(|existing| Arc::ptr_eq(existing.mast_forest(), forest.mast_forest()))
{
guard.push(forest);
}
}
pub(crate) fn clear(&self) {
self.forests.lock().expect("mast forest log poisoned").clear();
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReplaySnapshotWrite {
pub path: PathBuf,
pub event_count: usize,
pub forest_count: usize,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("failed to write replay snapshot to {}: {}", path.display(), message)]
pub struct ReplaySnapshotWriteError {
pub path: PathBuf,
pub message: String,
}
#[derive(Clone, Debug, Default)]
pub struct ReplaySnapshotRecorder {
status: Arc<Mutex<Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>>>>,
}
impl ReplaySnapshotRecorder {
pub fn new() -> Self {
Self::default()
}
pub fn take(&self) -> Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>> {
self.status.lock().expect("replay snapshot status poisoned").take()
}
pub(crate) fn record_success(&self, write: ReplaySnapshotWrite) {
*self.status.lock().expect("replay snapshot status poisoned") = Some(Ok(write));
}
pub(crate) fn record_error(&self, path: PathBuf, err: impl ToString) {
*self.status.lock().expect("replay snapshot status poisoned") =
Some(Err(ReplaySnapshotWriteError {
path,
message: err.to_string(),
}));
}
}
const SNAPSHOT_MAGIC: [u8; 6] = *b"MDNSNP";
const SNAPSHOT_VERSION: u8 = 3;
pub struct ReplaySnapshot {
pub package: Arc<Package>,
pub stack_inputs: StackInputs,
pub advice_inputs: AdviceInputs,
pub options: ExecutionOptions,
pub mast_forests: Vec<LoadedMastForest>,
pub event_log: Vec<Vec<AdviceMutation>>,
}
impl ReplaySnapshot {
pub fn write_to_file(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
std::fs::write(path, self.to_bytes())
}
pub fn read_from_file(path: impl AsRef<Path>) -> Result<Self, ReplaySnapshotError> {
let bytes = std::fs::read(path).map_err(ReplaySnapshotError::Io)?;
Self::read_from_bytes(&bytes).map_err(ReplaySnapshotError::Deserialization)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
self.write_into(&mut bytes);
bytes
}
pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
let mut reader = miden_core::serde::SliceReader::new(bytes);
Self::read_from(&mut reader)
}
}
impl Serializable for ReplaySnapshot {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_bytes(&SNAPSHOT_MAGIC);
target.write_u8(SNAPSHOT_VERSION);
self.package.write_into(target);
self.stack_inputs.write_into(target);
self.advice_inputs.write_into(target);
write_execution_options(&self.options, target);
target.write_usize(self.mast_forests.len());
for forest in &self.mast_forests {
forest.mast_forest().as_ref().write_into(target);
match forest.package_debug_info().ok().flatten() {
Some(debug_info) => {
target.write_bool(true);
debug_info.as_ref().write_into(target);
}
None => {
target.write_bool(false);
}
}
}
write_event_log(&self.event_log, target);
}
}
impl Deserializable for ReplaySnapshot {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let magic: [u8; 6] = source.read_array()?;
if magic != SNAPSHOT_MAGIC {
return Err(DeserializationError::InvalidValue(
"not a Miden debugger replay snapshot (bad magic)".to_string(),
));
}
let version = source.read_u8()?;
if version != SNAPSHOT_VERSION {
return Err(DeserializationError::InvalidValue(format!(
"unsupported replay snapshot version {version} (expected {SNAPSHOT_VERSION})"
)));
}
let package = Arc::new(Package::read_from_trusted(source)?);
let stack_inputs = StackInputs::read_from(source)?;
let advice_inputs = AdviceInputs::read_from(source)?;
let options = read_execution_options(source)?;
let forest_count = source.read_usize()?;
let mut mast_forests = Vec::with_capacity(forest_count);
for _ in 0..forest_count {
let mast_forest = Arc::new(MastForest::read_from(source)?);
mast_forests.push(if source.read_bool()? {
let debug_info = Some(PackageDebugInfo::read_from(source)?);
LoadedMastForest::with_package_debug_info(mast_forest, Ok(debug_info))
} else {
LoadedMastForest::new(mast_forest)
});
}
let event_log = read_event_log(source)?;
Ok(Self {
package,
stack_inputs,
advice_inputs,
options,
mast_forests,
event_log,
})
}
}
fn write_execution_options<W: ByteWriter>(options: &ExecutionOptions, target: &mut W) {
target.write_u32(options.max_cycles());
target.write_u32(options.expected_cycles());
target.write_usize(options.core_trace_fragment_size());
target.write_usize(options.max_advice_size_bytes());
target.write_usize(options.max_hash_len_bytes());
target.write_bool(options.overlapped_trace_build());
target.write_usize(options.max_num_continuations());
target.write_usize(options.max_stack_depth());
target.write_usize(options.max_memory_elements());
}
fn read_execution_options<R: ByteReader>(
source: &mut R,
) -> Result<ExecutionOptions, DeserializationError> {
let max_cycles = source.read_u32()?;
let expected_cycles = source.read_u32()?;
let core_trace_fragment_size = source.read_usize()?;
let max_advice_size_bytes = source.read_usize()?;
let max_hash_len_bytes = source.read_usize()?;
let overlapped_trace_build = source.read_bool()?;
let max_num_continuations = source.read_usize()?;
let max_stack_depth = source.read_usize()?;
let max_memory_elements = source.read_usize()?;
ExecutionOptions::new(Some(max_cycles), expected_cycles, core_trace_fragment_size)
.map_err(|err| {
DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
})
.and_then(|options| {
options
.with_max_advice_size_bytes(max_advice_size_bytes)
.with_max_hash_len_bytes(max_hash_len_bytes)
.with_overlapped_trace_build(overlapped_trace_build)
.with_max_num_continuations(max_num_continuations)
.with_max_memory_elements(max_memory_elements)
.with_max_stack_depth(max_stack_depth)
.map_err(|err| {
DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
})
})
}
#[derive(Debug, thiserror::Error)]
pub enum ReplaySnapshotError {
#[error("failed to read replay snapshot file: {0}")]
Io(std::io::Error),
#[error("failed to deserialize replay snapshot: {0}")]
Deserialization(DeserializationError),
}
#[cfg(test)]
mod tests {
use miden_assembly::{Assembler, DefaultSourceManager};
use miden_core::{Felt, Word, crypto::merkle::InnerNodeInfo};
use super::*;
fn word(values: [u32; 4]) -> Word {
Word::from(values.map(Felt::from))
}
#[test]
fn replay_snapshot_round_trips() {
let source_manager = Arc::new(DefaultSourceManager::default());
let program = Assembler::new(source_manager)
.assemble_program("program", "begin push.1 push.2 add drop end")
.map(Arc::<Package>::from)
.expect("failed to assemble test program");
let forest = LoadedMastForest::with_package_debug_info(
program.mast_forest().clone(),
program.debug_info(),
);
let event_log = vec![
vec![AdviceMutation::extend_advice_stack(
[Felt::from(7u32), Felt::from(8u32)].into_iter().collect(),
)],
vec![],
vec![AdviceMutation::extend_merkle_store([InnerNodeInfo {
value: word([1, 2, 3, 4]),
left: word([5, 6, 7, 8]),
right: word([9, 10, 11, 12]),
}])],
];
let snapshot = ReplaySnapshot {
package: program.clone(),
stack_inputs: StackInputs::new(&[Felt::from(42u32), Felt::from(43u32)]).unwrap(),
advice_inputs: AdviceInputs::default()
.with_stack([Felt::from(99u32)].into_iter().collect()),
options: ExecutionOptions::new(Some(100_000), 32, 1024)
.unwrap()
.with_max_advice_size_bytes(256)
.with_max_hash_len_bytes(512)
.with_overlapped_trace_build(false)
.with_max_num_continuations(128)
.with_max_memory_elements(1024)
.with_max_stack_depth(128)
.unwrap(),
mast_forests: vec![forest],
event_log,
};
let restored = ReplaySnapshot::read_from_bytes(&snapshot.to_bytes())
.expect("snapshot failed to deserialize");
assert_eq!(restored.package.commitment(), snapshot.package.commitment());
assert_eq!(restored.stack_inputs, snapshot.stack_inputs);
assert_eq!(restored.advice_inputs, snapshot.advice_inputs);
assert_eq!(restored.options, snapshot.options);
assert_eq!(restored.mast_forests.len(), 1);
assert_eq!(restored.event_log.len(), 3);
assert_eq!(restored.event_log[1].len(), 0, "empty event batch must survive");
match restored.event_log[0].as_slice() {
[AdviceMutation::ExtendStack { stack }] => {
assert_eq!(
stack.iter().copied().collect::<Vec<_>>(),
[Felt::from(7u32), Felt::from(8u32)],
);
}
_ => panic!("unexpected first event batch"),
}
match restored.event_log[2].as_slice() {
[AdviceMutation::ExtendMerkleStore { inner_nodes }] => {
assert_eq!(inner_nodes.len(), 1);
assert_eq!(inner_nodes[0].value, word([1, 2, 3, 4]));
assert_eq!(inner_nodes[0].right, word([9, 10, 11, 12]));
}
_ => panic!("unexpected merkle-store event batch"),
}
}
#[test]
fn replay_snapshot_rejects_bad_magic() {
let err = ReplaySnapshot::read_from_bytes(b"not a snapshot at all really");
assert!(err.is_err(), "expected deserialization to fail on bad magic");
}
#[test]
fn replay_snapshot_rejects_previous_version() {
let mut bytes = SNAPSHOT_MAGIC.to_vec();
bytes.push(SNAPSHOT_VERSION - 1);
let Err(err) = ReplaySnapshot::read_from_bytes(&bytes) else {
panic!("expected deserialization to reject a snapshot from a previous version");
};
assert!(err.to_string().contains("unsupported replay snapshot version"));
}
}