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