Skip to main content

miden_protocol/transaction/
tx_args.rs

1use alloc::collections::BTreeMap;
2use alloc::string::{String, ToString};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5use core::fmt::Display;
6
7use miden_core::mast::MastNodeExt;
8use miden_crypto::merkle::InnerNodeInfo;
9use miden_crypto_derive::WordWrapper;
10use miden_mast_package::Package;
11use miden_mast_package::debug_info::PackageDebugInfo;
12use miden_processor::LoadedMastForest;
13
14use super::{Felt, Hasher, Word};
15use crate::account::auth::{PublicKeyCommitment, Signature};
16use crate::assembly::{Library, Path};
17use crate::errors::TransactionScriptError;
18use crate::note::{NoteId, NoteRecipient};
19use crate::package::{loaded_mast_forest, package_debug_info};
20use crate::utils::create_external_node_forest;
21use crate::utils::serde::{
22    ByteReader,
23    ByteWriter,
24    Deserializable,
25    DeserializationError,
26    Serializable,
27};
28use crate::vm::{AdviceInputs, AdviceMap, Program};
29use crate::{EMPTY_WORD, MastForest, MastNodeId};
30
31// TRANSACTION ARGUMENTS
32// ================================================================================================
33
34/// Optional transaction arguments.
35///
36/// - Transaction script: a program that is executed in a transaction after all input notes scripts
37///   have been executed.
38/// - Transaction script arguments: a [`Word`], which will be pushed to the operand stack before the
39///   transaction script execution. If these arguments are not specified, the [`EMPTY_WORD`] would
40///   be used as a default value. If the [AdviceInputs] are propagated with some user defined map
41///   entries, this script arguments word could be used as a key to access the corresponding value.
42/// - Note arguments: data put onto the stack right before a note script is executed. These are
43///   different from note storage, as the user executing the transaction can specify arbitrary note
44///   args.
45/// - Advice inputs: provides data needed by the runtime, like the details of public output notes.
46/// - Foreign account inputs: provides foreign account data that will be used during the foreign
47///   procedure invocation (FPI).
48/// - Auth arguments: data put onto the stack right before authentication procedure execution. If
49///   this argument is not specified, the [`EMPTY_WORD`] would be used as a default value. If the
50///   [AdviceInputs] are propagated with some user defined map entries, this argument could be used
51///   as a key to access the corresponding value.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct TransactionArgs {
54    tx_script: Option<TransactionScript>,
55    tx_script_args: Word,
56    note_args: BTreeMap<NoteId, Word>,
57    advice_inputs: AdviceInputs,
58    auth_args: Word,
59}
60
61impl TransactionArgs {
62    // CONSTRUCTORS
63    // --------------------------------------------------------------------------------------------
64
65    /// Returns new [TransactionArgs] instantiated with the provided transaction script, advice
66    /// map and foreign account inputs.
67    pub fn new(advice_map: AdviceMap) -> Self {
68        let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() };
69
70        Self {
71            tx_script: None,
72            tx_script_args: EMPTY_WORD,
73            note_args: Default::default(),
74            advice_inputs,
75            auth_args: EMPTY_WORD,
76        }
77    }
78
79    /// Returns new [TransactionArgs] instantiated with the provided transaction script.
80    ///
81    /// If the transaction script is already set, it will be overwritten with the newly provided
82    /// one.
83    #[must_use]
84    pub fn with_tx_script(mut self, tx_script: TransactionScript) -> Self {
85        self.tx_script = Some(tx_script);
86        self
87    }
88
89    /// Returns new [TransactionArgs] instantiated with the provided transaction script and its
90    /// arguments.
91    ///
92    /// If the transaction script and arguments are already set, they will be overwritten with the
93    /// newly provided ones.
94    #[must_use]
95    pub fn with_tx_script_and_args(
96        mut self,
97        tx_script: TransactionScript,
98        tx_script_args: Word,
99    ) -> Self {
100        self.tx_script = Some(tx_script);
101        self.tx_script_args = tx_script_args;
102        self
103    }
104
105    /// Returns new [TransactionArgs] instantiated with the provided note arguments.
106    ///
107    /// If the note arguments were already set, they will be overwritten with the newly provided
108    /// ones.
109    #[must_use]
110    pub fn with_note_args(mut self, note_args: BTreeMap<NoteId, Word>) -> Self {
111        self.note_args = note_args;
112        self
113    }
114
115    /// Returns new [TransactionArgs] instantiated with the provided auth arguments.
116    #[must_use]
117    pub fn with_auth_args(mut self, auth_args: Word) -> Self {
118        self.auth_args = auth_args;
119        self
120    }
121
122    // PUBLIC ACCESSORS
123    // --------------------------------------------------------------------------------------------
124
125    /// Returns a reference to the transaction script.
126    pub fn tx_script(&self) -> Option<&TransactionScript> {
127        self.tx_script.as_ref()
128    }
129
130    /// Returns the transaction script arguments, or [`EMPTY_WORD`] if the arguments were not
131    /// specified.
132    ///
133    /// These arguments could be potentially used as a key to access the advice map during the
134    /// transaction script execution. Notice that the corresponding map entry should be provided
135    /// separately during the creation with the [`TransactionArgs::new`] or using the
136    /// [`TransactionArgs::extend_advice_map`] method.
137    pub fn tx_script_args(&self) -> Word {
138        self.tx_script_args
139    }
140
141    /// Returns a reference to a specific note argument.
142    pub fn get_note_args(&self, note_id: NoteId) -> Option<&Word> {
143        self.note_args.get(&note_id)
144    }
145
146    /// Returns a reference to the internal [AdviceInputs].
147    pub fn advice_inputs(&self) -> &AdviceInputs {
148        &self.advice_inputs
149    }
150
151    /// Returns a reference to the authentication procedure argument, or [`EMPTY_WORD`] if the
152    /// argument was not specified.
153    ///
154    /// This argument could be potentially used as a key to access the advice map during the
155    /// transaction script execution. Notice that the corresponding map entry should be provided
156    /// separately during the creation with the [`TransactionArgs::new`] or using the
157    /// [`TransactionArgs::extend_advice_map`] method.
158    pub fn auth_args(&self) -> Word {
159        self.auth_args
160    }
161
162    // STATE MUTATORS
163    // --------------------------------------------------------------------------------------------
164
165    /// Populates the advice inputs with the expected recipient data for creating output notes.
166    ///
167    /// The advice inputs' map is extended with the following entries:
168    /// - RECIPIENT: [SERIAL_SCRIPT_HASH, STORAGE_COMMITMENT]
169    /// - SERIAL_SCRIPT_HASH: [SERIAL_HASH, SCRIPT_ROOT]
170    /// - SERIAL_HASH: [SERIAL_NUM, EMPTY_WORD]
171    /// - storage_commitment |-> storage_items.
172    /// - script_root |-> script.
173    pub fn add_output_note_recipient<T: AsRef<NoteRecipient>>(&mut self, note_recipient: T) {
174        let note_recipient = note_recipient.as_ref();
175        let storage = note_recipient.storage();
176        let script = note_recipient.script();
177        let script_encoded: Vec<Felt> = script.into();
178
179        // Build the advice map entries
180        let script_root: Word = script.root().into();
181        let sn_hash = Hasher::merge(&[note_recipient.serial_num(), Word::empty()]);
182        let sn_script_hash = Hasher::merge(&[sn_hash, script_root]);
183
184        let new_elements = vec![
185            (sn_hash, concat_words(note_recipient.serial_num(), Word::empty())),
186            (sn_script_hash, concat_words(sn_hash, script_root)),
187            (note_recipient.digest(), concat_words(sn_script_hash, storage.commitment())),
188            (storage.commitment(), storage.to_elements()),
189            (script_root, script_encoded),
190        ];
191
192        self.advice_inputs.extend(AdviceInputs::default().with_map(new_elements));
193    }
194
195    /// Adds the `signature` corresponding to `pub_key` on `message` to the advice inputs' map.
196    ///
197    /// The advice inputs' map is extended with the following key:
198    ///
199    /// - hash(pub_key, message) |-> signature (prepared for VM execution).
200    pub fn add_signature(
201        &mut self,
202        pub_key: PublicKeyCommitment,
203        message: Word,
204        signature: Signature,
205    ) {
206        let pk_word: Word = pub_key.into();
207        self.advice_inputs
208            .map
209            .insert(Hasher::merge(&[pk_word, message]), signature.to_prepared_signature(message));
210    }
211
212    /// Populates the advice inputs with the specified note recipient details.
213    ///
214    /// The advice inputs' map is extended with the following keys:
215    ///
216    /// - recipient |-> recipient details (inputs_hash, script_root, serial_num).
217    /// - storage_commitment |-> storage_items.
218    /// - script_root |-> script.
219    pub fn extend_output_note_recipients<T, L>(&mut self, notes: L)
220    where
221        L: IntoIterator<Item = T>,
222        T: AsRef<NoteRecipient>,
223    {
224        for note in notes {
225            self.add_output_note_recipient(note);
226        }
227    }
228
229    /// Extends the internal advice inputs' map with the provided key-value pairs.
230    pub fn extend_advice_map<T: IntoIterator<Item = (Word, Vec<Felt>)>>(&mut self, iter: T) {
231        self.advice_inputs.map.extend(iter);
232    }
233
234    /// Extends the internal advice inputs' merkle store with the provided nodes.
235    pub fn extend_merkle_store<I: Iterator<Item = InnerNodeInfo>>(&mut self, iter: I) {
236        self.advice_inputs.store.extend(iter);
237    }
238
239    /// Extends the advice inputs in self with the provided ones.
240    pub fn extend_advice_inputs(&mut self, advice_inputs: AdviceInputs) {
241        self.advice_inputs.extend(advice_inputs);
242    }
243}
244
245/// Concatenates two [`Word`]s into a [`Vec<Felt>`] containing 8 elements.
246fn concat_words(first: Word, second: Word) -> Vec<Felt> {
247    let mut result = Vec::with_capacity(8);
248    result.extend(first);
249    result.extend(second);
250    result
251}
252
253impl Default for TransactionArgs {
254    fn default() -> Self {
255        Self::new(AdviceMap::default())
256    }
257}
258
259impl Serializable for TransactionArgs {
260    fn write_into<W: ByteWriter>(&self, target: &mut W) {
261        self.tx_script.write_into(target);
262        self.tx_script_args.write_into(target);
263        self.note_args.write_into(target);
264        self.advice_inputs.write_into(target);
265        self.auth_args.write_into(target);
266    }
267}
268
269impl Deserializable for TransactionArgs {
270    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
271        let tx_script = Option::<TransactionScript>::read_from(source)?;
272        let tx_script_args = Word::read_from(source)?;
273        let note_args = BTreeMap::<NoteId, Word>::read_from(source)?;
274        let advice_inputs = AdviceInputs::read_from(source)?;
275        let auth_args = Word::read_from(source)?;
276
277        Ok(Self {
278            tx_script,
279            tx_script_args,
280            note_args,
281            advice_inputs,
282            auth_args,
283        })
284    }
285}
286
287// TRANSACTION SCRIPT ROOT
288// ================================================================================================
289
290/// The MAST root of a [`TransactionScript`].
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, WordWrapper)]
292pub struct TransactionScriptRoot(Word);
293
294impl From<TransactionScriptRoot> for Word {
295    fn from(root: TransactionScriptRoot) -> Self {
296        root.0
297    }
298}
299
300impl Display for TransactionScriptRoot {
301    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
302        Display::fmt(&self.0, f)
303    }
304}
305
306impl Serializable for TransactionScriptRoot {
307    fn write_into<W: ByteWriter>(&self, target: &mut W) {
308        target.write(self.0);
309    }
310
311    fn get_size_hint(&self) -> usize {
312        self.0.get_size_hint()
313    }
314}
315
316impl Deserializable for TransactionScriptRoot {
317    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
318        let word: Word = source.read()?;
319        Ok(Self::from_raw(word))
320    }
321}
322
323// TRANSACTION SCRIPT
324// ================================================================================================
325
326/// The attribute name used to mark the entrypoint procedure in a transaction script library.
327pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script";
328
329/// Transaction script.
330///
331/// A transaction script is a program that is executed in a transaction after all input notes
332/// have been executed.
333///
334/// The [TransactionScript] object is composed of an executable program defined by a [MastForest]
335/// and an associated entrypoint.
336#[derive(Clone, Debug)]
337pub struct TransactionScript {
338    mast: Arc<MastForest>,
339    entrypoint: MastNodeId,
340    package_debug_info: Option<Arc<PackageDebugInfo>>,
341}
342
343impl TransactionScript {
344    // CONSTRUCTORS
345    // --------------------------------------------------------------------------------------------
346
347    /// Returns a new [TransactionScript] instantiated with the provided code.
348    // TODO: we can remove this `Program` based constructor once the compiler integrates the
349    // `@transaction_script` attribute (https://github.com/0xMiden/compiler/issues/1190).
350    pub fn new(code: Program) -> Self {
351        Self::from_parts(code.mast_forest().clone(), code.entrypoint())
352    }
353
354    /// Returns a new [TransactionScript] instantiated from the provided MAST forest and entrypoint.
355    ///
356    /// # Panics
357    /// Panics if the specified entrypoint is not in the provided MAST forest.
358    pub fn from_parts(mast: Arc<MastForest>, entrypoint: MastNodeId) -> Self {
359        assert!(mast.get_node_by_id(entrypoint).is_some());
360
361        Self {
362            mast,
363            entrypoint,
364            package_debug_info: None,
365        }
366    }
367
368    /// Creates a [TransactionScript] from a [`Package`].
369    ///
370    /// The package must be an executable (i.e., its target type must be
371    /// [`TargetType::Executable`](miden_mast_package::TargetType::Executable)).
372    ///
373    /// # Errors
374    /// Returns an error if the package cannot be converted to an executable program.
375    pub fn from_package(package: &Package) -> Result<Self, TransactionScriptError> {
376        let program =
377            package.try_into_program().map_err(TransactionScriptError::PackageNotProgram)?;
378
379        Ok(Self {
380            mast: program.mast_forest().clone(),
381            entrypoint: program.entrypoint(),
382            package_debug_info: package_debug_info(package),
383        })
384    }
385
386    /// Returns a new [TransactionScript] instantiated from the provided library.
387    ///
388    /// The library must contain exactly one procedure with the `@transaction_script` attribute,
389    /// which will be used as the entrypoint.
390    ///
391    /// # Errors
392    /// Returns an error if:
393    /// - The library does not contain a procedure with the `@transaction_script` attribute.
394    /// - The library contains multiple procedures with the `@transaction_script` attribute.
395    pub fn from_library(library: &Library) -> Result<Self, TransactionScriptError> {
396        let mut entrypoint = None;
397
398        for export in library.manifest.exports() {
399            if let Some(proc_export) = export.as_procedure()
400                && proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE)
401            {
402                if entrypoint.is_some() {
403                    return Err(TransactionScriptError::MultipleProceduresWithAttribute);
404                }
405                entrypoint =
406                    Some(proc_export.node.ok_or(TransactionScriptError::NoProcedureWithAttribute)?);
407            }
408        }
409
410        let entrypoint = entrypoint.ok_or(TransactionScriptError::NoProcedureWithAttribute)?;
411
412        Ok(Self {
413            mast: library.mast_forest().clone(),
414            entrypoint,
415            package_debug_info: package_debug_info(library),
416        })
417    }
418
419    /// Returns a new [TransactionScript] containing only a reference to a procedure in the
420    /// provided library.
421    ///
422    /// This method is useful when a library contains multiple transaction scripts and you need
423    /// to extract a specific one by its fully qualified path (e.g.,
424    /// `::miden::standards::tx_scripts::send_notes::main`).
425    ///
426    /// The procedure at the specified path must have the `@transaction_script` attribute.
427    ///
428    /// Note: This method creates a minimal [MastForest] containing only an external node
429    /// referencing the procedure's digest, rather than copying the entire library. The actual
430    /// procedure code will be resolved at runtime via the `MastForestStore`.
431    ///
432    /// # Errors
433    /// Returns an error if:
434    /// - The library does not contain a procedure at the specified path.
435    /// - The procedure at the specified path does not have the `@transaction_script` attribute.
436    pub fn from_library_reference(
437        library: &Library,
438        path: &Path,
439    ) -> Result<Self, TransactionScriptError> {
440        // Find the export matching the path
441        let export =
442            library.manifest.exports().find(|e| e.path().as_ref() == path).ok_or_else(|| {
443                TransactionScriptError::ProcedureNotFound(path.to_string().into())
444            })?;
445
446        // Get the procedure export and verify it has the @transaction_script attribute
447        let proc_export = export
448            .as_procedure()
449            .ok_or_else(|| TransactionScriptError::ProcedureNotFound(path.to_string().into()))?;
450
451        if !proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE) {
452            return Err(TransactionScriptError::ProcedureMissingAttribute(path.to_string().into()));
453        }
454
455        // Get the digest of the procedure from the library
456        let digest = proc_export.digest;
457
458        // Create a minimal MastForest with just an external node referencing the digest
459        let (mast, entrypoint) = create_external_node_forest(digest);
460
461        Ok(Self {
462            mast: Arc::new(mast),
463            entrypoint,
464            package_debug_info: package_debug_info(library),
465        })
466    }
467
468    // PUBLIC ACCESSORS
469    // --------------------------------------------------------------------------------------------
470
471    /// Returns a reference to the [MastForest] backing this transaction script.
472    pub fn mast(&self) -> Arc<MastForest> {
473        self.mast.clone()
474    }
475
476    /// Returns the MAST forest and package-owned debug information backing this transaction script.
477    pub fn loaded_mast_forest(&self) -> LoadedMastForest {
478        loaded_mast_forest(self.mast.clone(), self.package_debug_info.clone())
479    }
480
481    /// Returns the commitment of this transaction script (i.e., the script's MAST root).
482    pub fn root(&self) -> TransactionScriptRoot {
483        TransactionScriptRoot::from_raw(self.mast[self.entrypoint].digest())
484    }
485
486    /// Returns a new [TransactionScript] with the provided advice map entries merged into the
487    /// underlying [MastForest].
488    ///
489    /// This allows adding advice map entries to an already-compiled transaction script,
490    /// which is useful when the entries are determined after script compilation.
491    pub fn with_advice_map(self, advice_map: AdviceMap) -> Self {
492        if advice_map.is_empty() {
493            return self;
494        }
495
496        let mast = (*self.mast).clone().with_advice_map(advice_map);
497        Self {
498            mast: Arc::new(mast),
499            entrypoint: self.entrypoint,
500            package_debug_info: self.package_debug_info,
501        }
502    }
503}
504
505impl PartialEq for TransactionScript {
506    fn eq(&self, other: &Self) -> bool {
507        self.mast == other.mast && self.entrypoint == other.entrypoint
508    }
509}
510
511impl Eq for TransactionScript {}
512
513// SERIALIZATION
514// ================================================================================================
515
516impl Serializable for TransactionScript {
517    fn write_into<W: ByteWriter>(&self, target: &mut W) {
518        self.mast.write_into(target);
519        target.write_u32(u32::from(self.entrypoint));
520    }
521}
522
523impl Deserializable for TransactionScript {
524    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
525        let mast = MastForest::read_from(source)?;
526        let entrypoint = MastNodeId::from_u32_safe(source.read_u32()?, &mast)?;
527
528        Ok(Self::from_parts(Arc::new(mast), entrypoint))
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use miden_core::advice::AdviceMap;
535
536    use crate::transaction::TransactionArgs;
537    use crate::utils::serde::{Deserializable, Serializable};
538
539    #[test]
540    fn test_tx_args_serialization() {
541        let tx_args = TransactionArgs::new(AdviceMap::default());
542        let bytes: std::vec::Vec<u8> = tx_args.to_bytes();
543        let decoded = TransactionArgs::read_from_bytes(&bytes).unwrap();
544
545        assert_eq!(tx_args, decoded);
546    }
547
548    #[test]
549    fn test_transaction_script_preserves_package_debug_info() {
550        use super::TransactionScript;
551        use crate::assembly::Assembler;
552
553        let assembler = Assembler::default();
554        let package =
555            assembler.assemble_program("test-transaction-script", "begin nop end").unwrap();
556        let script = TransactionScript::from_package(&package).unwrap();
557
558        assert!(script.loaded_mast_forest().package_debug_info().unwrap().is_some());
559    }
560
561    #[test]
562    fn test_transaction_script_with_advice_map() {
563        use miden_core::{Felt, Word};
564
565        use super::TransactionScript;
566        use crate::assembly::Assembler;
567
568        let assembler = Assembler::default();
569        let program = assembler
570            .assemble_program("test-transaction-script", "begin nop end")
571            .unwrap()
572            .try_into_program()
573            .unwrap();
574        let script = TransactionScript::new(program);
575
576        assert!(script.mast().advice_map().is_empty());
577
578        // Empty advice map should be a no-op
579        let original_root = script.root();
580        let script = script.with_advice_map(AdviceMap::default());
581        assert_eq!(original_root, script.root());
582
583        // Non-empty advice map should add entries
584        let key = Word::from([1u32, 2, 3, 4]);
585        let value = vec![Felt::new_unchecked(42), Felt::new_unchecked(43)];
586        let mut advice_map = AdviceMap::default();
587        advice_map.insert(key, value.clone());
588
589        let script = script.with_advice_map(advice_map);
590
591        let mast = script.mast();
592        let stored = mast.advice_map().get(&key).expect("entry should be present");
593        assert_eq!(stored.as_ref(), value.as_slice());
594    }
595
596    #[test]
597    fn test_transaction_script_from_library() {
598        use assert_matches::assert_matches;
599
600        use super::TransactionScript;
601        use crate::errors::TransactionScriptError;
602        use crate::testing::assembler::assemble_test_library;
603        use crate::utils::serde::{Deserializable, Serializable};
604
605        let source = "
606            @transaction_script
607            pub proc main
608                push.1 drop
609            end
610        ";
611        let library = assemble_test_library("test-tx-script", "test::tx_script", source);
612
613        let script = TransactionScript::from_library(&library).unwrap();
614
615        // the script must round-trip through serialization unchanged
616        let bytes = script.to_bytes();
617        let decoded = TransactionScript::read_from_bytes(&bytes).unwrap();
618        assert_eq!(script, decoded);
619
620        // a library without the attribute is rejected
621        let no_attr = assemble_test_library(
622            "test-tx-script-no-attr",
623            "test::tx_script_no_attr",
624            "pub proc main push.1 drop end",
625        );
626        assert_matches!(
627            TransactionScript::from_library(&no_attr),
628            Err(TransactionScriptError::NoProcedureWithAttribute)
629        );
630
631        // a library with multiple tagged procedures is rejected
632        let multiple = assemble_test_library(
633            "test-tx-script-multiple",
634            "test::tx_script_multiple",
635            "@transaction_script pub proc main_a push.1 drop end
636             @transaction_script pub proc main_b push.2 drop end",
637        );
638        assert_matches!(
639            TransactionScript::from_library(&multiple),
640            Err(TransactionScriptError::MultipleProceduresWithAttribute)
641        );
642    }
643
644    #[test]
645    fn test_transaction_script_from_library_reference() {
646        use alloc::string::ToString;
647
648        use assert_matches::assert_matches;
649
650        use super::TransactionScript;
651        use crate::Word;
652        use crate::assembly::Path;
653        use crate::errors::TransactionScriptError;
654        use crate::testing::assembler::assemble_test_library;
655
656        let source = "
657            @transaction_script
658            pub proc main_a
659                push.1 drop
660            end
661
662            @transaction_script
663            pub proc main_b
664                push.2 drop
665            end
666
667            pub proc helper
668                push.3 drop
669            end
670        ";
671        let library =
672            assemble_test_library("test-tx-script-reference", "test::tx_script_reference", source);
673
674        // each tagged procedure can be extracted selectively, and the resulting script's root
675        // matches the digest of the referenced procedure
676        for proc_name in ["main_a", "main_b"] {
677            let export = library
678                .manifest
679                .exports()
680                .find(|e| e.path().as_ref().to_string().ends_with(proc_name))
681                .unwrap();
682            let digest = export.as_procedure().unwrap().digest;
683
684            let script =
685                TransactionScript::from_library_reference(&library, export.path().as_ref())
686                    .unwrap();
687            assert_eq!(Word::from(script.root()), digest);
688        }
689
690        // an unknown path is rejected
691        assert_matches!(
692            TransactionScript::from_library_reference(&library, Path::new("::foo::bar::main")),
693            Err(TransactionScriptError::ProcedureNotFound(_))
694        );
695
696        // a procedure without the attribute is rejected
697        let helper = library
698            .manifest
699            .exports()
700            .find(|e| e.path().as_ref().to_string().ends_with("helper"))
701            .unwrap();
702        assert_matches!(
703            TransactionScript::from_library_reference(&library, helper.path().as_ref()),
704            Err(TransactionScriptError::ProcedureMissingAttribute(_))
705        );
706    }
707}