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