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