Skip to main content

miden_core/mast/
mod.rs

1//! MAST forest: a collection of procedures represented as Merkle trees.
2//!
3//! # Deserializing from untrusted sources
4//!
5//! When loading a `MastForest` from bytes you don't fully trust (network, user upload, etc.),
6//! use [`UntrustedMastForest`] instead of calling `MastForest::read_from_bytes` directly:
7//!
8//! ```ignore
9//! use miden_core::mast::UntrustedMastForest;
10//!
11//! let forest = UntrustedMastForest::read_from_bytes(&bytes)?
12//!     .validate()?;
13//! ```
14//!
15//! [`UntrustedMastForest::read_from_bytes`] applies default parsing and validation budgets derived
16//! from the input size. Use [`UntrustedMastForest::read_from_bytes_with_options`] with
17//! [`UntrustedMastForestReadOptions`] to tune the wire byte budget. This limits allocations driven
18//! directly by wire counts while reading the payload. A separate validation helper budget is
19//! derived from it for later allocations needed to materialize and check hashless payloads.
20//!
21//! ```ignore
22//! use miden_core::mast::{UntrustedMastForest, UntrustedMastForestReadOptions};
23//!
24//! let options = UntrustedMastForestReadOptions::new()
25//!     .with_wire_byte_budget(bytes.len());
26//! let forest = UntrustedMastForest::read_from_bytes_with_options(&bytes, options)?
27//!     .validate()?;
28//! ```
29//!
30//! This recomputes all node hashes and checks structural invariants before returning a usable
31//! `MastForest`. Direct deserialization via `MastForest::read_from_bytes` trusts the serialized
32//! hashes and should only be used for data from trusted sources (e.g. compiled locally).
33//!
34//! In practice, the public entry points split into three policies:
35//! - [`MastForest::read_from_bytes`]: trusted full deserialization; rejects hashless payloads and
36//!   trusts serialized non-external digests.
37//! - [`MastForestWireView::new`]: trusted wire-backed cache access; scans only the layout needed
38//!   for random access and rejects hashless payloads.
39//! - [`UntrustedMastForest::read_from_bytes`] and
40//!   [`UntrustedMastForest::read_from_bytes_with_options`]: untrusted paths; parse with bounded
41//!   readers and require [`UntrustedMastForest::validate`] before use.
42
43#[cfg(test)]
44use alloc::collections::BTreeSet;
45use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};
46use core::{fmt, ops::Index};
47
48use dense_order::{
49    canonicalize_parts, validate_dense_node_order, validate_mast_forest_parts_bounds,
50};
51#[cfg(any(test, feature = "arbitrary"))]
52use proptest::prelude::*;
53
54mod dense_order;
55mod node;
56#[cfg(any(test, feature = "arbitrary"))]
57pub use node::arbitrary;
58pub(crate) use node::collect_immediate_placements;
59pub use node::{
60    BasicBlockNode, BasicBlockNodeBuilder, CallNode, CallNodeBuilder, DynNode, DynNodeBuilder,
61    ExternalNode, ExternalNodeBuilder, JoinNode, JoinNodeBuilder, LoopNode, LoopNodeBuilder,
62    MastForestContributor, MastNode, MastNodeBuilder, MastNodeContext, MastNodeExt, OP_BATCH_SIZE,
63    OP_GROUP_SIZE, OpBatch, SplitNode, SplitNodeBuilder,
64};
65
66use crate::{
67    Felt, Word,
68    advice::AdviceMap,
69    serde::{ByteWriter, Deserializable, DeserializationError, Serializable},
70    utils::{DenseIdMap, Idx, IndexVec, hash_string_to_word},
71};
72
73mod serialization;
74pub use serialization::{
75    AdviceMapView, AdviceValueView, MastForestReadMode, MastForestReadView, MastForestView,
76    MastForestWireView, MastNodeEntry, MastNodeInfo,
77};
78
79mod dense_builder;
80pub use dense_builder::DenseMastForestBuilder;
81
82mod untrusted;
83pub use untrusted::{UntrustedMastForest, UntrustedMastForestReadOptions};
84
85mod merger;
86pub(crate) use merger::MastForestMerger;
87pub use merger::MastForestRootMap;
88
89mod multi_forest_node_iterator;
90pub(crate) use multi_forest_node_iterator::*;
91
92mod node_builder_utils;
93pub use node_builder_utils::build_node_with_remapped_ids;
94
95mod sparse;
96pub use sparse::{MastForestId, SparseMastForest, SparseMastForestBuilder, VisitKind};
97
98#[cfg(test)]
99mod tests;
100
101// MAST FOREST
102// ================================================================================================
103
104/// Represents one or more procedures, represented as a collection of [`MastNode`]s.
105///
106/// A [`MastForest`] does not have an entrypoint, and hence is not executable. A
107/// [`crate::program::Program`] can be built from a [`MastForest`] to specify an entrypoint.
108///
109/// Finalized dense forests keep nodes in final dense order:
110/// - external nodes first, sorted by digest, with no duplicate external digests;
111/// - basic blocks next, in the input order seen by finalization;
112/// - internal nodes last, with every child before its parent and finalization input order as the
113///   tie-breaker.
114///
115/// Serialization expects this in-memory order and validates it before writing.
116///
117/// Normal construction goes through builders. Once finalized, a `MastForest` exposes no append API;
118/// code that changes dense nodes must rebuild and finalize a new forest so ordering, root, and
119/// commitment invariants stay in sync.
120#[derive(Clone, Debug, Default)]
121#[cfg_attr(
122    all(feature = "arbitrary", test),
123    miden_test_serialization_macros::serialization_test
124)]
125pub struct MastForest {
126    /// All of the nodes local to the trees comprising the MAST forest.
127    nodes: IndexVec<MastNodeId, MastNode>,
128
129    /// Roots of procedures defined within this MAST forest.
130    roots: Vec<MastNodeId>,
131
132    /// Advice map to be loaded into the VM prior to executing procedures from this MAST forest.
133    advice_map: AdviceMap,
134
135    /// Commitments to this MAST forest's roots, external dependencies, and advice map.
136    commitment: MastForestCommitment,
137}
138
139/// Commitment values derived from a MAST forest.
140///
141/// Commitment roles:
142/// - [`MastForest::interface_commitment`] identifies the public procedure roots only.
143/// - [`MastForest::dependency_commitment`] identifies the external dependency digests only.
144/// - [`MastForest::advice_commitment`] identifies the stored advice map only.
145/// - [`MastForest::commitment`] identifies the stored dense forest data: public roots, external
146///   dependencies, and advice. Direct forest-backed static libraries use this value as their source
147///   identity. Package-backed static libraries use the package commitment, which is derived from
148///   this forest commitment.
149#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
150struct MastForestCommitment {
151    /// Commitment to the forest's roots, external dependencies, and advice map.
152    commitment: Word,
153
154    /// Commitment to this MAST forest's public procedure roots.
155    interface_commitment: Word,
156
157    /// Commitment to this MAST forest's external dependencies.
158    dependency_commitment: Word,
159
160    /// Commitment to this MAST forest's advice map.
161    advice_commitment: Word,
162}
163
164/// Complete parts needed to construct a finalized [`MastForest`].
165pub(crate) struct MastForestParts {
166    pub nodes: IndexVec<MastNodeId, MastNode>,
167    pub roots: Vec<MastNodeId>,
168    pub advice_map: AdviceMap,
169}
170
171// ------------------------------------------------------------------------------------------------
172/// Constructors
173impl MastForest {
174    /// Creates a new empty [`MastForest`].
175    pub fn new() -> Self {
176        Self {
177            nodes: IndexVec::new(),
178            roots: Vec::new(),
179            advice_map: AdviceMap::default(),
180            commitment: empty_mast_forest_commitment(),
181        }
182    }
183
184    /// Builds a [`MastForest`] from raw parts and validates local structure.
185    ///
186    /// This is hidden because raw dense parts are not a stable public construction API.
187    #[doc(hidden)]
188    pub fn from_raw_parts(
189        nodes: IndexVec<MastNodeId, MastNode>,
190        roots: Vec<MastNodeId>,
191        advice_map: AdviceMap,
192    ) -> Result<Self, MastForestError> {
193        Self::from_parts(MastForestParts { nodes, roots, advice_map })
194    }
195
196    /// Builds a [`MastForest`] from raw parts and returns the node ID remapping applied during
197    /// canonicalization.
198    ///
199    /// This is hidden because raw dense parts and builder-local ID remapping are only used by
200    /// internal builders and tests.
201    #[doc(hidden)]
202    pub fn from_raw_parts_with_id_map(
203        nodes: IndexVec<MastNodeId, MastNode>,
204        roots: Vec<MastNodeId>,
205        advice_map: AdviceMap,
206    ) -> Result<(Self, DenseIdMap<MastNodeId, MastNodeId>), MastForestError> {
207        Self::from_parts_with_id_map(MastForestParts { nodes, roots, advice_map })
208    }
209
210    /// Builds a [`MastForest`] from completed parts.
211    pub(crate) fn from_parts(parts: MastForestParts) -> Result<Self, MastForestError> {
212        Self::from_parts_with_id_map(parts).map(|(forest, _remapping)| forest)
213    }
214
215    pub(crate) fn from_parts_with_id_map(
216        parts: MastForestParts,
217    ) -> Result<(Self, DenseIdMap<MastNodeId, MastNodeId>), MastForestError> {
218        validate_mast_forest_parts_bounds(&parts)?;
219        let (parts, id_remapping) = canonicalize_parts(parts)?;
220
221        let forest = Self {
222            commitment: compute_mast_forest_commitment(
223                &parts.nodes,
224                &parts.roots,
225                &parts.advice_map,
226            ),
227            nodes: parts.nodes,
228            roots: parts.roots,
229            advice_map: parts.advice_map,
230        };
231
232        forest.validate_dense_node_order()?;
233        forest.validate()?;
234        forest.validate_node_hashes()?;
235        Ok((forest, id_remapping))
236    }
237
238    pub(in crate::mast) fn from_trusted_deserialization_parts(
239        parts: MastForestParts,
240    ) -> Result<Self, MastForestError> {
241        validate_mast_forest_parts_bounds(&parts)?;
242        // Trusted dense serialization is expected to have checked this already. Re-check the
243        // cheap ordering invariant here as a precaution before accepting the finalized forest.
244        validate_dense_node_order(&parts.nodes)?;
245        Ok(Self {
246            commitment: compute_mast_forest_commitment(
247                &parts.nodes,
248                &parts.roots,
249                &parts.advice_map,
250            ),
251            nodes: parts.nodes,
252            roots: parts.roots,
253            advice_map: parts.advice_map,
254        })
255    }
256}
257
258// ------------------------------------------------------------------------------------------------
259/// Equality implementations
260impl PartialEq for MastForest {
261    fn eq(&self, other: &Self) -> bool {
262        self.nodes == other.nodes
263            && self.roots == other.roots
264            && self.advice_map == other.advice_map
265    }
266}
267
268impl Eq for MastForest {}
269
270// ------------------------------------------------------------------------------------------------
271/// State mutators
272impl MastForest {
273    /// The maximum number of nodes that can be stored in a single MAST forest.
274    const MAX_NODES: usize = (1 << 30) - 1;
275
276    /// Marks the given [`MastNodeId`] as being the root of a procedure.
277    ///
278    /// If the specified node is already marked as a root, this will have no effect.
279    ///
280    /// # Panics
281    /// - if `new_root_id`'s internal index is larger than the number of nodes in this forest (i.e.
282    ///   clearly doesn't belong to this MAST forest).
283    #[cfg(any(test, feature = "arbitrary"))]
284    pub fn make_root(&mut self, new_root_id: MastNodeId) {
285        assert!(new_root_id.to_usize() < self.nodes.len());
286
287        if !self.roots.contains(&new_root_id) {
288            self.roots.push(new_root_id);
289            self.commitment = self.compute_mast_forest_commitment();
290        }
291    }
292
293    /// Removes all nodes in the provided set from the MAST forest. The nodes MUST be orphaned (i.e.
294    /// have no parent). Otherwise, this parent's reference is considered "dangling" after the
295    /// removal (i.e. will point to an incorrect node after the removal), and this removal operation
296    /// would result in an invalid [`MastForest`].
297    ///
298    /// It also returns the map from old node IDs to new node IDs. Any [`MastNodeId`] used in
299    /// reference to the old [`MastForest`] should be remapped using this map.
300    #[cfg(test)]
301    fn remove_nodes(
302        &mut self,
303        nodes_to_remove: &BTreeSet<MastNodeId>,
304    ) -> BTreeMap<MastNodeId, MastNodeId> {
305        if nodes_to_remove.is_empty() {
306            return BTreeMap::new();
307        }
308
309        self.assert_nodes_to_remove_are_orphaned(nodes_to_remove);
310
311        let old_nodes = core::mem::replace(&mut self.nodes, IndexVec::new());
312        let old_root_ids = core::mem::take(&mut self.roots);
313        let (retained_nodes, id_remappings) = remove_nodes(old_nodes.into_inner(), nodes_to_remove);
314
315        self.remap_and_add_nodes(retained_nodes, &id_remappings);
316        self.remap_and_add_roots(old_root_ids, &id_remappings);
317
318        self.commitment = self.compute_mast_forest_commitment();
319
320        id_remappings
321    }
322
323    /// Merges all `forests` into a new [`MastForest`].
324    ///
325    /// Merging two forests means combining all their constituent parts, i.e. [`MastNode`]s and
326    /// roots. During this process, any duplicate or unreachable nodes are removed. Additionally,
327    /// [`MastNodeId`]s of nodes may change and references to them are remapped to their new
328    /// location.
329    ///
330    /// For example, consider this representation of a forest's nodes with all of these nodes being
331    /// roots:
332    ///
333    /// ```text
334    /// [Block(foo), Block(bar)]
335    /// ```
336    ///
337    /// If we merge another forest into it:
338    ///
339    /// ```text
340    /// [Block(bar), Call(0)]
341    /// ```
342    ///
343    /// then we would expect this forest:
344    ///
345    /// ```text
346    /// [Block(foo), Block(bar), Call(1)]
347    /// ```
348    ///
349    /// - The `Call` to the `bar` block was remapped to its new index (now 1, previously 0).
350    /// - The `Block(bar)` was deduplicated any only exists once in the merged forest.
351    ///
352    /// The function also returns a vector of [`MastForestRootMap`]s, whose length equals the number
353    /// of passed `forests`. The indices in the vector correspond to the ones in `forests`. The map
354    /// of a given forest contains the new locations of its roots in the merged forest. To
355    /// illustrate, the above example would return a vector of two maps:
356    ///
357    /// ```text
358    /// vec![{0 -> 0, 1 -> 1}
359    ///      {0 -> 1, 1 -> 2}]
360    /// ```
361    ///
362    /// - The root locations of the original forest are unchanged.
363    /// - For the second forest, the `bar` block has moved from index 0 to index 1 in the merged
364    ///   forest, and the `Call` has moved from index 1 to 2.
365    ///
366    /// If any forest being merged contains an `External(qux)` node and another forest contains a
367    /// node whose digest is `qux`, then the external node will be replaced with the `qux` node,
368    /// which is effectively deduplication.
369    pub fn merge<'forest>(
370        forests: impl IntoIterator<Item = &'forest MastForest>,
371    ) -> Result<(MastForest, MastForestRootMap), MastForestError> {
372        MastForestMerger::merge(forests)
373    }
374}
375
376// ------------------------------------------------------------------------------------------------
377/// Helpers
378impl MastForest {
379    #[cfg(test)]
380    fn assert_nodes_to_remove_are_orphaned(&self, nodes_to_remove: &BTreeSet<MastNodeId>) {
381        for (node_idx, node) in self.nodes.iter().enumerate() {
382            let node_id = MastNodeId::new_unchecked(node_idx.try_into().expect("too many nodes"));
383            if nodes_to_remove.contains(&node_id) {
384                continue;
385            }
386
387            node.for_each_child(|child_id| {
388                assert!(
389                    !nodes_to_remove.contains(&child_id),
390                    "cannot remove node {child_id:?}; retained node {node_id:?} references it"
391                );
392            });
393        }
394    }
395
396    /// Adds all provided nodes to the internal set of nodes, remapping all [`MastNodeId`]
397    /// references in those nodes.
398    ///
399    /// # Panics
400    /// - Panics if the internal set of nodes is not empty.
401    #[cfg(test)]
402    fn remap_and_add_nodes(
403        &mut self,
404        nodes_to_add: Vec<MastNode>,
405        id_remappings: &BTreeMap<MastNodeId, MastNodeId>,
406    ) {
407        assert!(self.nodes.is_empty());
408        let node_builders =
409            nodes_to_add.into_iter().map(|node| node.to_builder(self)).collect::<Vec<_>>();
410
411        // Add each node to the new MAST forest, making sure to rewrite any outdated internal
412        // `MastNodeId`s
413        for live_node_builder in node_builders {
414            let node = live_node_builder.remap_children(id_remappings).build_linked().unwrap();
415            self.nodes.push(node).unwrap();
416        }
417    }
418
419    /// Remaps and adds all old root ids to the internal set of roots.
420    ///
421    /// # Panics
422    /// - Panics if the internal set of roots is not empty.
423    #[cfg(test)]
424    fn remap_and_add_roots(
425        &mut self,
426        old_root_ids: Vec<MastNodeId>,
427        id_remappings: &BTreeMap<MastNodeId, MastNodeId>,
428    ) {
429        assert!(self.roots.is_empty());
430
431        for old_root_id in old_root_ids {
432            if let Some(new_root_id) = id_remappings.get(&old_root_id).copied() {
433                self.make_root(new_root_id);
434            }
435        }
436    }
437}
438
439/// Returns the set of nodes that are live, as well as the mapping from "old ID" to "new ID" for all
440/// live nodes.
441#[cfg(test)]
442fn remove_nodes(
443    mast_nodes: Vec<MastNode>,
444    nodes_to_remove: &BTreeSet<MastNodeId>,
445) -> (Vec<MastNode>, BTreeMap<MastNodeId, MastNodeId>) {
446    // Note: this allows us to safely use `usize as u32`, guaranteeing that it won't wrap around.
447    assert!(mast_nodes.len() < u32::MAX as usize);
448
449    let mut retained_nodes = Vec::with_capacity(mast_nodes.len());
450    let mut id_remappings = BTreeMap::new();
451
452    for (old_node_index, old_node) in mast_nodes.into_iter().enumerate() {
453        let old_node_id: MastNodeId = MastNodeId(old_node_index as u32);
454
455        if !nodes_to_remove.contains(&old_node_id) {
456            let new_node_id: MastNodeId = MastNodeId(retained_nodes.len() as u32);
457            id_remappings.insert(old_node_id, new_node_id);
458
459            retained_nodes.push(old_node);
460        }
461    }
462
463    (retained_nodes, id_remappings)
464}
465
466fn empty_mast_forest_commitment() -> MastForestCommitment {
467    let interface_commitment = crate::chiplets::hasher::hash_words(&[]);
468    let dependency_commitment = crate::chiplets::hasher::hash_words(&[]);
469    let advice_commitment = AdviceMap::default().commitment();
470    MastForestCommitment::new(interface_commitment, dependency_commitment, advice_commitment)
471}
472
473fn compute_nodes_commitment(
474    nodes: &IndexVec<MastNodeId, MastNode>,
475    node_ids: &[MastNodeId],
476) -> Word {
477    let mut digests: Vec<Word> = node_ids.iter().map(|&id| nodes[id].digest()).collect();
478    digests.sort_unstable();
479    crate::chiplets::hasher::hash_words(&digests)
480}
481
482fn compute_dependency_commitment(nodes: &IndexVec<MastNodeId, MastNode>) -> Word {
483    let mut digests: Vec<Word> = nodes
484        .iter()
485        .filter(|node| node.is_external())
486        .map(MastNodeExt::digest)
487        .collect();
488    digests.sort_unstable();
489    crate::chiplets::hasher::hash_words(&digests)
490}
491
492impl MastForestCommitment {
493    fn new(
494        interface_commitment: Word,
495        dependency_commitment: Word,
496        advice_commitment: Word,
497    ) -> Self {
498        let commitment = crate::chiplets::hasher::hash_words(&[
499            interface_commitment,
500            dependency_commitment,
501            advice_commitment,
502        ]);
503        Self {
504            commitment,
505            interface_commitment,
506            dependency_commitment,
507            advice_commitment,
508        }
509    }
510}
511
512fn compute_mast_forest_commitment(
513    nodes: &IndexVec<MastNodeId, MastNode>,
514    roots: &[MastNodeId],
515    advice_map: &AdviceMap,
516) -> MastForestCommitment {
517    let interface_commitment = compute_nodes_commitment(nodes, roots);
518    let dependency_commitment = compute_dependency_commitment(nodes);
519    let advice_commitment = advice_map.commitment();
520    MastForestCommitment::new(interface_commitment, dependency_commitment, advice_commitment)
521}
522
523// ------------------------------------------------------------------------------------------------
524/// Public accessors
525impl MastForest {
526    /// Returns the [`MastNode`] associated with the provided [`MastNodeId`] if valid, or else
527    /// `None`.
528    ///
529    /// This is the fallible version of indexing (e.g. `mast_forest[node_id]`).
530    #[inline(always)]
531    pub fn get_node_by_id(&self, node_id: MastNodeId) -> Option<&MastNode> {
532        self.nodes.get(node_id)
533    }
534
535    /// Returns the [`MastNodeId`] of the procedure associated with a given digest, if any.
536    #[inline(always)]
537    pub fn find_procedure_root(&self, digest: Word) -> Option<MastNodeId> {
538        self.roots.iter().find(|&&root_id| self[root_id].digest() == digest).copied()
539    }
540
541    /// Returns true if a node with the specified ID is a root of a procedure in this MAST forest.
542    pub fn is_procedure_root(&self, node_id: MastNodeId) -> bool {
543        self.roots.contains(&node_id)
544    }
545
546    /// Returns true if a node with the specified ID is a root of a procedure in this MAST forest,
547    /// and the digest of that procedure is `digest`.
548    ///
549    /// This is primarily intended for use in confirming that procedure exports of a package,
550    /// which declare their MAST node and digest, actually exist in the MAST.
551    pub fn is_procedure_root_with_exact_digest(&self, node_id: MastNodeId, digest: Word) -> bool {
552        self.is_procedure_root(node_id) && self[node_id].digest() == digest
553    }
554
555    /// Returns an iterator over the digests of all procedures in this MAST forest.
556    pub fn procedure_digests(&self) -> impl Iterator<Item = Word> + '_ {
557        self.roots.iter().map(|&root_id| self[root_id].digest())
558    }
559
560    /// Returns an iterator over the digests of local procedures in this MAST forest.
561    ///
562    /// A local procedure is defined as a procedure which is not a single external node.
563    pub fn local_procedure_digests(&self) -> impl Iterator<Item = Word> + '_ {
564        self.roots.iter().filter_map(|&root_id| {
565            let node = &self[root_id];
566            if node.is_external() { None } else { Some(node.digest()) }
567        })
568    }
569
570    /// Returns an iterator over the IDs of the procedures in this MAST forest.
571    pub fn procedure_roots(&self) -> &[MastNodeId] {
572        &self.roots
573    }
574
575    /// Returns the number of procedures in this MAST forest.
576    pub fn num_procedures(&self) -> u32 {
577        self.roots
578            .len()
579            .try_into()
580            .expect("MAST forest contains more than 2^32 procedures.")
581    }
582
583    /// Returns the [Word] representing the content hash of a subset of [`MastNodeId`]s.
584    ///
585    /// # Panics
586    /// This function panics if any `node_ids` is not a node of this forest.
587    pub fn compute_nodes_commitment<'a>(
588        &self,
589        node_ids: impl IntoIterator<Item = &'a MastNodeId>,
590    ) -> Word {
591        let node_ids = node_ids.into_iter().copied().collect::<Vec<_>>();
592        compute_nodes_commitment(&self.nodes, &node_ids)
593    }
594
595    /// Returns the commitment to this MAST forest's public procedure roots.
596    ///
597    /// The commitment is computed as the sequential hash of all procedure root digests after
598    /// sorting them by digest.
599    pub fn interface_commitment(&self) -> Word {
600        self.commitment.interface_commitment
601    }
602
603    /// Returns the commitment to this MAST forest's external dependencies.
604    ///
605    /// The commitment is computed as the sequential hash of all external node digests after
606    /// sorting them by digest.
607    pub fn dependency_commitment(&self) -> Word {
608        self.commitment.dependency_commitment
609    }
610
611    /// Returns the commitment to this MAST forest's advice map.
612    ///
613    /// The commitment is computed over advice entries in key order.
614    pub fn advice_commitment(&self) -> Word {
615        self.commitment.advice_commitment
616    }
617
618    fn compute_mast_forest_commitment(&self) -> MastForestCommitment {
619        compute_mast_forest_commitment(&self.nodes, &self.roots, &self.advice_map)
620    }
621
622    /// Returns the commitment to this MAST forest.
623    ///
624    /// The commitment is computed from the interface, dependency, and advice commitments.
625    pub fn commitment(&self) -> Word {
626        self.commitment.commitment
627    }
628
629    /// Returns the number of nodes in this MAST forest.
630    pub fn num_nodes(&self) -> u32 {
631        self.nodes.len() as u32
632    }
633
634    /// Returns the underlying nodes in this MAST forest.
635    pub fn nodes(&self) -> &[MastNode] {
636        self.nodes.as_slice()
637    }
638
639    pub fn advice_map(&self) -> &AdviceMap {
640        &self.advice_map
641    }
642
643    /// Returns this forest with `advice_map` entries added.
644    pub fn with_advice_map(mut self, advice_map: AdviceMap) -> Self {
645        self.advice_map.extend(advice_map);
646        self.commitment = self.compute_mast_forest_commitment();
647        self
648    }
649
650    // SERIALIZATION
651    // --------------------------------------------------------------------------------------------
652
653    /// Serializes this MastForest with the HASHLESS flag set.
654    ///
655    /// Hashless forest bytes omit rebuildable internal node hashes. External node digests stay on
656    /// the wire because they cannot be rebuilt from local structure. Trusted deserialization
657    /// rejects this flag.
658    ///
659    /// Use this when producing data for untrusted validation.
660    pub fn write_hashless<W: ByteWriter>(&self, target: &mut W) {
661        serialization::write_hashless_into(self, target);
662    }
663}
664
665/// Validation methods
666impl MastForest {
667    pub(in crate::mast) fn validate_dense_node_order(&self) -> Result<(), MastForestError> {
668        validate_dense_node_order(&self.nodes)
669    }
670
671    fn validate_basic_block_invariants(&self) -> Result<(), MastForestError> {
672        for (node_id_idx, node) in self.nodes.iter().enumerate() {
673            let node_id =
674                MastNodeId::new_unchecked(node_id_idx.try_into().expect("too many nodes"));
675            if let MastNode::Block(basic_block) = node {
676                basic_block.validate_batch_invariants().map_err(|error_msg| {
677                    MastForestError::InvalidBatchPadding(node_id, error_msg)
678                })?;
679            }
680        }
681
682        Ok(())
683    }
684
685    /// Validates that all BasicBlockNodes in this forest satisfy the core invariants:
686    /// 1. Power-of-two number of groups in each batch
687    /// 2. No operation group ends with an operation requiring an immediate value
688    /// 3. The last operation group in a batch cannot contain operations requiring immediate values
689    /// 4. OpBatch structural consistency (num_groups <= BATCH_SIZE, group size <= GROUP_SIZE,
690    ///    indptr integrity, bounds checking)
691    ///
692    /// This addresses the gap created by PR 2094, where padding NOOPs are now inserted
693    /// at assembly time rather than dynamically during execution, and adds comprehensive
694    /// structural validation to prevent deserialization-time panics.
695    pub fn validate(&self) -> Result<(), MastForestError> {
696        self.validate_basic_block_invariants()?;
697        Ok(())
698    }
699
700    /// Validates that stored node digests match the hashes implied by local structure.
701    ///
702    /// For `External` nodes the digest is accepted as-is because it is externally provided and
703    /// cannot be reconstructed from local structure alone.
704    fn validate_node_hashes(&self) -> Result<(), MastForestError> {
705        let computed_hashes = self.compute_node_hashes()?;
706        for (node_idx, (node, computed_digest)) in
707            self.nodes.iter().zip(computed_hashes).enumerate()
708        {
709            let expected_digest = node.digest();
710            if expected_digest != computed_digest {
711                return Err(MastForestError::HashMismatch {
712                    node_id: MastNodeId::new_unchecked(node_idx as u32),
713                    expected: expected_digest,
714                    computed: computed_digest,
715                });
716            }
717        }
718
719        Ok(())
720    }
721
722    /// Computes node hashes in topological order.
723    ///
724    /// The returned vector is aligned with node indices, so `digests[node_id as usize]` is the
725    /// digest of that node.
726    ///
727    /// For `External` nodes, the existing digest is returned unchanged.
728    ///
729    /// Returns [`MastForestError::ForwardReference`] if nodes are not in topological order.
730    fn compute_node_hashes(&self) -> Result<Vec<Word>, MastForestError> {
731        use crate::chiplets::hasher;
732
733        /// Checks that child_id references a node that appears before node_id in topological order.
734        fn check_no_forward_ref(
735            node_id: MastNodeId,
736            child_id: MastNodeId,
737        ) -> Result<(), MastForestError> {
738            if child_id.0 >= node_id.0 {
739                return Err(MastForestError::ForwardReference(node_id, child_id));
740            }
741            Ok(())
742        }
743
744        let mut computed_hashes = Vec::with_capacity(self.nodes.len());
745        for (node_idx, node) in self.nodes.iter().enumerate() {
746            let node_id = MastNodeId::new_unchecked(node_idx as u32);
747
748            // Check topological ordering and compute digest.
749            let computed_digest = match node {
750                MastNode::Block(block) => node::hash_op_batches(block.op_batches()),
751                MastNode::Join(join) => {
752                    let left_id = join.first();
753                    let right_id = join.second();
754                    check_no_forward_ref(node_id, left_id)?;
755                    check_no_forward_ref(node_id, right_id)?;
756
757                    let left_digest = computed_hashes[left_id.0 as usize];
758                    let right_digest = computed_hashes[right_id.0 as usize];
759                    hasher::merge_in_mast_domain(&[left_digest, right_digest], JoinNode::DOMAIN)
760                },
761                MastNode::Split(split) => {
762                    let true_id = split.on_true();
763                    let false_id = split.on_false();
764                    check_no_forward_ref(node_id, true_id)?;
765                    check_no_forward_ref(node_id, false_id)?;
766
767                    let true_digest = computed_hashes[true_id.0 as usize];
768                    let false_digest = computed_hashes[false_id.0 as usize];
769                    hasher::merge_in_mast_domain(&[true_digest, false_digest], SplitNode::DOMAIN)
770                },
771                MastNode::Loop(loop_node) => {
772                    let body_id = loop_node.body();
773                    check_no_forward_ref(node_id, body_id)?;
774
775                    let body_digest = computed_hashes[body_id.0 as usize];
776                    hasher::merge_in_mast_domain(&[body_digest, Word::default()], LoopNode::DOMAIN)
777                },
778                MastNode::Call(call) => {
779                    let callee_id = call.callee();
780                    check_no_forward_ref(node_id, callee_id)?;
781
782                    let callee_digest = computed_hashes[callee_id.0 as usize];
783                    let domain = if call.is_syscall() {
784                        CallNode::SYSCALL_DOMAIN
785                    } else {
786                        CallNode::CALL_DOMAIN
787                    };
788                    hasher::merge_in_mast_domain(&[callee_digest, Word::default()], domain)
789                },
790                MastNode::Dyn(dyn_node) => {
791                    if dyn_node.is_dyncall() {
792                        DynNode::DYNCALL_DEFAULT_DIGEST
793                    } else {
794                        DynNode::DYN_DEFAULT_DIGEST
795                    }
796                },
797                MastNode::External(_) => {
798                    // External nodes have externally-provided digests that cannot be recomputed.
799                    node.digest()
800                },
801            };
802
803            computed_hashes.push(computed_digest);
804        }
805
806        Ok(computed_hashes)
807    }
808}
809
810// MAST FOREST INDEXING
811// ------------------------------------------------------------------------------------------------
812
813impl Index<MastNodeId> for MastForest {
814    type Output = MastNode;
815
816    #[inline(always)]
817    fn index(&self, node_id: MastNodeId) -> &Self::Output {
818        &self.nodes[node_id]
819    }
820}
821
822// EXECUTABLE MAST FOREST
823// ================================================================================================
824
825/// A MAST forest that can be used as the source of nodes during program execution.
826///
827/// Implemented by both [`MastForest`] (a dense forest containing all nodes) and
828/// [`SparseMastForest`] (a sparse subset of a forest containing only the nodes visited during
829/// some prior execution). The latter preserves the original [`MastNodeId`]s of its source forest,
830/// which allows it to stand in for the dense forest during re-execution.
831pub trait ExecutableMastForest {
832    /// Returns the [`MastNode`] associated with the provided [`MastNodeId`] if present, or else
833    /// `None`.
834    fn get_node_by_id(&self, node_id: MastNodeId) -> Option<&MastNode>;
835
836    /// Returns the digest of the node associated with the provided [`MastNodeId`] if present, or
837    /// else `None`.
838    ///
839    /// For dense forests this is equivalent to `get_node_by_id(id).map(|n| n.digest())`. For
840    /// [`SparseMastForest`], it additionally consults the digest-only entries — nodes that were
841    /// referenced (but not entered) during execution and which were therefore stored as digest
842    /// only. Use this method whenever only the digest of a referenced node is needed (e.g. when
843    /// populating the hasher state of a parent's trace row).
844    fn get_digest_by_id(&self, node_id: MastNodeId) -> Option<Word>;
845
846    /// Returns the [`MastNodeId`] of the procedure associated with a given digest, if any.
847    fn find_procedure_root(&self, digest: Word) -> Option<MastNodeId>;
848
849    /// Returns the advice map associated with this forest.
850    fn advice_map(&self) -> &AdviceMap;
851}
852
853impl ExecutableMastForest for MastForest {
854    #[inline(always)]
855    fn get_node_by_id(&self, node_id: MastNodeId) -> Option<&MastNode> {
856        MastForest::get_node_by_id(self, node_id)
857    }
858
859    #[inline(always)]
860    fn get_digest_by_id(&self, node_id: MastNodeId) -> Option<Word> {
861        MastForest::get_node_by_id(self, node_id).map(MastNodeExt::digest)
862    }
863
864    #[inline(always)]
865    fn find_procedure_root(&self, digest: Word) -> Option<MastNodeId> {
866        MastForest::find_procedure_root(self, digest)
867    }
868
869    #[inline(always)]
870    fn advice_map(&self) -> &AdviceMap {
871        MastForest::advice_map(self)
872    }
873}
874
875// Blanket impl: an `Arc<T>` is an `ExecutableMastForest` whenever the underlying `T` is, which
876// allows the executor and tracer plumbing to be generic over a forest type while the live
877// (`Arc<MastForest>`) and replay (`Arc<SparseMastForest>`) paths each pick a concrete instance.
878impl<T> Index<MastNodeId> for Arc<T>
879where
880    T: Index<MastNodeId, Output = MastNode> + ?Sized,
881{
882    type Output = MastNode;
883
884    #[inline(always)]
885    fn index(&self, node_id: MastNodeId) -> &Self::Output {
886        &(**self)[node_id]
887    }
888}
889
890impl<T: ExecutableMastForest + ?Sized> ExecutableMastForest for Arc<T> {
891    #[inline(always)]
892    fn get_node_by_id(&self, node_id: MastNodeId) -> Option<&MastNode> {
893        T::get_node_by_id(self, node_id)
894    }
895
896    #[inline(always)]
897    fn get_digest_by_id(&self, node_id: MastNodeId) -> Option<Word> {
898        T::get_digest_by_id(self, node_id)
899    }
900
901    #[inline(always)]
902    fn find_procedure_root(&self, digest: Word) -> Option<MastNodeId> {
903        T::find_procedure_root(self, digest)
904    }
905
906    #[inline(always)]
907    fn advice_map(&self) -> &AdviceMap {
908        T::advice_map(self)
909    }
910}
911
912// MAST NODE ID
913// ================================================================================================
914
915/// An opaque handle to a [`MastNode`] in some [`MastForest`]. It is the responsibility of the user
916/// to use a given [`MastNodeId`] with the corresponding [`MastForest`].
917///
918/// Note that the [`MastForest`] does *not* ensure that equal [`MastNode`]s have equal
919/// [`MastNodeId`] handles. Hence, [`MastNodeId`] equality must not be used to test for equality of
920/// the underlying [`MastNode`].
921#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
922#[cfg_attr(
923    all(feature = "arbitrary", test),
924    miden_test_serialization_macros::serialization_test
925)]
926pub struct MastNodeId(u32);
927
928/// Operations that mutate a MAST often produce this mapping between old and new NodeIds.
929pub type Remapping = BTreeMap<MastNodeId, MastNodeId>;
930
931impl MastNodeId {
932    /// Returns a new `MastNodeId` with the provided inner value, or an error if the provided
933    /// `value` is greater than the number of nodes in the forest.
934    ///
935    /// For use in deserialization.
936    pub fn from_u32_safe(
937        value: u32,
938        mast_forest: &MastForest,
939    ) -> Result<Self, DeserializationError> {
940        Self::from_u32_with_node_count(value, mast_forest.nodes.len())
941    }
942
943    /// Returns a new [`MastNodeId`] from the given `value` without checking its validity.
944    pub fn new_unchecked(value: u32) -> Self {
945        Self(value)
946    }
947
948    /// Returns a new [`MastNodeId`] with the provided `id`, or an error if `id` is greater or equal
949    /// to `node_count`. The `node_count` is the total number of nodes in the [`MastForest`] for
950    /// which this ID is being constructed.
951    ///
952    /// This function can be used when deserializing an id whose corresponding node is not yet in
953    /// the forest and [`Self::from_u32_safe`] would fail. For instance, when deserializing the ids
954    /// referenced by the Join node in this forest:
955    ///
956    /// ```text
957    /// [Join(1, 2), Block(foo), Block(bar)]
958    /// ```
959    ///
960    /// Since it is less safe than [`Self::from_u32_safe`] and usually not needed it is not public.
961    pub(super) fn from_u32_with_node_count(
962        id: u32,
963        node_count: usize,
964    ) -> Result<Self, DeserializationError> {
965        if (id as usize) < node_count {
966            Ok(Self(id))
967        } else {
968            Err(DeserializationError::InvalidValue(format!(
969                "Invalid deserialized MAST node ID '{id}', but {node_count} is the number of nodes in the forest",
970            )))
971        }
972    }
973
974    /// Remap the NodeId to its new position using the given [`Remapping`].
975    pub fn remap(&self, remapping: &Remapping) -> Self {
976        *remapping.get(self).unwrap_or(self)
977    }
978}
979
980impl From<u32> for MastNodeId {
981    fn from(value: u32) -> Self {
982        MastNodeId::new_unchecked(value)
983    }
984}
985
986impl Idx for MastNodeId {}
987
988impl From<MastNodeId> for u32 {
989    fn from(value: MastNodeId) -> Self {
990        value.0
991    }
992}
993
994impl Serializable for MastNodeId {
995    fn write_into<W: ByteWriter>(&self, target: &mut W) {
996        Serializable::write_into(&self.0, target);
997    }
998}
999
1000impl Deserializable for MastNodeId {
1001    fn read_from<R: crate::serde::ByteReader>(
1002        source: &mut R,
1003    ) -> Result<Self, DeserializationError> {
1004        Ok(Self(<u32 as Deserializable>::read_from(source)?))
1005    }
1006
1007    fn min_serialized_size() -> usize {
1008        <u32 as Deserializable>::min_serialized_size()
1009    }
1010}
1011
1012impl fmt::Display for MastNodeId {
1013    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1014        write!(f, "MastNodeId({})", self.0)
1015    }
1016}
1017
1018#[cfg(any(test, feature = "arbitrary"))]
1019impl Arbitrary for MastNodeId {
1020    type Parameters = ();
1021
1022    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
1023        use proptest::prelude::*;
1024        any::<u32>().prop_map(MastNodeId).boxed()
1025    }
1026
1027    type Strategy = BoxedStrategy<Self>;
1028}
1029
1030// ITERATOR
1031
1032/// Iterates over all the nodes a root depends on, in pre-order. The iteration can include other
1033/// roots in the same forest.
1034pub struct SubtreeIterator<'a> {
1035    forest: &'a MastForest,
1036    discovered: Vec<MastNodeId>,
1037    unvisited: Vec<MastNodeId>,
1038}
1039impl<'a> SubtreeIterator<'a> {
1040    pub fn new(root: &MastNodeId, forest: &'a MastForest) -> Self {
1041        let discovered = vec![];
1042        let unvisited = vec![*root];
1043        SubtreeIterator { forest, discovered, unvisited }
1044    }
1045}
1046impl Iterator for SubtreeIterator<'_> {
1047    type Item = MastNodeId;
1048    fn next(&mut self) -> Option<MastNodeId> {
1049        while let Some(id) = self.unvisited.pop() {
1050            let node = &self.forest[id];
1051            if !node.has_children() {
1052                return Some(id);
1053            } else {
1054                self.discovered.push(id);
1055                node.append_children_to(&mut self.unvisited);
1056            }
1057        }
1058        self.discovered.pop()
1059    }
1060}
1061
1062/// Derives an error code from an error message by hashing the message and returning the 0th element
1063/// of the resulting [`Word`].
1064pub fn error_code_from_msg(msg: impl AsRef<str>) -> Felt {
1065    // hash the message and return 0th felt of the resulting Word
1066    hash_string_to_word(msg.as_ref())[0]
1067}
1068
1069// MAST FOREST ERROR
1070// ================================================================================================
1071
1072/// Represents the types of errors that can occur when dealing with MAST forest.
1073#[derive(Debug, thiserror::Error, PartialEq, Eq)]
1074pub enum MastForestError {
1075    #[error("MAST forest node count exceeds the maximum of {} nodes", MastForest::MAX_NODES)]
1076    TooManyNodes,
1077    #[error("node id {0} is greater than or equal to forest length {1}")]
1078    NodeIdOverflow(MastNodeId, usize),
1079    #[error("basic block cannot be created from an empty list of operations")]
1080    EmptyBasicBlock,
1081    #[error("advice map key {0} already exists when merging forests")]
1082    AdviceMapKeyCollisionOnMerge(Word),
1083    #[error("digest is required for deserialization")]
1084    DigestRequiredForDeserialization,
1085    #[error("invalid batch in basic block node {0:?}: {1}")]
1086    InvalidBatchPadding(MastNodeId, String),
1087    #[error("invalid node order at {node_id:?}: {reason}")]
1088    InvalidNodeOrder { node_id: MastNodeId, reason: String },
1089    #[error(
1090        "node {0:?} references child {1:?} which comes after it in the forest (forward reference)"
1091    )]
1092    ForwardReference(MastNodeId, MastNodeId),
1093    #[error("hash mismatch for node {node_id:?}: expected {expected:?}, computed {computed:?}")]
1094    HashMismatch {
1095        node_id: MastNodeId,
1096        expected: Word,
1097        computed: Word,
1098    },
1099    #[error("deserialization failed: {0}")]
1100    Deserialization(DeserializationError),
1101}
1102
1103// Custom serde implementation for MastForest delegates to the binary serialization format.