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