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