Skip to main content

miden_protocol/note/
script.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3use core::fmt::Display;
4
5use miden_core::mast::MastNodeExt;
6use miden_crypto_derive::WordWrapper;
7use miden_mast_package::Package;
8use miden_processor::LoadedMastForest;
9
10use super::Felt;
11use crate::assembly::Path;
12use crate::assembly::mast::{MastForest, MastNodeId};
13use crate::crypto::utils::{bytes_to_elements_with_padding, padded_elements_to_bytes};
14use crate::errors::NoteError;
15use crate::script::MastForestScript;
16use crate::utils::serde::{
17    ByteReader,
18    ByteWriter,
19    Deserializable,
20    DeserializationError,
21    Serializable,
22};
23use crate::vm::AdviceMap;
24use crate::{PrettyPrint, Word};
25
26/// The attribute name used to mark the entrypoint procedure in a note script package.
27const NOTE_SCRIPT_ATTRIBUTE: &str = "note_script";
28
29// NOTE SCRIPT ROOT
30// ================================================================================================
31
32/// The MAST root of a [`NoteScript`].
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)]
34pub struct NoteScriptRoot(Word);
35
36impl From<NoteScriptRoot> for Word {
37    fn from(root: NoteScriptRoot) -> Self {
38        root.0
39    }
40}
41
42impl Display for NoteScriptRoot {
43    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44        Display::fmt(&self.0, f)
45    }
46}
47
48impl Serializable for NoteScriptRoot {
49    fn write_into<W: ByteWriter>(&self, target: &mut W) {
50        target.write(self.0);
51    }
52
53    fn get_size_hint(&self) -> usize {
54        self.0.get_size_hint()
55    }
56}
57
58impl Deserializable for NoteScriptRoot {
59    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
60        let word: Word = source.read()?;
61        Ok(Self::from_raw(word))
62    }
63}
64
65// NOTE SCRIPT
66// ================================================================================================
67
68/// An executable program of a note.
69///
70/// A note's script represents a program which must be executed for a note to be consumed. As such
71/// it defines the rules and side effects of consuming a given note.
72#[derive(Debug, Clone)]
73pub struct NoteScript(MastForestScript);
74
75impl NoteScript {
76    // CONSTRUCTORS
77    // --------------------------------------------------------------------------------------------
78
79    /// Returns a new [NoteScript] deserialized from the provided bytes.
80    ///
81    /// # Errors
82    /// Returns an error if note script deserialization fails.
83    pub fn from_bytes(bytes: &[u8]) -> Result<Self, NoteError> {
84        Self::read_from_bytes(bytes).map_err(NoteError::NoteScriptDeserializationError)
85    }
86
87    /// Returns a new [NoteScript] instantiated from the provided components.
88    ///
89    /// # Errors
90    /// Returns an error if the specified entrypoint is not in the provided MAST forest.
91    pub fn from_parts(mast: Arc<MastForest>, entrypoint: MastNodeId) -> Result<Self, NoteError> {
92        MastForestScript::from_parts(mast, entrypoint)
93            .map_err(NoteError::MastForestScript)
94            .map(Self)
95    }
96
97    /// Returns a new [NoteScript] instantiated from the provided package.
98    ///
99    /// The package must contain exactly one procedure with the `@note_script` attribute,
100    /// which will be used as the entrypoint.
101    ///
102    /// # Errors
103    /// Returns an error if:
104    /// - The package is an executable (i.e., its target type is
105    ///   [`TargetType::Executable`](miden_mast_package::TargetType::Executable)).
106    /// - The package does not contain a procedure with the `@note_script` attribute.
107    /// - The package contains multiple procedures with the `@note_script` attribute.
108    pub fn from_package(package: &Package) -> Result<Self, NoteError> {
109        let script = MastForestScript::from_package(package, NOTE_SCRIPT_ATTRIBUTE)
110            .map_err(NoteError::MastForestScript)?;
111        Ok(Self(script))
112    }
113
114    /// Returns a new [NoteScript] containing only a reference to a procedure in the provided
115    /// package.
116    ///
117    /// This method is useful when a package contains multiple note scripts and you need to
118    /// extract a specific one by its fully qualified path (e.g.,
119    /// `miden::standards::notes::burn::main`).
120    ///
121    /// The procedure at the specified path must have the `@note_script` attribute.
122    ///
123    /// Note: This method creates a minimal [MastForest] containing only an external node
124    /// referencing the procedure's digest, rather than copying the entire package. The actual
125    /// procedure code will be resolved at runtime via the `MastForestStore`.
126    ///
127    /// # Errors
128    /// Returns an error if:
129    /// - The package does not contain a procedure at the specified path.
130    /// - The procedure at the specified path does not have the `@note_script` attribute.
131    pub fn from_package_reference(package: &Package, path: &Path) -> Result<Self, NoteError> {
132        let script = MastForestScript::from_package_reference(package, path, NOTE_SCRIPT_ATTRIBUTE)
133            .map_err(NoteError::MastForestScript)?;
134        Ok(Self(script))
135    }
136
137    // PUBLIC ACCESSORS
138    // --------------------------------------------------------------------------------------------
139
140    /// Returns the commitment of this note script (i.e., the script's MAST root).
141    pub fn root(&self) -> NoteScriptRoot {
142        NoteScriptRoot::from_raw(self.0.digest())
143    }
144
145    /// Returns a reference to the [MastForest] backing this note script.
146    pub fn mast(&self) -> Arc<MastForest> {
147        self.0.mast()
148    }
149
150    /// Returns the MAST forest and package-owned debug information backing this note script.
151    pub fn loaded_mast_forest(&self) -> LoadedMastForest {
152        self.0.loaded_mast_forest()
153    }
154
155    /// Returns an entrypoint node ID of the current script.
156    pub fn entrypoint(&self) -> MastNodeId {
157        self.0.entrypoint()
158    }
159
160    /// Removes debug info from this note script, if any.
161    pub fn clear_debug_info(&mut self) {
162        self.0.clear_debug_info();
163    }
164
165    /// Returns a new [NoteScript] with the provided advice map entries merged into the
166    /// underlying [MastForest].
167    ///
168    /// This allows adding advice map entries to an already-compiled note script,
169    /// which is useful when the entries are determined after script compilation.
170    pub fn with_advice_map(self, advice_map: AdviceMap) -> Self {
171        Self(self.0.with_advice_map(advice_map))
172    }
173
174    // CONVERSIONS
175    // --------------------------------------------------------------------------------------------
176
177    /// Returns the encoding of this note script as field elements.
178    ///
179    /// The serialized script is packed into field elements, 7 bytes per element.
180    pub fn to_elements(&self) -> Vec<Felt> {
181        bytes_to_elements_with_padding(&self.to_bytes())
182    }
183
184    /// Decodes a [`NoteScript`] from the elements produced by [`NoteScript::to_elements`].
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if `elements` do not encode a valid note script.
189    pub fn try_from_elements(elements: &[Felt]) -> Result<Self, DeserializationError> {
190        let bytes = padded_elements_to_bytes(elements).ok_or_else(|| {
191            DeserializationError::InvalidValue("encoded note script is not padded".into())
192        })?;
193
194        Self::read_from_bytes(&bytes)
195    }
196}
197
198impl PartialEq for NoteScript {
199    fn eq(&self, other: &Self) -> bool {
200        self.0 == other.0
201    }
202}
203
204impl Eq for NoteScript {}
205
206impl AsRef<NoteScript> for NoteScript {
207    fn as_ref(&self) -> &NoteScript {
208        self
209    }
210}
211
212// SERIALIZATION
213// ================================================================================================
214
215impl Serializable for NoteScript {
216    fn write_into<W: ByteWriter>(&self, target: &mut W) {
217        self.0.write_into(target);
218    }
219
220    fn get_size_hint(&self) -> usize {
221        self.0.get_size_hint()
222    }
223}
224
225impl Deserializable for NoteScript {
226    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
227        Ok(Self(MastForestScript::read_from(source)?))
228    }
229}
230
231// PRETTY-PRINTING
232// ================================================================================================
233
234impl PrettyPrint for NoteScript {
235    fn render(&self) -> miden_core::prettier::Document {
236        use miden_core::prettier::*;
237        let mast = self.0.mast();
238        let entrypoint = mast[self.0.entrypoint()].to_pretty_print(&mast);
239
240        indent(4, const_text("begin") + nl() + entrypoint.render()) + nl() + const_text("end")
241    }
242}
243
244impl Display for NoteScript {
245    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
246        self.pretty_print(f)
247    }
248}
249
250// TESTS
251// ================================================================================================
252
253#[cfg(test)]
254mod tests {
255
256    use super::{Felt, NoteScript};
257    use crate::testing::assembler::assemble_test_package;
258    use crate::testing::note::DEFAULT_NOTE_SCRIPT;
259
260    #[test]
261    fn test_note_script_to_from_mast_elements() -> anyhow::Result<()> {
262        let package = assemble_test_package(
263            "test-note-script-roundtrip",
264            "test::note_roundtrip",
265            DEFAULT_NOTE_SCRIPT,
266        );
267        let note_script = NoteScript::from_package(&package)?;
268
269        let encoded = note_script.to_elements();
270        let decoded = NoteScript::try_from_elements(&encoded)?;
271        assert_eq!(note_script, decoded);
272
273        Ok(())
274    }
275
276    #[test]
277    fn test_note_script_preserves_package_debug_info() {
278        let package = assemble_test_package(
279            "test-note-script-debug-info",
280            "test::note_debug_info",
281            DEFAULT_NOTE_SCRIPT,
282        );
283        let note_script = NoteScript::from_package(&package).unwrap();
284
285        assert!(note_script.loaded_mast_forest().package_debug_info().unwrap().is_some());
286    }
287
288    #[test]
289    fn test_note_script_with_advice_map() {
290        use miden_core::advice::AdviceMap;
291
292        use crate::Word;
293
294        let package = assemble_test_package(
295            "test-note-script-with-advice-map",
296            "test::note_with_advice_map",
297            DEFAULT_NOTE_SCRIPT,
298        );
299        let script = NoteScript::from_package(&package).unwrap();
300
301        assert!(script.mast().advice_map().is_empty());
302
303        // Empty advice map should be a no-op
304        let original_root = script.root();
305        let script = script.with_advice_map(AdviceMap::default());
306        assert_eq!(original_root, script.root());
307
308        // Non-empty advice map should add entries
309        let key = Word::from([5u32, 6, 7, 8]);
310        let value = vec![Felt::new_unchecked(100)];
311        let mut advice_map = AdviceMap::default();
312        advice_map.insert(key, value.clone());
313
314        let script = script.with_advice_map(advice_map);
315
316        let mast = script.mast();
317        let stored = mast.advice_map().get(&key).expect("entry should be present");
318        assert_eq!(stored.as_ref(), value.as_slice());
319    }
320
321    #[test]
322    fn test_note_script_from_executable_package() {
323        use assert_matches::assert_matches;
324
325        use crate::assembly::Assembler;
326        use crate::errors::NoteError;
327        use crate::script::MastForestScriptError;
328
329        // an executable package is rejected: note scripts are identified only by the @note_script
330        // attribute
331        let package = Assembler::default()
332            .assemble_program("test-note-script-executable", "begin nop end")
333            .unwrap();
334        assert_matches!(
335            NoteScript::from_package(&package),
336            Err(NoteError::MastForestScript(MastForestScriptError::ExecutablePackage))
337        );
338    }
339}