Skip to main content

miden_protocol/transaction/
tx_args.rs

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