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