Skip to main content

miden_debug_engine/exec/
snapshot.rs

1//! A self-contained snapshot of a recorded execution, sufficient to replay it in the debugger
2//! without the original host.
3//!
4//! A live execution (e.g. a transaction driven through the DAP executor) resolves two kinds of
5//! host interaction that a bare debugger host cannot reproduce on its own: the advice mutations
6//! returned by event handlers, and the MAST forests resolved for `call`/`dyncall` targets (account
7//! code, note scripts, etc.). A [ReplaySnapshot] captures both, alongside the program and its
8//! inputs, so the same execution can be re-run later by feeding the recorded event log into an
9//! event-replay debugger host (see `Executor::into_debug_with_replay` and
10//! `State::new_for_transaction`).
11
12use alloc::{
13    string::{String, ToString},
14    sync::Arc,
15    vec::Vec,
16};
17
18use miden_core::{
19    mast::MastForest,
20    program::StackInputs,
21    serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
22};
23use miden_debug_types::Uri;
24use miden_mast_package::{Package, debug_info::PackageDebugInfo};
25use miden_processor::{
26    ExecutionOptions, LoadedMastForest,
27    advice::{AdviceInputs, AdviceMutation},
28};
29use miden_utils_sync::RwLock;
30
31use super::advice::{read_event_log, write_event_log};
32
33/// A shared, cloneable log of the MAST forests a host resolved during execution.
34///
35/// The debugger's event-replay host serves recorded advice mutations for `on_event`, but it still
36/// has to resolve the code for `call`/`dyncall` targets itself. Recording the forests the live
37/// host returned — deduplicated, since the same forest is resolved for many nodes — lets the
38/// replay host load exactly that set and reach the same targets.
39#[derive(Clone, Default)]
40pub struct MastForestRecorder {
41    forests: Arc<RwLock<Vec<LoadedMastForest>>>,
42}
43
44impl MastForestRecorder {
45    /// Create a new, empty recorder.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Returns a copy of the recorded forests.
51    pub fn snapshot(&self) -> Vec<LoadedMastForest> {
52        self.forests.read().clone()
53    }
54
55    /// Record a forest resolved by the host, ignoring forests already recorded this run.
56    #[cfg(feature = "dap")]
57    pub(crate) fn record(&self, forest: LoadedMastForest) {
58        let mut guard = self.forests.write();
59        if !guard
60            .iter()
61            .any(|existing| Arc::ptr_eq(existing.mast_forest(), forest.mast_forest()))
62        {
63            guard.push(forest);
64        }
65    }
66
67    /// Discard everything recorded so far, e.g. when execution restarts from the beginning.
68    #[cfg(feature = "dap")]
69    pub(crate) fn clear(&self) {
70        self.forests.write().clear();
71    }
72}
73
74/// Successful replay snapshot write metadata.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct ReplaySnapshotWrite {
77    pub path: Uri,
78    pub event_count: usize,
79    pub forest_count: usize,
80}
81
82/// Error metadata for a failed replay snapshot write.
83#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
84#[error("failed to write replay snapshot to {}: {}", path, message)]
85pub struct ReplaySnapshotWriteError {
86    pub path: Uri,
87    pub message: String,
88}
89
90/// Shared status handle for a configured replay snapshot write.
91#[derive(Clone, Debug, Default)]
92pub struct ReplaySnapshotRecorder {
93    status: Arc<RwLock<Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>>>>,
94}
95
96impl ReplaySnapshotRecorder {
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Returns the last snapshot write status, leaving the handle empty.
102    pub fn take(&self) -> Option<Result<ReplaySnapshotWrite, ReplaySnapshotWriteError>> {
103        self.status.write().take()
104    }
105
106    #[cfg(feature = "dap")]
107    pub(crate) fn record_success(&self, write: ReplaySnapshotWrite) {
108        *self.status.write() = Some(Ok(write));
109    }
110
111    #[cfg(feature = "dap")]
112    pub(crate) fn record_error(&self, path: Uri, err: impl ToString) {
113        *self.status.write() = Some(Err(ReplaySnapshotWriteError {
114            path,
115            message: err.to_string(),
116        }));
117    }
118}
119
120/// Magic bytes identifying a replay snapshot file, followed by a format version. Bumping the
121/// version invalidates older snapshots, whose serialized shape may differ.
122const SNAPSHOT_MAGIC: [u8; 6] = *b"MDNSNP";
123const SNAPSHOT_VERSION: u8 = 3;
124
125/// Everything needed to replay a recorded execution in the debugger.
126pub struct ReplaySnapshot {
127    /// The program that was executed (for a transaction, the transaction kernel).
128    pub package: Arc<Package>,
129    /// The operand stack inputs the program started with.
130    pub stack_inputs: StackInputs,
131    /// The advice inputs the program started with.
132    pub advice_inputs: AdviceInputs,
133    /// The VM execution options used by the recorded run.
134    pub options: ExecutionOptions,
135    /// The MAST forests resolved by the host during execution (account code, note scripts, ...),
136    /// which the replay host must be able to resolve for the same `call`/`dyncall` targets.
137    pub mast_forests: Vec<LoadedMastForest>,
138    /// The advice mutations produced by event handlers, one entry per `on_event` invocation, in
139    /// execution order — the event replay queue.
140    pub event_log: Vec<Vec<AdviceMutation>>,
141}
142
143impl ReplaySnapshot {
144    /// Serialize the snapshot to `path`.
145    #[cfg(feature = "std")]
146    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
147        std::fs::write(path, self.to_bytes())
148    }
149
150    /// Read and deserialize a snapshot from `path`.
151    #[cfg(feature = "std")]
152    pub fn read_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ReplaySnapshotError> {
153        let bytes = std::fs::read(path).map_err(ReplaySnapshotError::Io)?;
154        Self::read_from_bytes(&bytes).map_err(ReplaySnapshotError::Deserialization)
155    }
156
157    /// Serialize the snapshot to a byte vector.
158    pub fn to_bytes(&self) -> Vec<u8> {
159        let mut bytes = Vec::new();
160        self.write_into(&mut bytes);
161        bytes
162    }
163
164    /// Deserialize a snapshot from bytes.
165    pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
166        let mut reader = miden_core::serde::SliceReader::new(bytes);
167        Self::read_from(&mut reader)
168    }
169}
170
171impl Serializable for ReplaySnapshot {
172    fn write_into<W: ByteWriter>(&self, target: &mut W) {
173        target.write_bytes(&SNAPSHOT_MAGIC);
174        target.write_u8(SNAPSHOT_VERSION);
175        self.package.write_into(target);
176        self.stack_inputs.write_into(target);
177        self.advice_inputs.write_into(target);
178        write_execution_options(&self.options, target);
179        target.write_usize(self.mast_forests.len());
180        for forest in &self.mast_forests {
181            forest.mast_forest().as_ref().write_into(target);
182            match forest.package_debug_info().ok().flatten() {
183                Some(debug_info) => {
184                    target.write_bool(true);
185                    debug_info.as_ref().write_into(target);
186                }
187                None => {
188                    target.write_bool(false);
189                }
190            }
191        }
192        write_event_log(&self.event_log, target);
193    }
194}
195
196impl Deserializable for ReplaySnapshot {
197    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
198        let magic: [u8; 6] = source.read_array()?;
199        if magic != SNAPSHOT_MAGIC {
200            return Err(DeserializationError::InvalidValue(
201                "not a Miden debugger replay snapshot (bad magic)".to_string(),
202            ));
203        }
204        let version = source.read_u8()?;
205        if version != SNAPSHOT_VERSION {
206            return Err(DeserializationError::InvalidValue(format!(
207                "unsupported replay snapshot version {version} (expected {SNAPSHOT_VERSION})"
208            )));
209        }
210        let package = Arc::new(Package::read_from_trusted(source)?);
211        let stack_inputs = StackInputs::read_from(source)?;
212        let advice_inputs = AdviceInputs::read_from(source)?;
213        let options = read_execution_options(source)?;
214        let forest_count = source.read_usize()?;
215        let mut mast_forests = Vec::with_capacity(forest_count);
216        for _ in 0..forest_count {
217            let mast_forest = Arc::new(MastForest::read_from(source)?);
218            mast_forests.push(if source.read_bool()? {
219                let debug_info = Some(PackageDebugInfo::read_from(source)?);
220                LoadedMastForest::with_package_debug_info(mast_forest, Ok(debug_info))
221            } else {
222                LoadedMastForest::new(mast_forest)
223            });
224        }
225        let event_log = read_event_log(source)?;
226        Ok(Self {
227            package,
228            stack_inputs,
229            advice_inputs,
230            options,
231            mast_forests,
232            event_log,
233        })
234    }
235}
236
237fn write_execution_options<W: ByteWriter>(options: &ExecutionOptions, target: &mut W) {
238    target.write_u32(options.max_cycles());
239    target.write_u32(options.expected_cycles());
240    target.write_usize(options.core_trace_fragment_size());
241    target.write_usize(options.max_advice_size_bytes());
242    target.write_usize(options.max_hash_len_bytes());
243    target.write_bool(options.overlapped_trace_build());
244    target.write_usize(options.max_num_continuations());
245    target.write_usize(options.max_stack_depth());
246    target.write_usize(options.max_memory_elements());
247}
248
249fn read_execution_options<R: ByteReader>(
250    source: &mut R,
251) -> Result<ExecutionOptions, DeserializationError> {
252    let max_cycles = source.read_u32()?;
253    let expected_cycles = source.read_u32()?;
254    let core_trace_fragment_size = source.read_usize()?;
255    let max_advice_size_bytes = source.read_usize()?;
256    let max_hash_len_bytes = source.read_usize()?;
257    let overlapped_trace_build = source.read_bool()?;
258    let max_num_continuations = source.read_usize()?;
259    let max_stack_depth = source.read_usize()?;
260    let max_memory_elements = source.read_usize()?;
261
262    ExecutionOptions::new(Some(max_cycles), expected_cycles, core_trace_fragment_size)
263        .map_err(|err| {
264            DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
265        })
266        .and_then(|options| {
267            options
268                .with_max_advice_size_bytes(max_advice_size_bytes)
269                .with_max_hash_len_bytes(max_hash_len_bytes)
270                .with_overlapped_trace_build(overlapped_trace_build)
271                .with_max_num_continuations(max_num_continuations)
272                .with_max_memory_elements(max_memory_elements)
273                .with_max_stack_depth(max_stack_depth)
274                .map_err(|err| {
275                    DeserializationError::InvalidValue(format!("invalid execution options: {err}"))
276                })
277        })
278}
279
280/// Error reading a [ReplaySnapshot] from a file.
281#[derive(Debug, thiserror::Error)]
282#[cfg(feature = "std")]
283pub enum ReplaySnapshotError {
284    #[error("failed to read replay snapshot file: {0}")]
285    Io(std::io::Error),
286    #[error("failed to deserialize replay snapshot: {0}")]
287    Deserialization(DeserializationError),
288}
289
290#[cfg(test)]
291mod tests;