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