Skip to main content

miden_core/mast/node/
dyn_node.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::fmt;
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7use super::{MastForestContributor, MastNodeExt};
8use crate::{
9    Felt, Word,
10    mast::{MastForest, MastForestError, MastNodeId},
11    operations::opcodes,
12    prettier::{Document, PrettyPrint, const_text},
13    utils::LookupByIdx,
14};
15
16// DYN NODE
17// ================================================================================================
18
19/// A Dyn node specifies that the node to be executed next is defined dynamically via the stack.
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
23pub struct DynNode {
24    is_dyncall: bool,
25    digest: Word,
26}
27
28/// Constants
29impl DynNode {
30    /// The domain of the Dyn block (used for control block hashing).
31    pub const DYN_DOMAIN: Felt = Felt::new_unchecked(opcodes::DYN as u64);
32
33    /// The domain of the Dyncall block (used for control block hashing).
34    pub const DYNCALL_DOMAIN: Felt = Felt::new_unchecked(opcodes::DYNCALL as u64);
35}
36
37/// Default digest constants
38impl DynNode {
39    /// The default digest for a DynNode representing a dyncall operation.
40    pub const DYNCALL_DEFAULT_DIGEST: Word = Word::new([
41        Felt::new_unchecked(16830415514927835337),
42        Felt::new_unchecked(12164645914672292987),
43        Felt::new_unchecked(13192574193032437705),
44        Felt::new_unchecked(4604554596675732269),
45    ]);
46
47    /// The default digest for a DynNode representing a dynexec operation.
48    pub const DYN_DEFAULT_DIGEST: Word = Word::new([
49        Felt::new_unchecked(16952228088962355159),
50        Felt::new_unchecked(5793482471479538911),
51        Felt::new_unchecked(14446299416172848527),
52        Felt::new_unchecked(13522295374716441620),
53    ]);
54}
55
56/// Public accessors
57impl DynNode {
58    /// Returns true if the [`DynNode`] represents a dyncall operation, and false for dynexec.
59    pub fn is_dyncall(&self) -> bool {
60        self.is_dyncall
61    }
62
63    /// Returns the domain of this dyn node.
64    pub fn domain(&self) -> Felt {
65        if self.is_dyncall() {
66            Self::DYNCALL_DOMAIN
67        } else {
68            Self::DYN_DOMAIN
69        }
70    }
71}
72
73// PRETTY PRINTING
74// ================================================================================================
75
76impl DynNode {
77    pub(super) fn to_display<'a>(&'a self, _mast_forest: &'a MastForest) -> impl fmt::Display + 'a {
78        self.clone()
79    }
80
81    pub(super) fn to_pretty_print<'a>(
82        &'a self,
83        _mast_forest: &'a MastForest,
84    ) -> impl PrettyPrint + 'a {
85        self.clone()
86    }
87}
88
89impl PrettyPrint for DynNode {
90    fn render(&self) -> Document {
91        if self.is_dyncall() {
92            const_text("dyncall")
93        } else {
94            const_text("dyn")
95        }
96    }
97}
98
99impl fmt::Display for DynNode {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        self.pretty_print(f)
102    }
103}
104
105// MAST NODE TRAIT IMPLEMENTATION
106// ================================================================================================
107
108impl MastNodeExt for DynNode {
109    /// Returns a commitment to a Dyn node.
110    fn digest(&self) -> Word {
111        self.digest
112    }
113
114    fn to_display<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn fmt::Display + 'a> {
115        Box::new(DynNode::to_display(self, mast_forest))
116    }
117
118    fn to_pretty_print<'a>(&'a self, mast_forest: &'a MastForest) -> Box<dyn PrettyPrint + 'a> {
119        Box::new(DynNode::to_pretty_print(self, mast_forest))
120    }
121
122    fn has_children(&self) -> bool {
123        false
124    }
125
126    fn append_children_to(&self, _target: &mut Vec<MastNodeId>) {
127        // No children for dyn nodes
128    }
129
130    fn for_each_child<F>(&self, _f: F)
131    where
132        F: FnMut(MastNodeId),
133    {
134        // DynNode has no children
135    }
136
137    fn domain(&self) -> Felt {
138        self.domain()
139    }
140
141    type Builder = DynNodeBuilder;
142
143    fn to_builder(self, _forest: &MastForest) -> Self::Builder {
144        let builder = if self.is_dyncall {
145            DynNodeBuilder::new_dyncall()
146        } else {
147            DynNodeBuilder::new_dyn()
148        };
149        builder.with_digest(self.digest)
150    }
151}
152
153// ARBITRARY IMPLEMENTATION
154// ================================================================================================
155
156#[cfg(all(feature = "arbitrary", test))]
157impl proptest::prelude::Arbitrary for DynNode {
158    type Parameters = ();
159
160    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
161        use proptest::prelude::*;
162
163        // Generate whether it's a dyncall or dynexec
164        any::<bool>()
165            .prop_map(|is_dyncall| {
166                if is_dyncall {
167                    DynNodeBuilder::new_dyncall().build()
168                } else {
169                    DynNodeBuilder::new_dyn().build()
170                }
171            })
172            .no_shrink()  // Pure random values, no meaningful shrinking pattern
173            .boxed()
174    }
175
176    type Strategy = proptest::prelude::BoxedStrategy<Self>;
177}
178
179// ------------------------------------------------------------------------------------------------
180/// Builder for creating [`DynNode`] instances.
181#[derive(Debug)]
182pub struct DynNodeBuilder {
183    is_dyncall: bool,
184    digest: Option<Word>,
185}
186
187impl DynNodeBuilder {
188    /// Creates a new builder for a DynNode representing a dynexec operation.
189    pub fn new_dyn() -> Self {
190        Self { is_dyncall: false, digest: None }
191    }
192
193    /// Creates a new builder for a DynNode representing a dyncall operation.
194    pub fn new_dyncall() -> Self {
195        Self { is_dyncall: true, digest: None }
196    }
197
198    /// Builds the DynNode.
199    pub fn build(self) -> DynNode {
200        // Use the forced digest if provided, otherwise use the default digest
201        let digest = if let Some(forced_digest) = self.digest {
202            forced_digest
203        } else if self.is_dyncall {
204            DynNode::DYNCALL_DEFAULT_DIGEST
205        } else {
206            DynNode::DYN_DEFAULT_DIGEST
207        };
208
209        DynNode { is_dyncall: self.is_dyncall, digest }
210    }
211}
212
213impl MastForestContributor for DynNodeBuilder {
214    fn add_to_forest(self, forest: &mut MastForest) -> Result<MastNodeId, MastForestError> {
215        // Use the forced digest if provided, otherwise use the default digest
216        let digest = if let Some(forced_digest) = self.digest {
217            forced_digest
218        } else if self.is_dyncall {
219            DynNode::DYNCALL_DEFAULT_DIGEST
220        } else {
221            DynNode::DYN_DEFAULT_DIGEST
222        };
223
224        // Create the node in the forest with Linked variant from the start
225        // Move the data directly without intermediate cloning
226        let node_id = forest
227            .nodes
228            .push(DynNode { is_dyncall: self.is_dyncall, digest }.into())
229            .map_err(|_| MastForestError::TooManyNodes)?;
230
231        Ok(node_id)
232    }
233
234    fn fingerprint_for_node(
235        &self,
236        _forest: &MastForest,
237        _hash_by_node_id: &impl LookupByIdx<MastNodeId, Word>,
238    ) -> Result<Word, MastForestError> {
239        Ok(if let Some(forced_digest) = self.digest {
240            forced_digest
241        } else if self.is_dyncall {
242            DynNode::DYNCALL_DEFAULT_DIGEST
243        } else {
244            DynNode::DYN_DEFAULT_DIGEST
245        })
246    }
247
248    fn remap_children(self, _remapping: &impl LookupByIdx<MastNodeId, MastNodeId>) -> Self {
249        // DynNode has no children to remap, but preserve the digest
250        self
251    }
252
253    fn with_digest(mut self, digest: Word) -> Self {
254        self.digest = Some(digest);
255        self
256    }
257}
258
259impl DynNodeBuilder {
260    /// Add this node to a forest using relaxed validation.
261    ///
262    /// This method is used during deserialization where nodes may reference child nodes
263    /// that haven't been added to the forest yet. The child node IDs have already been
264    /// validated against the expected final node count during the `try_into_mast_node_builder`
265    /// step, so we can safely skip validation here.
266    ///
267    /// Note: This is not part of the `MastForestContributor` trait because it's only
268    /// intended for internal use during deserialization.
269    pub(in crate::mast) fn add_to_forest_relaxed(
270        self,
271        forest: &mut MastForest,
272    ) -> Result<MastNodeId, MastForestError> {
273        // Use the forced digest if provided, otherwise use the default digest
274        let digest = if let Some(forced_digest) = self.digest {
275            forced_digest
276        } else if self.is_dyncall {
277            DynNode::DYNCALL_DEFAULT_DIGEST
278        } else {
279            DynNode::DYN_DEFAULT_DIGEST
280        };
281
282        // Create the node in the forest with Linked variant from the start
283        // Move the data directly without intermediate cloning
284        let node_id = forest
285            .nodes
286            .push(DynNode { is_dyncall: self.is_dyncall, digest }.into())
287            .map_err(|_| MastForestError::TooManyNodes)?;
288
289        Ok(node_id)
290    }
291}
292
293#[cfg(any(test, feature = "arbitrary"))]
294impl proptest::prelude::Arbitrary for DynNodeBuilder {
295    type Parameters = ();
296    type Strategy = proptest::strategy::BoxedStrategy<Self>;
297
298    fn arbitrary_with(_params: Self::Parameters) -> Self::Strategy {
299        use proptest::prelude::*;
300
301        any::<bool>()
302            .prop_map(|is_dyncall| {
303                if is_dyncall {
304                    Self::new_dyncall()
305                } else {
306                    Self::new_dyn()
307                }
308            })
309            .boxed()
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use miden_crypto::hash::poseidon2::Poseidon2;
316
317    use super::*;
318
319    /// Ensures that the hash of `DynNode` is indeed the hash of 2 empty words, in the `DynNode`
320    /// domain.
321    #[test]
322    pub fn test_dyn_node_digest() {
323        let mut forest = MastForest::new();
324        let dyn_node_id = DynNodeBuilder::new_dyn().add_to_forest(&mut forest).unwrap();
325        let dyn_node = forest.get_node_by_id(dyn_node_id).unwrap().unwrap_dyn();
326        assert_eq!(
327            dyn_node.digest(),
328            Poseidon2::merge_in_domain(&[Word::default(), Word::default()], DynNode::DYN_DOMAIN)
329        );
330
331        let dyncall_node_id = DynNodeBuilder::new_dyncall().add_to_forest(&mut forest).unwrap();
332        let dyncall_node = forest.get_node_by_id(dyncall_node_id).unwrap().unwrap_dyn();
333        assert_eq!(
334            dyncall_node.digest(),
335            Poseidon2::merge_in_domain(
336                &[Word::default(), Word::default()],
337                DynNode::DYNCALL_DOMAIN
338            )
339        );
340    }
341}