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::{MastArtifact, 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    /// # Arguments
146    ///
147    /// * `package` - The package containing the
148    ///   [`Executable`](miden_mast_package::MastArtifact::Executable) or
149    ///   [`Library`](miden_mast_package::MastArtifact::Library).
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if:
154    /// - The package contains a library which does not contain a procedure with the `@note_script`
155    ///   attribute.
156    /// - The package contains a library which contains multiple procedures with the `@note_script`
157    ///   attribute.
158    pub fn from_package(package: &Package) -> Result<Self, NoteError> {
159        match &package.mast {
160            // `NoteScript`s are compiled as executables by the miden compiler's
161            // cargo extension. Source, the "midenc_flags_from_target" function:
162            // https://github.com/0xMiden/compiler/blob/d3cd8cd4a2c1dfeae8a61643aa42734a35e3e840/tools/cargo-miden/src/commands/build.rs#L334
163            MastArtifact::Executable(executable) => {
164                let program = executable.as_ref().clone();
165
166                Ok(NoteScript::new(program))
167            },
168            MastArtifact::Library(library) => Ok(NoteScript::from_library(library))?,
169        }
170    }
171
172    // PUBLIC ACCESSORS
173    // --------------------------------------------------------------------------------------------
174
175    /// Returns the commitment of this note script (i.e., the script's MAST root).
176    pub fn root(&self) -> Word {
177        self.mast[self.entrypoint].digest()
178    }
179
180    /// Returns a reference to the [MastForest] backing this note script.
181    pub fn mast(&self) -> Arc<MastForest> {
182        self.mast.clone()
183    }
184
185    /// Returns an entrypoint node ID of the current script.
186    pub fn entrypoint(&self) -> MastNodeId {
187        self.entrypoint
188    }
189
190    /// Clears all debug info from this script's [`MastForest`]: decorators, error codes, and
191    /// procedure names.
192    ///
193    /// See [`MastForest::clear_debug_info`] for more details.
194    pub fn clear_debug_info(&mut self) {
195        let mut mast = self.mast.clone();
196        Arc::make_mut(&mut mast).clear_debug_info();
197        self.mast = mast;
198    }
199
200    /// Returns a new [NoteScript] with the provided advice map entries merged into the
201    /// underlying [MastForest].
202    ///
203    /// This allows adding advice map entries to an already-compiled note script,
204    /// which is useful when the entries are determined after script compilation.
205    pub fn with_advice_map(self, advice_map: AdviceMap) -> Self {
206        if advice_map.is_empty() {
207            return self;
208        }
209
210        let mut mast = (*self.mast).clone();
211        mast.advice_map_mut().extend(advice_map);
212        Self {
213            mast: Arc::new(mast),
214            entrypoint: self.entrypoint,
215        }
216    }
217}
218
219// CONVERSIONS INTO NOTE SCRIPT
220// ================================================================================================
221
222impl From<&NoteScript> for Vec<Felt> {
223    fn from(script: &NoteScript) -> Self {
224        let mut bytes = script.mast.to_bytes();
225        let len = bytes.len();
226
227        // Pad the data so that it can be encoded with u32
228        let missing = if !len.is_multiple_of(4) { 4 - (len % 4) } else { 0 };
229        bytes.resize(bytes.len() + missing, 0);
230
231        let final_size = 2 + bytes.len();
232        let mut result = Vec::with_capacity(final_size);
233
234        // Push the length, this is used to remove the padding later
235        result.push(Felt::from(u32::from(script.entrypoint)));
236        result.push(Felt::new(len as u64));
237
238        // A Felt can not represent all u64 values, so the data is encoded using u32.
239        let mut encoded: &[u8] = &bytes;
240        while encoded.len() >= 4 {
241            let (data, rest) =
242                encoded.split_first_chunk::<4>().expect("The length has been checked");
243            let number = u32::from_le_bytes(*data);
244            result.push(Felt::new(number.into()));
245
246            encoded = rest;
247        }
248
249        result
250    }
251}
252
253impl From<NoteScript> for Vec<Felt> {
254    fn from(value: NoteScript) -> Self {
255        (&value).into()
256    }
257}
258
259impl AsRef<NoteScript> for NoteScript {
260    fn as_ref(&self) -> &NoteScript {
261        self
262    }
263}
264
265// CONVERSIONS FROM NOTE SCRIPT
266// ================================================================================================
267
268impl TryFrom<&[Felt]> for NoteScript {
269    type Error = DeserializationError;
270
271    fn try_from(elements: &[Felt]) -> Result<Self, Self::Error> {
272        if elements.len() < 2 {
273            return Err(DeserializationError::UnexpectedEOF);
274        }
275
276        let entrypoint: u32 = elements[0]
277            .as_canonical_u64()
278            .try_into()
279            .map_err(|err: TryFromIntError| DeserializationError::InvalidValue(err.to_string()))?;
280        let len = elements[1].as_canonical_u64();
281        let mut data = Vec::with_capacity(elements.len() * 4);
282
283        for &felt in &elements[2..] {
284            let element: u32 =
285                felt.as_canonical_u64().try_into().map_err(|err: TryFromIntError| {
286                    DeserializationError::InvalidValue(err.to_string())
287                })?;
288            data.extend(element.to_le_bytes())
289        }
290        data.shrink_to(len as usize);
291
292        // TODO: Use UntrustedMastForest and check where else we deserialize mast forests.
293        let mast = MastForest::read_from_bytes(&data)?;
294        let entrypoint = MastNodeId::from_u32_safe(entrypoint, &mast)?;
295        Ok(NoteScript::from_parts(Arc::new(mast), entrypoint))
296    }
297}
298
299impl TryFrom<Vec<Felt>> for NoteScript {
300    type Error = DeserializationError;
301
302    fn try_from(value: Vec<Felt>) -> Result<Self, Self::Error> {
303        value.as_slice().try_into()
304    }
305}
306
307// SERIALIZATION
308// ================================================================================================
309
310impl Serializable for NoteScript {
311    fn write_into<W: ByteWriter>(&self, target: &mut W) {
312        self.mast.write_into(target);
313        target.write_u32(u32::from(self.entrypoint));
314    }
315
316    fn get_size_hint(&self) -> usize {
317        // TODO: this is a temporary workaround. Replace mast.to_bytes().len() with
318        // MastForest::get_size_hint() (or a similar size-hint API) once it becomes
319        // available.
320        let mast_size = self.mast.to_bytes().len();
321        let u32_size = 0u32.get_size_hint();
322
323        mast_size + u32_size
324    }
325}
326
327impl Deserializable for NoteScript {
328    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
329        let mast = MastForest::read_from(source)?;
330        let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?;
331
332        Ok(Self::from_parts(Arc::new(mast), entrypoint))
333    }
334}
335
336// PRETTY-PRINTING
337// ================================================================================================
338
339impl PrettyPrint for NoteScript {
340    fn render(&self) -> miden_core::prettier::Document {
341        use miden_core::prettier::*;
342        let entrypoint = self.mast[self.entrypoint].to_pretty_print(&self.mast);
343
344        indent(4, const_text("begin") + nl() + entrypoint.render()) + nl() + const_text("end")
345    }
346}
347
348impl Display for NoteScript {
349    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
350        self.pretty_print(f)
351    }
352}
353
354// HELPER FUNCTIONS
355// ================================================================================================
356
357/// Creates a minimal [MastForest] containing only an external node referencing the given digest.
358///
359/// This is useful for creating lightweight references to procedures without copying entire
360/// libraries. The external reference will be resolved at runtime, assuming the source library
361/// is loaded into the VM's MastForestStore.
362fn create_external_node_forest(digest: Word) -> (MastForest, MastNodeId) {
363    let mut mast = MastForest::new();
364    let node_id = ExternalNodeBuilder::new(digest)
365        .add_to_forest(&mut mast)
366        .expect("adding external node to empty forest should not fail");
367    mast.make_root(node_id);
368    (mast, node_id)
369}
370
371// TESTS
372// ================================================================================================
373
374#[cfg(test)]
375mod tests {
376    use super::{Felt, NoteScript, Vec};
377    use crate::assembly::Assembler;
378    use crate::testing::note::DEFAULT_NOTE_CODE;
379
380    #[test]
381    fn test_note_script_to_from_felt() {
382        let assembler = Assembler::default();
383        let script_src = DEFAULT_NOTE_CODE;
384        let program = assembler.assemble_program(script_src).unwrap();
385        let note_script = NoteScript::new(program);
386
387        let encoded: Vec<Felt> = (&note_script).into();
388        let decoded: NoteScript = encoded.try_into().unwrap();
389
390        assert_eq!(note_script, decoded);
391    }
392
393    #[test]
394    fn test_note_script_with_advice_map() {
395        use miden_core::advice::AdviceMap;
396
397        use crate::Word;
398
399        let assembler = Assembler::default();
400        let program = assembler.assemble_program("begin nop end").unwrap();
401        let script = NoteScript::new(program);
402
403        assert!(script.mast().advice_map().is_empty());
404
405        // Empty advice map should be a no-op
406        let original_root = script.root();
407        let script = script.with_advice_map(AdviceMap::default());
408        assert_eq!(original_root, script.root());
409
410        // Non-empty advice map should add entries
411        let key = Word::from([5u32, 6, 7, 8]);
412        let value = vec![Felt::new(100)];
413        let mut advice_map = AdviceMap::default();
414        advice_map.insert(key, value.clone());
415
416        let script = script.with_advice_map(advice_map);
417
418        let mast = script.mast();
419        let stored = mast.advice_map().get(&key).expect("entry should be present");
420        assert_eq!(stored.as_ref(), value.as_slice());
421    }
422}