Skip to main content

miden_protocol/note/
script.rs

1use alloc::string::ToString;
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::fmt::Display;
5use core::num::TryFromIntError;
6
7use miden_core::mast::MastNodeExt;
8
9use super::Felt;
10use crate::assembly::mast::{ExternalNodeBuilder, MastForest, MastForestContributor, MastNodeId};
11use crate::assembly::{Library, Path};
12use crate::errors::NoteError;
13use crate::utils::serde::{
14    ByteReader,
15    ByteWriter,
16    Deserializable,
17    DeserializationError,
18    Serializable,
19};
20use crate::vm::{AdviceMap, Program};
21use crate::{PrettyPrint, Word};
22
23/// The attribute name used to mark the entrypoint procedure in a note script library.
24const NOTE_SCRIPT_ATTRIBUTE: &str = "note_script";
25
26// NOTE SCRIPT
27// ================================================================================================
28
29/// An executable program of a note.
30///
31/// A note's script represents a program which must be executed for a note to be consumed. As such
32/// it defines the rules and side effects of consuming a given note.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct NoteScript {
35    mast: Arc<MastForest>,
36    entrypoint: MastNodeId,
37}
38
39impl NoteScript {
40    // CONSTRUCTORS
41    // --------------------------------------------------------------------------------------------
42
43    /// Returns a new [NoteScript] instantiated from the provided program.
44    pub fn new(code: Program) -> Self {
45        Self {
46            entrypoint: code.entrypoint(),
47            mast: code.mast_forest().clone(),
48        }
49    }
50
51    /// Returns a new [NoteScript] deserialized from the provided bytes.
52    ///
53    /// # Errors
54    /// Returns an error if note script deserialization fails.
55    pub fn from_bytes(bytes: &[u8]) -> Result<Self, NoteError> {
56        Self::read_from_bytes(bytes).map_err(NoteError::NoteScriptDeserializationError)
57    }
58
59    /// Returns a new [NoteScript] instantiated from the provided components.
60    ///
61    /// # Panics
62    /// Panics if the specified entrypoint is not in the provided MAST forest.
63    pub fn from_parts(mast: Arc<MastForest>, entrypoint: MastNodeId) -> Self {
64        assert!(mast.get_node_by_id(entrypoint).is_some());
65        Self { mast, entrypoint }
66    }
67
68    /// Returns a new [NoteScript] instantiated from the provided library.
69    ///
70    /// The library must contain exactly one procedure with the `@note_script` attribute,
71    /// which will be used as the entrypoint.
72    ///
73    /// # Errors
74    /// Returns an error if:
75    /// - The library does not contain a procedure with the `@note_script` attribute.
76    /// - The library contains multiple procedures with the `@note_script` attribute.
77    pub fn from_library(library: &Library) -> Result<Self, NoteError> {
78        let mut entrypoint = None;
79
80        for export in library.exports() {
81            if let Some(proc_export) = export.as_procedure() {
82                // Check for @note_script attribute
83                if proc_export.attributes.has(NOTE_SCRIPT_ATTRIBUTE) {
84                    if entrypoint.is_some() {
85                        return Err(NoteError::NoteScriptMultipleProceduresWithAttribute);
86                    }
87                    entrypoint = Some(proc_export.node);
88                }
89            }
90        }
91
92        let entrypoint = entrypoint.ok_or(NoteError::NoteScriptNoProcedureWithAttribute)?;
93
94        Ok(Self {
95            mast: library.mast_forest().clone(),
96            entrypoint,
97        })
98    }
99
100    /// Returns a new [NoteScript] containing only a reference to a procedure in the provided
101    /// library.
102    ///
103    /// This method is useful when a library contains multiple note scripts and you need to
104    /// extract a specific one by its fully qualified path (e.g.,
105    /// `miden::standards::notes::burn::main`).
106    ///
107    /// The procedure at the specified path must have the `@note_script` attribute.
108    ///
109    /// Note: This method creates a minimal [MastForest] containing only an external node
110    /// referencing the procedure's digest, rather than copying the entire library. The actual
111    /// procedure code will be resolved at runtime via the `MastForestStore`.
112    ///
113    /// # Errors
114    /// Returns an error if:
115    /// - The library does not contain a procedure at the specified path.
116    /// - The procedure at the specified path does not have the `@note_script` attribute.
117    pub fn from_library_reference(library: &Library, path: &Path) -> Result<Self, NoteError> {
118        // Find the export matching the path
119        let export = library
120            .exports()
121            .find(|e| e.path().as_ref() == path)
122            .ok_or_else(|| NoteError::NoteScriptProcedureNotFound(path.to_string().into()))?;
123
124        // Get the procedure export and verify it has the @note_script attribute
125        let proc_export = export
126            .as_procedure()
127            .ok_or_else(|| NoteError::NoteScriptProcedureNotFound(path.to_string().into()))?;
128
129        if !proc_export.attributes.has(NOTE_SCRIPT_ATTRIBUTE) {
130            return Err(NoteError::NoteScriptProcedureMissingAttribute(path.to_string().into()));
131        }
132
133        // Get the digest of the procedure from the library
134        let digest = library.mast_forest()[proc_export.node].digest();
135
136        // Create a minimal MastForest with just an external node referencing the digest
137        let (mast, entrypoint) = create_external_node_forest(digest);
138
139        Ok(Self { mast: Arc::new(mast), entrypoint })
140    }
141
142    // PUBLIC ACCESSORS
143    // --------------------------------------------------------------------------------------------
144
145    /// Returns the commitment of this note script (i.e., the script's MAST root).
146    pub fn root(&self) -> Word {
147        self.mast[self.entrypoint].digest()
148    }
149
150    /// Returns a reference to the [MastForest] backing this note script.
151    pub fn mast(&self) -> Arc<MastForest> {
152        self.mast.clone()
153    }
154
155    /// Returns an entrypoint node ID of the current script.
156    pub fn entrypoint(&self) -> MastNodeId {
157        self.entrypoint
158    }
159
160    /// Clears all debug info from this script's [`MastForest`]: decorators, error codes, and
161    /// procedure names.
162    ///
163    /// See [`MastForest::clear_debug_info`] for more details.
164    pub fn clear_debug_info(&mut self) {
165        let mut mast = self.mast.clone();
166        Arc::make_mut(&mut mast).clear_debug_info();
167        self.mast = mast;
168    }
169
170    /// Returns a new [NoteScript] with the provided advice map entries merged into the
171    /// underlying [MastForest].
172    ///
173    /// This allows adding advice map entries to an already-compiled note script,
174    /// which is useful when the entries are determined after script compilation.
175    pub fn with_advice_map(self, advice_map: AdviceMap) -> Self {
176        if advice_map.is_empty() {
177            return self;
178        }
179
180        let mut mast = (*self.mast).clone();
181        mast.advice_map_mut().extend(advice_map);
182        Self {
183            mast: Arc::new(mast),
184            entrypoint: self.entrypoint,
185        }
186    }
187}
188
189// CONVERSIONS INTO NOTE SCRIPT
190// ================================================================================================
191
192impl From<&NoteScript> for Vec<Felt> {
193    fn from(script: &NoteScript) -> Self {
194        let mut bytes = script.mast.to_bytes();
195        let len = bytes.len();
196
197        // Pad the data so that it can be encoded with u32
198        let missing = if !len.is_multiple_of(4) { 4 - (len % 4) } else { 0 };
199        bytes.resize(bytes.len() + missing, 0);
200
201        let final_size = 2 + bytes.len();
202        let mut result = Vec::with_capacity(final_size);
203
204        // Push the length, this is used to remove the padding later
205        result.push(Felt::from(u32::from(script.entrypoint)));
206        result.push(Felt::new(len as u64));
207
208        // A Felt can not represent all u64 values, so the data is encoded using u32.
209        let mut encoded: &[u8] = &bytes;
210        while encoded.len() >= 4 {
211            let (data, rest) =
212                encoded.split_first_chunk::<4>().expect("The length has been checked");
213            let number = u32::from_le_bytes(*data);
214            result.push(Felt::new(number.into()));
215
216            encoded = rest;
217        }
218
219        result
220    }
221}
222
223impl From<NoteScript> for Vec<Felt> {
224    fn from(value: NoteScript) -> Self {
225        (&value).into()
226    }
227}
228
229impl AsRef<NoteScript> for NoteScript {
230    fn as_ref(&self) -> &NoteScript {
231        self
232    }
233}
234
235// CONVERSIONS FROM NOTE SCRIPT
236// ================================================================================================
237
238impl TryFrom<&[Felt]> for NoteScript {
239    type Error = DeserializationError;
240
241    fn try_from(elements: &[Felt]) -> Result<Self, Self::Error> {
242        if elements.len() < 2 {
243            return Err(DeserializationError::UnexpectedEOF);
244        }
245
246        let entrypoint: u32 = elements[0]
247            .as_canonical_u64()
248            .try_into()
249            .map_err(|err: TryFromIntError| DeserializationError::InvalidValue(err.to_string()))?;
250        let len = elements[1].as_canonical_u64();
251        let mut data = Vec::with_capacity(elements.len() * 4);
252
253        for &felt in &elements[2..] {
254            let element: u32 =
255                felt.as_canonical_u64().try_into().map_err(|err: TryFromIntError| {
256                    DeserializationError::InvalidValue(err.to_string())
257                })?;
258            data.extend(element.to_le_bytes())
259        }
260        data.shrink_to(len as usize);
261
262        // TODO: Use UntrustedMastForest and check where else we deserialize mast forests.
263        let mast = MastForest::read_from_bytes(&data)?;
264        let entrypoint = MastNodeId::from_u32_safe(entrypoint, &mast)?;
265        Ok(NoteScript::from_parts(Arc::new(mast), entrypoint))
266    }
267}
268
269impl TryFrom<Vec<Felt>> for NoteScript {
270    type Error = DeserializationError;
271
272    fn try_from(value: Vec<Felt>) -> Result<Self, Self::Error> {
273        value.as_slice().try_into()
274    }
275}
276
277// SERIALIZATION
278// ================================================================================================
279
280impl Serializable for NoteScript {
281    fn write_into<W: ByteWriter>(&self, target: &mut W) {
282        self.mast.write_into(target);
283        target.write_u32(u32::from(self.entrypoint));
284    }
285
286    fn get_size_hint(&self) -> usize {
287        // TODO: this is a temporary workaround. Replace mast.to_bytes().len() with
288        // MastForest::get_size_hint() (or a similar size-hint API) once it becomes
289        // available.
290        let mast_size = self.mast.to_bytes().len();
291        let u32_size = 0u32.get_size_hint();
292
293        mast_size + u32_size
294    }
295}
296
297impl Deserializable for NoteScript {
298    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
299        let mast = MastForest::read_from(source)?;
300        let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?;
301
302        Ok(Self::from_parts(Arc::new(mast), entrypoint))
303    }
304}
305
306// PRETTY-PRINTING
307// ================================================================================================
308
309impl PrettyPrint for NoteScript {
310    fn render(&self) -> miden_core::prettier::Document {
311        use miden_core::prettier::*;
312        let entrypoint = self.mast[self.entrypoint].to_pretty_print(&self.mast);
313
314        indent(4, const_text("begin") + nl() + entrypoint.render()) + nl() + const_text("end")
315    }
316}
317
318impl Display for NoteScript {
319    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
320        self.pretty_print(f)
321    }
322}
323
324// HELPER FUNCTIONS
325// ================================================================================================
326
327/// Creates a minimal [MastForest] containing only an external node referencing the given digest.
328///
329/// This is useful for creating lightweight references to procedures without copying entire
330/// libraries. The external reference will be resolved at runtime, assuming the source library
331/// is loaded into the VM's MastForestStore.
332fn create_external_node_forest(digest: Word) -> (MastForest, MastNodeId) {
333    let mut mast = MastForest::new();
334    let node_id = ExternalNodeBuilder::new(digest)
335        .add_to_forest(&mut mast)
336        .expect("adding external node to empty forest should not fail");
337    mast.make_root(node_id);
338    (mast, node_id)
339}
340
341// TESTS
342// ================================================================================================
343
344#[cfg(test)]
345mod tests {
346    use super::{Felt, NoteScript, Vec};
347    use crate::assembly::Assembler;
348    use crate::testing::note::DEFAULT_NOTE_CODE;
349
350    #[test]
351    fn test_note_script_to_from_felt() {
352        let assembler = Assembler::default();
353        let script_src = DEFAULT_NOTE_CODE;
354        let program = assembler.assemble_program(script_src).unwrap();
355        let note_script = NoteScript::new(program);
356
357        let encoded: Vec<Felt> = (&note_script).into();
358        let decoded: NoteScript = encoded.try_into().unwrap();
359
360        assert_eq!(note_script, decoded);
361    }
362
363    #[test]
364    fn test_note_script_with_advice_map() {
365        use miden_core::advice::AdviceMap;
366
367        use crate::Word;
368
369        let assembler = Assembler::default();
370        let program = assembler.assemble_program("begin nop end").unwrap();
371        let script = NoteScript::new(program);
372
373        assert!(script.mast().advice_map().is_empty());
374
375        // Empty advice map should be a no-op
376        let original_root = script.root();
377        let script = script.with_advice_map(AdviceMap::default());
378        assert_eq!(original_root, script.root());
379
380        // Non-empty advice map should add entries
381        let key = Word::from([5u32, 6, 7, 8]);
382        let value = vec![Felt::new(100)];
383        let mut advice_map = AdviceMap::default();
384        advice_map.insert(key, value.clone());
385
386        let script = script.with_advice_map(advice_map);
387
388        let mast = script.mast();
389        let stored = mast.advice_map().get(&key).expect("entry should be present");
390        assert_eq!(stored.as_ref(), value.as_slice());
391    }
392}