Skip to main content

miden_protocol/transaction/
script.rs

1use alloc::sync::Arc;
2use core::fmt::Display;
3
4use miden_crypto_derive::WordWrapper;
5use miden_mast_package::Package;
6use miden_processor::LoadedMastForest;
7
8use crate::Word;
9use crate::assembly::Path;
10use crate::assembly::mast::{MastForest, MastNodeId};
11use crate::script::{MastForestScript, MastForestScriptError};
12use crate::utils::serde::{
13    ByteReader,
14    ByteWriter,
15    Deserializable,
16    DeserializationError,
17    Serializable,
18};
19use crate::vm::AdviceMap;
20
21// TRANSACTION SCRIPT ROOT
22// ================================================================================================
23
24/// The MAST root of a [`TransactionScript`].
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)]
26pub struct TransactionScriptRoot(Word);
27
28impl From<TransactionScriptRoot> for Word {
29    fn from(root: TransactionScriptRoot) -> Self {
30        root.0
31    }
32}
33
34impl Display for TransactionScriptRoot {
35    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36        Display::fmt(&self.0, f)
37    }
38}
39
40impl Serializable for TransactionScriptRoot {
41    fn write_into<W: ByteWriter>(&self, target: &mut W) {
42        target.write(self.0);
43    }
44
45    fn get_size_hint(&self) -> usize {
46        self.0.get_size_hint()
47    }
48}
49
50impl Deserializable for TransactionScriptRoot {
51    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
52        let word: Word = source.read()?;
53        Ok(Self::from_raw(word))
54    }
55}
56
57// TRANSACTION SCRIPT
58// ================================================================================================
59
60/// The attribute name used to mark the entrypoint procedure in a transaction script package.
61pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script";
62
63/// Transaction script.
64///
65/// A transaction script is a program that is executed in a transaction after all input notes
66/// have been executed.
67///
68/// The [TransactionScript] object is composed of an executable program defined by a [MastForest]
69/// and an associated entrypoint.
70#[derive(Clone, Debug)]
71pub struct TransactionScript(MastForestScript);
72
73impl TransactionScript {
74    // CONSTRUCTORS
75    // --------------------------------------------------------------------------------------------
76
77    /// Returns a new [TransactionScript] instantiated from the provided MAST forest and entrypoint.
78    ///
79    /// # Errors
80    /// Returns an error if the specified entrypoint is not in the provided MAST forest.
81    pub fn from_parts(
82        mast: Arc<MastForest>,
83        entrypoint: MastNodeId,
84    ) -> Result<Self, MastForestScriptError> {
85        MastForestScript::from_parts(mast, entrypoint).map(Self)
86    }
87
88    /// Creates a [TransactionScript] from a [`Package`].
89    ///
90    /// The package must contain exactly one procedure with the `@transaction_script` attribute,
91    /// which will be used as the entrypoint.
92    ///
93    /// # Errors
94    /// Returns an error if:
95    /// - The package is an executable (i.e., its target type is
96    ///   [`TargetType::Executable`](miden_mast_package::TargetType::Executable)).
97    /// - The package does not contain a procedure with the `@transaction_script` attribute.
98    /// - The package contains multiple procedures with the `@transaction_script` attribute.
99    pub fn from_package(package: &Package) -> Result<Self, MastForestScriptError> {
100        MastForestScript::from_package(package, TRANSACTION_SCRIPT_ATTRIBUTE).map(Self)
101    }
102
103    /// Returns a new [TransactionScript] containing only a reference to a procedure in the
104    /// provided package.
105    ///
106    /// This method is useful when a package contains multiple transaction scripts and you need
107    /// to extract a specific one by its fully qualified path (e.g.,
108    /// `::miden::standards::tx_scripts::send_notes::main`).
109    ///
110    /// The procedure at the specified path must have the `@transaction_script` attribute.
111    ///
112    /// Note: This method creates a minimal [MastForest] containing only an external node
113    /// referencing the procedure's digest, rather than copying the entire package. The actual
114    /// procedure code will be resolved at runtime via the `MastForestStore`.
115    ///
116    /// # Errors
117    /// Returns an error if:
118    /// - The package does not contain a procedure at the specified path.
119    /// - The procedure at the specified path does not have the `@transaction_script` attribute.
120    pub fn from_package_reference(
121        package: &Package,
122        path: &Path,
123    ) -> Result<Self, MastForestScriptError> {
124        MastForestScript::from_package_reference(package, path, TRANSACTION_SCRIPT_ATTRIBUTE)
125            .map(Self)
126    }
127
128    // PUBLIC ACCESSORS
129    // --------------------------------------------------------------------------------------------
130
131    /// Returns a reference to the [MastForest] backing this transaction script.
132    pub fn mast(&self) -> Arc<MastForest> {
133        self.0.mast()
134    }
135
136    /// Returns the MAST forest and package-owned debug information backing this transaction script.
137    pub fn loaded_mast_forest(&self) -> LoadedMastForest {
138        self.0.loaded_mast_forest()
139    }
140
141    /// Returns the commitment of this transaction script (i.e., the script's MAST root).
142    pub fn root(&self) -> TransactionScriptRoot {
143        TransactionScriptRoot::from_raw(self.0.digest())
144    }
145
146    /// Returns the entrypoint node ID of this transaction script.
147    pub fn entrypoint(&self) -> MastNodeId {
148        self.0.entrypoint()
149    }
150
151    /// Returns a new [TransactionScript] with the provided advice map entries merged into the
152    /// underlying [MastForest].
153    ///
154    /// This allows adding advice map entries to an already-compiled transaction script,
155    /// which is useful when the entries are determined after script compilation.
156    pub fn with_advice_map(self, advice_map: AdviceMap) -> Self {
157        Self(self.0.with_advice_map(advice_map))
158    }
159}
160
161impl PartialEq for TransactionScript {
162    fn eq(&self, other: &Self) -> bool {
163        self.0 == other.0
164    }
165}
166
167#[cfg(test)]
168mod entrypoint_tests {
169    use alloc::sync::Arc;
170
171    use super::TransactionScript;
172    use crate::Word;
173    use crate::utils::create_external_node_forest;
174
175    #[test]
176    fn entrypoint_returns_the_script_entrypoint() {
177        let (mast, entrypoint) = create_external_node_forest(Word::empty());
178        let script = TransactionScript::from_parts(Arc::new(mast), entrypoint)
179            .expect("test MAST forest should contain its entrypoint");
180
181        assert_eq!(script.entrypoint(), entrypoint);
182    }
183}
184
185impl Eq for TransactionScript {}
186
187// SERIALIZATION
188// ================================================================================================
189
190impl Serializable for TransactionScript {
191    fn write_into<W: ByteWriter>(&self, target: &mut W) {
192        self.0.write_into(target);
193    }
194
195    fn get_size_hint(&self) -> usize {
196        self.0.get_size_hint()
197    }
198}
199
200impl Deserializable for TransactionScript {
201    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
202        Ok(Self(MastForestScript::read_from(source)?))
203    }
204}
205
206// TESTS
207// ================================================================================================
208
209#[cfg(test)]
210mod tests {
211    use miden_core::advice::AdviceMap;
212
213    use super::TransactionScript;
214
215    /// A minimal transaction script source with a single `@transaction_script` procedure.
216    const TX_SCRIPT_SOURCE: &str = "
217        @transaction_script
218        pub proc main
219            push.1 drop
220        end
221    ";
222
223    #[test]
224    fn test_transaction_script_preserves_package_debug_info() {
225        use crate::testing::assembler::assemble_test_package;
226
227        let package = assemble_test_package(
228            "test-tx-script-debug-info",
229            "test::tx_script_debug_info",
230            TX_SCRIPT_SOURCE,
231        );
232        let script = TransactionScript::from_package(&package).unwrap();
233
234        assert!(script.loaded_mast_forest().package_debug_info().unwrap().is_some());
235    }
236
237    #[test]
238    fn test_transaction_script_with_advice_map() {
239        use miden_core::{Felt, Word};
240
241        use crate::testing::assembler::assemble_test_package;
242
243        let package = assemble_test_package(
244            "test-tx-script-with-advice-map",
245            "test::tx_script_with_advice_map",
246            TX_SCRIPT_SOURCE,
247        );
248        let script = TransactionScript::from_package(&package).unwrap();
249        assert!(script.mast().advice_map().is_empty());
250
251        // Empty advice map should be a no-op
252        let original_root = script.root();
253        let script = script.with_advice_map(AdviceMap::default());
254        assert_eq!(original_root, script.root());
255
256        // Non-empty advice map should add entries
257        let key = Word::from([1u32, 2, 3, 4]);
258        let value = vec![Felt::new_unchecked(42), Felt::new_unchecked(43)];
259        let mut advice_map = AdviceMap::default();
260        advice_map.insert(key, value.clone());
261
262        let script = script.with_advice_map(advice_map);
263
264        let mast = script.mast();
265        let stored = mast.advice_map().get(&key).expect("entry should be present");
266        assert_eq!(stored.as_ref(), value.as_slice());
267    }
268
269    #[test]
270    fn test_transaction_script_from_library_package() {
271        use assert_matches::assert_matches;
272
273        use crate::script::MastForestScriptError;
274        use crate::testing::assembler::assemble_test_package;
275        use crate::utils::serde::{Deserializable, Serializable};
276
277        let package = assemble_test_package("test-tx-script", "test::tx_script", TX_SCRIPT_SOURCE);
278
279        let script = TransactionScript::from_package(&package).unwrap();
280
281        // the script must round-trip through serialization unchanged
282        let bytes = script.to_bytes();
283        let decoded = TransactionScript::read_from_bytes(&bytes).unwrap();
284        assert_eq!(script, decoded);
285
286        // a package without the attribute is rejected
287        let no_attr = assemble_test_package(
288            "test-tx-script-no-attr",
289            "test::tx_script_no_attr",
290            "pub proc main push.1 drop end",
291        );
292        assert_matches!(
293            TransactionScript::from_package(&no_attr),
294            Err(MastForestScriptError::NoProcedureWithAttribute(_))
295        );
296
297        // a package with multiple tagged procedures is rejected
298        let multiple = assemble_test_package(
299            "test-tx-script-multiple",
300            "test::tx_script_multiple",
301            "@transaction_script pub proc main_a push.1 drop end
302             @transaction_script pub proc main_b push.2 drop end",
303        );
304        assert_matches!(
305            TransactionScript::from_package(&multiple),
306            Err(MastForestScriptError::MultipleProceduresWithAttribute(_))
307        );
308    }
309
310    #[test]
311    fn test_transaction_script_from_executable_package() {
312        use assert_matches::assert_matches;
313
314        use crate::assembly::Assembler;
315        use crate::script::MastForestScriptError;
316
317        // an executable package is rejected: transaction scripts are identified only by the
318        // @transaction_script attribute
319        let package = Assembler::default()
320            .assemble_program("test-tx-script-executable", "begin nop end")
321            .unwrap();
322        assert_matches!(
323            TransactionScript::from_package(&package),
324            Err(MastForestScriptError::ExecutablePackage)
325        );
326    }
327
328    #[test]
329    fn test_transaction_script_from_package_reference() {
330        use alloc::string::ToString;
331
332        use assert_matches::assert_matches;
333
334        use crate::Word;
335        use crate::assembly::Path;
336        use crate::script::MastForestScriptError;
337        use crate::testing::assembler::assemble_test_package;
338
339        let source = "
340            @transaction_script
341            pub proc main_a
342                push.1 drop
343            end
344
345            @transaction_script
346            pub proc main_b
347                push.2 drop
348            end
349
350            pub proc helper
351                push.3 drop
352            end
353        ";
354        let package =
355            assemble_test_package("test-tx-script-reference", "test::tx_script_reference", source);
356
357        // each tagged procedure can be extracted selectively, and the resulting script's root
358        // matches the digest of the referenced procedure
359        for proc_name in ["main_a", "main_b"] {
360            let export = package
361                .manifest
362                .exports()
363                .find(|e| e.path().as_ref().to_string().ends_with(proc_name))
364                .unwrap();
365            let digest = export.as_procedure().unwrap().digest;
366
367            let script =
368                TransactionScript::from_package_reference(&package, export.path().as_ref())
369                    .unwrap();
370            assert_eq!(Word::from(script.root()), digest);
371        }
372
373        // an unknown path is rejected
374        assert_matches!(
375            TransactionScript::from_package_reference(&package, Path::new("::foo::bar::main")),
376            Err(MastForestScriptError::ProcedureNotFound(_))
377        );
378
379        // a procedure without the attribute is rejected
380        let helper = package
381            .manifest
382            .exports()
383            .find(|e| e.path().as_ref().to_string().ends_with("helper"))
384            .unwrap();
385        assert_matches!(
386            TransactionScript::from_package_reference(&package, helper.path().as_ref()),
387            Err(MastForestScriptError::ProcedureMissingAttribute(_))
388        );
389    }
390}