Skip to main content

miden_protocol/note/
script.rs

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