Skip to main content

miden_core/mast/serialization/
mod.rs

1//! MAST forest serialization keeps one fixed structural layout for normal and hashless payloads.
2//!
3//! The main goal is to keep random access cheap in both modes. Node structure
4//! stays in one fixed-width section. Variable-size data lives in separate sections. Internal node
5//! digests also live in a separate section so hashless payloads can omit them without changing the
6//! structural layout.
7//!
8//! Wire flags describe serializer intent, not reader trust policy. Trusted [`MastForest`] reads
9//! reject hashless payloads. [`crate::mast::UntrustedMastForest`] accepts them and rebuilds
10//! non-external digests before use. If a non-hashless payload is sent down the untrusted path,
11//! validation recomputes those digests and requires them to match the serialized values.
12//! Budgeted untrusted reads always bound wire counts during layout scanning via
13//! [`ByteReader::max_alloc`]. Validation also gets a second check:
14//! - later hashless helper allocations are charged against a validation budget before the
15//!   corresponding `Vec` or CSR scaffolding is created
16//! - that budget is derived from the wire budget by a coarse multiplier; this is intentionally a
17//!   simple bound for common callers, not an exact peak-memory formula
18//!
19//! The main layers fit together like this:
20//!
21//! ```text
22//! wire bytes
23//!     |
24//!     +--> ForestLayout -----------> MastForestWireView ----+
25//!     |        absolute offsets         trusted cache view   |
26//!     |                                                     v
27//!     +--> UntrustedMastForest ----validate----> ResolvedSerializedForest ---> MastForest
28//!              bytes + parsed state                digest-backed view            trusted runtime
29//!
30//! MastForestView is the shared random-access API implemented by MastForestWireView and
31//! MastForest.
32//! ```
33//!
34//! The format is:
35//!
36//! (Metadata)
37//! - MAGIC (4 bytes) + FLAGS (1 byte) + VERSION (3 bytes)
38//!
39//! (Counts)
40//! - internal nodes count (`usize`)
41//! - external nodes count (`usize`)
42//!
43//! (Procedure roots section)
44//! - procedure roots (`Vec<u32>` as MastNodeId values)
45//!
46//! (Basic block data section)
47//! - basic block data (padded operations + batch metadata)
48//!
49//! (Node entries section)
50//! - fixed-width structural node entries (`Vec<MastNodeEntry>`)
51//! - `Block` entries store offsets into the basic-block section above
52//!
53//! (External digest section)
54//! - digests for `External` nodes only (`Vec<Word>`, ordered by node index)
55//! - lookup is dense-by-kind: the Nth external node uses slot N in this section
56//!
57//! (Node hash section - omitted if FLAGS bit 1 is set)
58//! - digests for all non-external nodes (`Vec<Word>`, ordered by node index)
59//! - lookup is also dense-by-kind: the Nth non-external node uses slot N in this section
60//!
61//! (Advice map section)
62//! - Advice map (`AdviceMap`)
63//!
64//! (No trailing debug section)
65//!
66//! Readers reject any trailing payload after the advice map. Package-owned debug sections are now
67//! the only supported debug serialization path.
68//!
69//! In hashless format, the internal node-hash section is omitted. External node digests still stay
70//! on the wire because they cannot be rebuilt from local structure. This keeps hashless focused on
71//! the untrusted-validation use case: trusted reads reject `HASHLESS`, and the untrusted path
72//! rebuilds the data it actually trusts before use.
73//!
74//! Readers recover per-node digest lookup by scanning node entries once and building a compact
75//! "slot by node index" table. This preserves random access without forcing all digests into the
76//! same contiguous array on the wire.
77//!
78//! Public entry points adopt these policies:
79//! - [`MastForest::read_from_bytes`]: trusted dense execution payload, no hashless support.
80//! - [`MastForestWireView::new`]: trusted wire-backed cache access; rejects hashless and legacy
81//!   debug-bearing payloads.
82//! - [`crate::mast::SparseMastForest::read_from_bytes`]: separate trusted sparse replay payloads
83//!   for serialized trace-generation inputs. Sparse payloads preserve the sparse node and digest
84//!   maps produced by tracing; they do not share the dense `MastForest` wire format and are not an
85//!   untrusted validation boundary.
86//! - [`crate::mast::UntrustedMastForest::read_from_bytes`] /
87//!   [`crate::mast::UntrustedMastForest::read_from_bytes_with_options`]: untrusted parsing plus
88//!   later validation before use.
89
90#[cfg(test)]
91use alloc::string::ToString;
92use alloc::{boxed::Box, format, vec::Vec};
93use core::mem::size_of;
94
95use miden_utils_sync::OnceLockCompat;
96
97use super::{MastForest, MastNode, MastNodeId};
98use crate::{
99    Word,
100    advice::AdviceMap,
101    mast::node::MastNodeExt,
102    serde::{
103        BudgetedReader, ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable,
104        SliceReader,
105    },
106};
107
108mod info;
109pub use info::{MastNodeEntry, MastNodeInfo};
110
111mod view;
112use view::WireAdviceMapView;
113pub use view::{AdviceMapView, AdviceValueView, MastForestView};
114
115mod layout;
116pub(super) use layout::ForestLayout;
117use layout::{OffsetTrackingReader, TrackingReader, WireFlags, read_header_and_scan_layout};
118
119mod sparse;
120
121mod resolved;
122use resolved::{ResolvedSerializedForest, basic_block_offset_for_node_index};
123
124mod basic_blocks;
125use basic_blocks::{BasicBlockDataBuilder, basic_block_data_len};
126
127#[cfg(test)]
128mod seed_gen;
129
130#[cfg(test)]
131mod tests;
132
133// TYPE ALIASES
134// ================================================================================================
135
136/// Specifies an offset into the `node_data` section of an encoded [`MastForest`].
137type NodeDataOffset = u32;
138
139/// Default multiplier for the untrusted validation allocation budget.
140///
141/// The budgeted byte reader limits wire-driven parsing. Hashless validation also needs transient
142/// per-node allocations for the slot table and rebuilt digest data.
143/// The generic untrusted path also retains a recorded copy of the consumed
144/// serialized payload for deferred validation.
145///
146/// This convenience multiplier is therefore a coarse "wire bytes plus worst-case helper
147/// headroom" bound:
148/// - `* 6` covers the helper-allocation model introduced with explicit validation budgeting
149/// - `+ 1 * bytes_len` covers the retained serialized copy recorded during untrusted reads
150///
151/// It is deliberately conservative and exists to make the default
152/// [`crate::mast::UntrustedMastForest::read_from_bytes`] path usable without forcing callers to
153/// size each helper allocation themselves. Callers with stricter limits should use
154/// [`crate::mast::UntrustedMastForest::read_from_bytes_with_options`] and choose an explicit wire
155/// budget; the validation helper budget is derived from it.
156const DEFAULT_UNTRUSTED_ALLOCATION_BUDGET_MULTIPLIER: usize = 7;
157
158/// Byte-read budget multiplier for trusted full deserialization from a byte slice.
159///
160/// The budget is intentionally finite to reject malicious length prefixes, but larger than the
161/// source length because collection deserialization uses conservative per-element size estimates.
162const TRUSTED_BYTE_READ_BUDGET_MULTIPLIER: usize = 64;
163
164// CONSTANTS
165// ================================================================================================
166
167/// Magic bytes for detecting that a file is binary-encoded MAST.
168///
169/// The header is `b"MAST"` + flags byte + version bytes.
170///
171/// This repurposes the old `b"MAST\0"` terminator as the flags byte.
172const MAGIC: &[u8; 4] = b"MAST";
173
174/// Flag indicating that the internal node-hash section is omitted from the wire payload.
175///
176/// External digests still remain serialized in their own section because they cannot be rebuilt
177/// from local structure.
178pub(super) const FLAG_HASHLESS: u8 = 0x02;
179
180/// Mask for reserved flag bits that must be zero.
181///
182/// Bit 0 and bits 2-7 are reserved for future use. If any are set, deserialization fails.
183const FLAGS_RESERVED_MASK: u8 = 0xfd;
184
185/// The format version.
186///
187/// If future modifications are made to this format, the version should be incremented by 1. A
188/// version of `[255, 255, 255]` is reserved for future extensions that require extending the
189/// version field itself, but should be considered invalid for now.
190///
191/// Version history:
192/// - [0, 0, 0]: Initial format.
193/// - [0, 0, 1]: Added batch metadata to basic blocks (operations serialized in padded form with
194///   indptr, padding, and group metadata for exact OpBatch reconstruction). Added asm-op metadata
195///   and debug-variable storage in CSR layout (eliminates per-node metadata sections and round-trip
196///   conversions). Header changed from `MAST\0` to `MAST` + flags byte.
197/// - [0, 0, 2]: AssemblyOps moved out of inline metadata into a dedicated DebugInfo section.
198///   Removed `should_break` field from AssemblyOp serialization (#2646). Removed `breakpoint`
199///   instruction (#2655).
200/// - [0, 0, 3]: Added HASHLESS flag (bit 1). Trusted deserialization rejects HASHLESS. Split
201///   fixed-width node entries from digest storage. External digests moved to a dedicated section.
202///   Hashless serialization omits the general node-hash section entirely. Removed the unused
203///   metadata-count field from the wire header. Before any public release on this branch, the same
204///   unreleased wire version also grew explicit internal/external node counts in the header.
205/// - [0, 0, 4]: Removed the legacy inline metadata wire slots entirely. All assembly op metadata
206///   and debug variable metadata are now stored in the DebugInfo section as separate indexed
207///   records. MAST nodes are metadata-free identifiers. Before any public release on this branch,
208///   the same unreleased wire version also reserved bit 0 and stopped using it as a forest-level
209///   debug-presence flag.
210///
211/// Legacy wire versions (pre-#3192 decorator terminology):
212///   [0,0,1] stored metadata as serialized decorator variants in CSR per-node slots.
213///   [0,0,2] removed AssemblyOp from the decorator enum and stored them separately in DebugInfo.
214///   [0,0,3] removed the unused decorator-count wire field.
215///   [0,0,4] eliminated the decorator wire slots entirely.
216const VERSION: [u8; 3] = [0, 0, 4];
217
218// MAST FOREST SERIALIZATION/DESERIALIZATION
219// ================================================================================================
220
221impl Serializable for MastForest {
222    fn write_into<W: ByteWriter>(&self, target: &mut W) {
223        self.write_into_with_options(target, false);
224    }
225}
226
227impl MastForest {
228    /// Internal serialization with options.
229    ///
230    /// Current writers encode normal execution payloads or hashless validation payloads.
231    /// Both forms use the finalized dense node order already stored in the `MastForest`; writers
232    /// validate that order but do not sort nodes while writing.
233    fn write_into_with_options<W: ByteWriter>(&self, target: &mut W, hashless: bool) {
234        self.validate_dense_node_order()
235            .expect("dense MAST forest must be in final dense order before serialization");
236
237        let mut basic_block_data_builder = BasicBlockDataBuilder::new();
238
239        // magic & flags
240        target.write_bytes(MAGIC);
241        let flags = if hashless { FLAG_HASHLESS } else { 0 };
242        target.write_u8(flags);
243
244        // version
245        target.write_bytes(&VERSION);
246
247        // header counts
248        let node_count = self.nodes.len();
249        let external_node_count = self.nodes.iter().take_while(|node| node.is_external()).count();
250        let internal_node_count = node_count - external_node_count;
251        target.write_usize(internal_node_count);
252        target.write_usize(external_node_count);
253
254        // roots
255        let roots: Vec<u32> = self.roots.iter().copied().map(u32::from).collect();
256        roots.write_into(target);
257
258        let mut mast_node_entries = Vec::with_capacity(self.nodes.len());
259        let mut external_digests = Vec::with_capacity(external_node_count);
260        let mut node_hashes = Vec::new();
261
262        for mast_node in self.nodes.iter() {
263            let ops_offset = if let MastNode::Block(basic_block) = mast_node {
264                basic_block_data_builder.encode_basic_block(basic_block)
265            } else {
266                0
267            };
268
269            mast_node_entries.push(MastNodeEntry::new(mast_node, ops_offset));
270            if mast_node.is_external() {
271                external_digests.push(mast_node.digest());
272            } else if !hashless {
273                node_hashes.push(mast_node.digest());
274            }
275        }
276
277        let basic_block_data = basic_block_data_builder.finalize();
278        basic_block_data.write_into(target);
279
280        for mast_node_entry in mast_node_entries {
281            mast_node_entry.write_into(target);
282        }
283
284        for digest in external_digests {
285            digest.write_into(target);
286        }
287
288        if !hashless {
289            for digest in node_hashes {
290                digest.write_into(target);
291            }
292        }
293
294        self.advice_map.write_into(target);
295    }
296}
297
298pub(super) fn write_hashless_into<W: ByteWriter>(forest: &MastForest, target: &mut W) {
299    forest.write_into_with_options(target, true);
300}
301
302/// Trusted read backing mode for read-only MAST forest access.
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum MastForestReadMode {
305    /// Deserialize the full trusted cache into a materialized [`MastForest`].
306    Materialized,
307    /// Borrow complete trusted cache bytes and serve read-only data by random access.
308    WireBacked,
309}
310
311/// Read-only trusted MAST forest handle.
312#[derive(Debug)]
313pub enum MastForestReadView<'a> {
314    /// A fully materialized forest.
315    Materialized(MastForest),
316    /// A trusted wire-backed cache view.
317    WireBacked(Box<MastForestWireView<'a>>),
318}
319
320/// A trusted wire-backed view over serialized MAST forest bytes.
321///
322/// This view accepts complete payloads with hashes. It validates the header and the fixed-width
323/// structural sections needed for random access, but it does not fully materialize the forest.
324/// Hashless payloads are rejected because trusted cache bytes must be complete. Trailing payloads
325/// are rejected because debug metadata now belongs to package-owned debug sections.
326///
327/// Use this when callers need random access to roots or node metadata without deserializing the
328/// full forest. For strict trusted deserialization, use
329/// [`crate::mast::MastForest::read_from_bytes`].
330///
331/// # Examples
332///
333/// ```
334/// use miden_core::{
335///     mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastForestWireView},
336///     operations::Operation,
337///     serde::Serializable,
338/// };
339///
340/// let mut builder = DenseMastForestBuilder::new();
341/// let block_id = builder.push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])).unwrap();
342/// builder.mark_root(block_id);
343/// let forest = builder.build().unwrap();
344///
345/// let mut bytes = Vec::new();
346/// forest.write_into(&mut bytes);
347///
348/// let view = MastForestWireView::new(&bytes).unwrap();
349/// assert_eq!(view.node_count(), forest.nodes().len());
350/// assert!(view.node_info_at(0).is_ok());
351/// ```
352#[derive(Debug)]
353pub struct MastForestWireView<'a> {
354    bytes: &'a [u8],
355    layout: ForestLayout,
356    advice_map: WireAdviceMapView<'a>,
357    resolved: OnceLockCompat<Result<ResolvedSerializedForest<'a>, DeserializationError>>,
358}
359
360impl<'a> MastForestWireView<'a> {
361    /// Creates a new view from serialized bytes.
362    ///
363    /// The input must include all node hashes. Structural parsing is
364    /// delegated to the same single-pass scanner used by reader-based deserialization paths.
365    ///
366    /// This constructor validates the header and sections needed for node/roots/random-access
367    /// metadata, indexes `AdviceMap` keys for on-demand lookup, and rejects trailing payloads.
368    ///
369    /// Treat this as a trusted cache API, not as an untrusted-validation entry point. It is
370    /// appropriate for local tools that need random access over serialized structure, but callers
371    /// handling adversarial bytes should use [`crate::mast::UntrustedMastForest`] instead.
372    ///
373    /// In particular, this constructor does **not** protect callers from untrusted-input concerns
374    /// that are enforced by [`crate::mast::UntrustedMastForest::validate`]. It does not:
375    /// - verify that serialized non-external digests match the structure they describe
376    /// - check topological ordering / forward-reference constraints
377    /// - validate basic-block batch invariants
378    /// - materialize or expose package-owned debug sections
379    ///
380    /// For strict materialized validation, use
381    /// [`crate::mast::MastForest::read_from_bytes`].
382    ///
383    /// Digest lookup follows the wire layout:
384    /// - Non-external node digests are read from the internal-hash section.
385    /// - External node digests are read from the external-digest section.
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// use miden_core::{
391    ///     mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastForestWireView},
392    ///     operations::Operation,
393    ///     serde::Serializable,
394    /// };
395    ///
396    /// let mut builder = DenseMastForestBuilder::new();
397    /// let block_id = builder.push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])).unwrap();
398    /// builder.mark_root(block_id);
399    /// let forest = builder.build().unwrap();
400    ///
401    /// let mut bytes = Vec::new();
402    /// forest.write_into(&mut bytes);
403    ///
404    /// let view = MastForestWireView::new(&bytes).unwrap();
405    /// assert_eq!(view.node_count(), 1);
406    /// ```
407    pub fn new(bytes: &'a [u8]) -> Result<Self, DeserializationError> {
408        let mut reader = SliceReader::new(bytes);
409        let mut scanner = TrackingReader::new(&mut reader);
410        let (_flags, layout) = read_header_and_scan_layout(&mut scanner, false)?;
411        let advice_map = WireAdviceMapView::new(bytes, layout.advice_map_offset())?;
412        check_no_trailing_payload(bytes, advice_map.end_offset())?;
413        ResolvedSerializedForest::new(bytes, layout)?.validate_dense_node_order()?;
414
415        Ok(Self {
416            bytes,
417            layout,
418            advice_map,
419            resolved: OnceLockCompat::new(),
420        })
421    }
422
423    /// Returns the number of nodes in the serialized forest.
424    pub fn node_count(&self) -> usize {
425        self.layout.node_count
426    }
427
428    /// Returns the number of procedure roots in the serialized forest.
429    pub fn procedure_root_count(&self) -> usize {
430        self.layout.roots_count
431    }
432
433    /// Returns the procedure root id at the specified index.
434    ///
435    /// Returns an error if `index >= self.procedure_root_count()`.
436    pub fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
437        self.layout.read_procedure_root_at(self.bytes, index)
438    }
439
440    /// Returns the `MastNodeInfo` at the specified index.
441    ///
442    /// Returns an error if `index >= self.node_count()`.
443    ///
444    /// # Examples
445    ///
446    /// ```
447    /// use miden_core::{
448    ///     mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastForestWireView},
449    ///     operations::Operation,
450    ///     serde::Serializable,
451    /// };
452    ///
453    /// let mut builder = DenseMastForestBuilder::new();
454    /// let block_id = builder.push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])).unwrap();
455    /// builder.mark_root(block_id);
456    /// let forest = builder.build().unwrap();
457    ///
458    /// let mut bytes = Vec::new();
459    /// forest.write_into(&mut bytes);
460    ///
461    /// let view = MastForestWireView::new(&bytes).unwrap();
462    /// assert!(view.node_info_at(0).is_ok());
463    /// ```
464    pub fn node_info_at(&self, index: usize) -> Result<MastNodeInfo, DeserializationError> {
465        Ok(MastNodeInfo::from_entry(
466            self.node_entry_at(index)?,
467            self.node_digest_at(index)?,
468        ))
469    }
470
471    /// Returns the fixed-width structural node entry at the specified index.
472    ///
473    /// Returns an error if `index >= self.node_count()`.
474    pub fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
475        self.layout.read_node_entry_at(self.bytes, index)
476    }
477
478    /// Returns the digest for the node at the specified index.
479    ///
480    /// Returns an error if `index >= self.node_count()`.
481    pub fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
482        self.resolved()?.node_digest_at(index)
483    }
484
485    /// Returns a read-only view over the serialized forest advice map.
486    pub fn advice_map(&self) -> AdviceMapView<'_> {
487        AdviceMapView::wire(&self.advice_map)
488    }
489
490    fn resolved(&self) -> Result<&ResolvedSerializedForest<'a>, DeserializationError> {
491        self.resolved
492            .get_or_init(|| ResolvedSerializedForest::new(self.bytes, self.layout))
493            .as_ref()
494            .map_err(Clone::clone)
495    }
496}
497
498fn check_no_trailing_payload(
499    bytes: &[u8],
500    debug_info_offset: usize,
501) -> Result<(), DeserializationError> {
502    let payload = bytes.get(debug_info_offset..).ok_or(DeserializationError::UnexpectedEOF)?;
503    if payload.is_empty() {
504        return Ok(());
505    }
506    Err(extra_bytes_after_mast_forest_payload_error())
507}
508
509fn extra_bytes_after_mast_forest_payload_error() -> DeserializationError {
510    DeserializationError::InvalidValue("extra bytes after MastForest payload".into())
511}
512
513impl MastForest {
514    /// Reads trusted MAST forest bytes using the requested backing mode.
515    ///
516    /// [`MastForestReadMode::Materialized`] is equivalent to [`Self::read_from_bytes`].
517    /// [`MastForestReadMode::WireBacked`] returns a trusted random-access cache view and rejects
518    /// hashless and trailing payloads because trusted cache bytes must be complete execution
519    /// payloads.
520    pub fn read_view_from_bytes(
521        bytes: &[u8],
522        mode: MastForestReadMode,
523    ) -> Result<MastForestReadView<'_>, DeserializationError> {
524        match mode {
525            MastForestReadMode::Materialized => {
526                Self::read_from_bytes(bytes).map(MastForestReadView::Materialized)
527            },
528            MastForestReadMode::WireBacked => {
529                MastForestWireView::new(bytes).map(Box::new).map(MastForestReadView::WireBacked)
530            },
531        }
532    }
533}
534
535impl MastForestView for MastForestWireView<'_> {
536    fn node_count(&self) -> usize {
537        MastForestWireView::node_count(self)
538    }
539
540    fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
541        MastForestWireView::node_entry_at(self, index)
542    }
543
544    fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
545        MastForestWireView::node_digest_at(self, index)
546    }
547
548    fn procedure_root_count(&self) -> usize {
549        MastForestWireView::procedure_root_count(self)
550    }
551
552    fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
553        MastForestWireView::procedure_root_at(self, index)
554    }
555
556    fn advice_map(&self) -> AdviceMapView<'_> {
557        MastForestWireView::advice_map(self)
558    }
559}
560
561impl MastForestView for MastForestReadView<'_> {
562    fn node_count(&self) -> usize {
563        match self {
564            MastForestReadView::Materialized(forest) => MastForestView::node_count(forest),
565            MastForestReadView::WireBacked(view) => view.node_count(),
566        }
567    }
568
569    fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
570        match self {
571            MastForestReadView::Materialized(forest) => {
572                MastForestView::node_entry_at(forest, index)
573            },
574            MastForestReadView::WireBacked(view) => view.node_entry_at(index),
575        }
576    }
577
578    fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
579        match self {
580            MastForestReadView::Materialized(forest) => {
581                MastForestView::node_digest_at(forest, index)
582            },
583            MastForestReadView::WireBacked(view) => view.node_digest_at(index),
584        }
585    }
586
587    fn procedure_root_count(&self) -> usize {
588        match self {
589            MastForestReadView::Materialized(forest) => {
590                MastForestView::procedure_root_count(forest)
591            },
592            MastForestReadView::WireBacked(view) => view.procedure_root_count(),
593        }
594    }
595
596    fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
597        match self {
598            MastForestReadView::Materialized(forest) => {
599                MastForestView::procedure_root_at(forest, index)
600            },
601            MastForestReadView::WireBacked(view) => view.procedure_root_at(index),
602        }
603    }
604
605    fn advice_map(&self) -> AdviceMapView<'_> {
606        match self {
607            MastForestReadView::Materialized(forest) => MastForestView::advice_map(forest),
608            MastForestReadView::WireBacked(view) => view.advice_map(),
609        }
610    }
611}
612
613impl MastForestView for MastForest {
614    fn node_count(&self) -> usize {
615        self.nodes.len()
616    }
617
618    fn node_entry_at(&self, index: usize) -> Result<MastNodeEntry, DeserializationError> {
619        let node = self.nodes.as_slice().get(index).ok_or_else(|| {
620            DeserializationError::InvalidValue(format!("node index {index} out of bounds"))
621        })?;
622        let ops_offset = if matches!(node, MastNode::Block(_)) {
623            basic_block_offset_for_node_index(self.nodes.as_slice(), index)?
624        } else {
625            0
626        };
627
628        Ok(MastNodeEntry::new(node, ops_offset))
629    }
630
631    fn node_digest_at(&self, index: usize) -> Result<Word, DeserializationError> {
632        self.nodes.as_slice().get(index).map(MastNode::digest).ok_or_else(|| {
633            DeserializationError::InvalidValue(format!("node index {index} out of bounds"))
634        })
635    }
636
637    fn procedure_root_count(&self) -> usize {
638        self.roots.len()
639    }
640
641    fn procedure_root_at(&self, index: usize) -> Result<MastNodeId, DeserializationError> {
642        self.roots.get(index).copied().ok_or_else(|| {
643            DeserializationError::InvalidValue(format!(
644                "root index {} out of bounds for {} roots",
645                index,
646                self.roots.len()
647            ))
648        })
649    }
650
651    fn advice_map(&self) -> AdviceMapView<'_> {
652        AdviceMapView::materialized(&self.advice_map)
653    }
654}
655
656// TEST HELPERS
657// ================================================================================================
658
659#[cfg(test)]
660impl MastForestWireView<'_> {
661    fn debug_info_offset(&self) -> usize {
662        self.advice_map.end_offset()
663    }
664
665    fn node_entry_offset(&self) -> usize {
666        self.layout.node_entry_offset()
667    }
668
669    fn external_digest_offset(&self) -> usize {
670        self.layout.external_digest_offset()
671    }
672
673    fn node_hash_offset(&self) -> Option<usize> {
674        self.layout.node_hash_offset()
675    }
676
677    fn digest_slot_at(&self, index: usize) -> usize {
678        self.resolved()
679            .expect("digest slots should be readable for a valid serialized view")
680            .digest_slot_at(index)
681    }
682}
683
684#[cfg(test)]
685fn read_u8_at(bytes: &[u8], offset: &mut usize) -> Result<u8, DeserializationError> {
686    read_slice_at(bytes, offset, 1).map(|slice| slice[0])
687}
688
689#[cfg(test)]
690fn read_array_at<const N: usize>(
691    bytes: &[u8],
692    offset: &mut usize,
693) -> Result<[u8; N], DeserializationError> {
694    let slice = read_slice_at(bytes, offset, N)?;
695    let mut result = [0u8; N];
696    result.copy_from_slice(slice);
697    Ok(result)
698}
699
700#[cfg(test)]
701fn read_slice_at<'a>(
702    bytes: &'a [u8],
703    offset: &mut usize,
704    len: usize,
705) -> Result<&'a [u8], DeserializationError> {
706    let end = offset
707        .checked_add(len)
708        .ok_or_else(|| DeserializationError::InvalidValue("offset overflow".to_string()))?;
709    if end > bytes.len() {
710        return Err(DeserializationError::UnexpectedEOF);
711    }
712    let slice = &bytes[*offset..end];
713    *offset = end;
714    Ok(slice)
715}
716
717// NOTE: Mirrors ByteReader::read_usize (vint64) decoding to preserve wire compatibility.
718#[cfg(test)]
719fn read_usize_at(bytes: &[u8], offset: &mut usize) -> Result<usize, DeserializationError> {
720    if *offset >= bytes.len() {
721        return Err(DeserializationError::UnexpectedEOF);
722    }
723    let first_byte = bytes[*offset];
724    let length = first_byte.trailing_zeros() as usize + 1;
725
726    let result = if length == 9 {
727        let _marker = read_u8_at(bytes, offset)?;
728        let value = read_array_at::<8>(bytes, offset)?;
729        u64::from_le_bytes(value)
730    } else {
731        let mut encoded = [0u8; 8];
732        let value = read_slice_at(bytes, offset, length)?;
733        encoded[..length].copy_from_slice(value);
734        u64::from_le_bytes(encoded) >> length
735    };
736
737    if result > usize::MAX as u64 {
738        return Err(DeserializationError::InvalidValue(format!(
739            "Encoded value must be less than {}, but {} was provided",
740            usize::MAX,
741            result
742        )));
743    }
744
745    Ok(result as usize)
746}
747
748impl Deserializable for MastForest {
749    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
750        let (_flags, forest) = decode_from_reader(source, false)?;
751        forest.into_materialized()
752    }
753
754    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
755        let budget = bytes.len().saturating_mul(TRUSTED_BYTE_READ_BUDGET_MULTIPLIER);
756        let mut reader = BudgetedReader::new(SliceReader::new(bytes), budget);
757        let forest = Self::read_from(&mut reader)?;
758        if reader.has_more_bytes() {
759            return Err(extra_bytes_after_mast_forest_payload_error());
760        }
761        Ok(forest)
762    }
763}
764
765impl super::UntrustedMastForest {
766    pub(super) fn into_materialized(self) -> Result<MastForest, DeserializationError> {
767        let resolved = if let Some(allocation_budget) = self.remaining_allocation_budget {
768            ResolvedSerializedForest::new_with_allocation_budget(
769                &self.bytes,
770                self.layout,
771                allocation_budget,
772            )?
773        } else {
774            ResolvedSerializedForest::new(&self.bytes, self.layout)?
775        };
776
777        resolved.materialize(self.advice_map)
778    }
779}
780
781pub(super) fn read_untrusted_with_flags<R: ByteReader>(
782    source: &mut R,
783) -> Result<(super::UntrustedMastForest, u8), DeserializationError> {
784    let (flags, forest) = decode_from_reader(source, true)?;
785    log_untrusted_overspecification(flags);
786    Ok((forest, flags.bits()))
787}
788
789pub(super) fn read_untrusted_with_flags_and_allocation_budget<R: ByteReader>(
790    source: &mut R,
791    allocation_budget: usize,
792) -> Result<(super::UntrustedMastForest, u8), DeserializationError> {
793    let (flags, forest) = decode_from_reader_inner(source, true, Some(allocation_budget))?;
794    log_untrusted_overspecification(flags);
795    Ok((forest, flags.bits()))
796}
797
798fn log_untrusted_overspecification(flags: WireFlags) {
799    if !flags.is_hashless() {
800        log::error!(
801            "UntrustedMastForest expected HASHLESS input; supplied artifact includes wire node hashes, and validation will recompute them and require them to match"
802        );
803    }
804}
805
806fn decode_from_reader<R: ByteReader>(
807    source: &mut R,
808    allow_hashless: bool,
809) -> Result<(WireFlags, super::UntrustedMastForest), DeserializationError> {
810    decode_from_reader_inner(source, allow_hashless, None)
811}
812
813fn decode_from_reader_inner<R: ByteReader>(
814    source: &mut R,
815    allow_hashless: bool,
816    remaining_allocation_budget: Option<usize>,
817) -> Result<(WireFlags, super::UntrustedMastForest), DeserializationError> {
818    let mut recording = TrackingReader::new_recording(source);
819    let (flags, layout) = read_header_and_scan_layout(&mut recording, allow_hashless)?;
820    debug_assert_eq!(recording.offset(), layout.advice_map_offset());
821
822    let advice_map = AdviceMap::read_from(&mut recording)?;
823    Ok((
824        flags,
825        super::UntrustedMastForest {
826            bytes: recording.into_recorded(),
827            layout,
828            advice_map,
829            remaining_allocation_budget,
830        },
831    ))
832}
833
834pub(super) fn reserve_allocation<T>(
835    remaining_budget: &mut usize,
836    count: usize,
837    label: &str,
838) -> Result<(), DeserializationError> {
839    let bytes_needed = count
840        .checked_mul(size_of::<T>())
841        .ok_or_else(|| DeserializationError::InvalidValue(format!("{label} size overflow")))?;
842    if bytes_needed > *remaining_budget {
843        return Err(DeserializationError::InvalidValue(format!(
844            "{label} requires {bytes_needed} bytes, exceeding the remaining untrusted allocation budget of {} bytes",
845            *remaining_budget
846        )));
847    }
848
849    *remaining_budget -= bytes_needed;
850    Ok(())
851}
852
853pub(super) fn default_untrusted_allocation_budget(bytes_len: usize) -> usize {
854    bytes_len.saturating_mul(DEFAULT_UNTRUSTED_ALLOCATION_BUDGET_MULTIPLIER)
855}
856
857// UNTRUSTED DESERIALIZATION
858// ================================================================================================
859
860impl Deserializable for super::UntrustedMastForest {
861    /// Deserializes an [`super::UntrustedMastForest`] from a byte reader.
862    ///
863    /// Note: This method does not apply budgeting. For untrusted input, prefer using
864    /// [`read_from_bytes`](Self::read_from_bytes) which applies budgeted deserialization.
865    ///
866    /// After deserialization, callers should use [`super::UntrustedMastForest::validate()`]
867    /// to verify structural integrity and recompute all node hashes before using
868    /// the forest.
869    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
870        read_untrusted_with_flags(source).map(|(forest, _flags)| forest)
871    }
872
873    /// Deserializes an [`super::UntrustedMastForest`] from bytes using budgeted deserialization.
874    ///
875    /// This method uses the default untrusted wire/validation budget from
876    /// [`super::UntrustedMastForest::read_from_bytes`].
877    ///
878    /// After deserialization, callers should use [`super::UntrustedMastForest::validate()`]
879    /// to verify structural integrity and recompute all node hashes before using
880    /// the forest.
881    fn read_from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
882        super::UntrustedMastForest::read_from_bytes(bytes)
883    }
884}