Skip to content

Commit 141f668

Browse files
feat: migrate transaction scripts to Library (#3173)
* feat: migrate transaction scripts to library and add `@transaction_script` attribute * review: use TRANSACTION_SCRIPT_ATTRIBUTE constant * review: add `TransactionScript::from_library_reference` * review: test script formatting * review: use `TransactionScript::from_library` and `NoteScript::from_library` * chore: fmt
1 parent bd9311e commit 141f668

44 files changed

Lines changed: 618 additions & 221 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
- [BREAKING] Renamed `create_user_fungible_faucet` to `create_singlesig_user_fungible_faucet` and added the `create_multisig_user_fungible_faucet(auth_component: AuthMultisig, ...)` and `create_guarded_user_fungible_faucet(auth_component: AuthGuardedMultisig, ...)`. `create_network_fungible_faucet` now allowlists the canonical `ExpirationTransactionScript` in its tx-script allowlist ([#3143](https://github.com/0xMiden/protocol/pull/3143)).
7474
- Added the `CodeInspection` standard account component, exposing the `has_procedure`, `get_code_commitment`, `get_num_procedures`, and `get_procedure_root` introspection procedures on an account's public interface ([#3162](https://github.com/0xMiden/protocol/pull/3162)).
7575
- Updated `AuthRequest` event to carry either signature of TX summary, but not both ([#3157](https://github.com/0xMiden/protocol/pull/3157)).
76+
- [BREAKING] Added `@transaction_script` attribute to mark the script entrypoint. Migrated transaction scripts assembly to `Library` ([#3173](https://github.com/0xMiden/protocol/pull/3173)).
7677
- Added the `account_id::eqz` MASM helper to check whether an account ID is zero ([#3170](https://github.com/0xMiden/protocol/pull/3170)).
7778
- [BREAKING] Moved asset callback flag from asset vault key to account ID, making it immutable ([#3167](https://github.com/0xMiden/protocol/pull/3167)).
7879
- [BREAKING] Added `@account_procedure` attribute to mark which procedures should be included in the account component interface ([#3171](https://github.com/0xMiden/protocol/pull/3171)).

bin/bench-transaction/src/context_setups.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ fn tx_create_single_p2id_note_with_auth(auth_scheme: AuthScheme) -> Result<Trans
5959
use miden::protocol::output_note
6060
use miden::core::sys
6161
62-
begin
62+
@transaction_script
63+
pub proc main
6364
# create an output note with fungible asset
6465
push.{RECIPIENT}
6566
push.{note_type}

crates/miden-protocol/src/errors/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,14 @@ pub enum TransactionScriptError {
875875
AssemblyError(Report),
876876
#[error("failed to convert package to transaction script:\n{}", PrintDiagnostic::new(.0))]
877877
PackageNotProgram(Report),
878+
#[error("library does not contain a procedure with @transaction_script attribute")]
879+
NoProcedureWithAttribute,
880+
#[error("library contains multiple procedures with @transaction_script attribute")]
881+
MultipleProceduresWithAttribute,
882+
#[error("procedure at path '{0}' not found in library")]
883+
ProcedureNotFound(Box<str>),
884+
#[error("procedure at path '{0}' does not have @transaction_script attribute")]
885+
ProcedureMissingAttribute(Box<str>),
878886
}
879887

880888
// TRANSACTION INPUT ERROR

crates/miden-protocol/src/note/script.rs

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ use alloc::vec::Vec;
44
use core::fmt::Display;
55
use core::num::TryFromIntError;
66

7-
use miden_core::mast::{MastNode, MastNodeExt};
7+
use miden_core::mast::MastNodeExt;
88
use miden_core::utils::IndexVec;
99
use miden_crypto_derive::WordWrapper;
1010
use miden_mast_package::Package;
1111
use miden_mast_package::debug_info::PackageDebugInfo;
1212
use miden_processor::LoadedMastForest;
1313

1414
use super::Felt;
15-
use crate::assembly::mast::{ExternalNodeBuilder, MastForest, MastNodeId};
15+
use crate::assembly::mast::{MastForest, MastNodeId};
1616
use crate::assembly::{Library, Path};
1717
use crate::errors::NoteError;
1818
use crate::package::{loaded_mast_forest, package_debug_info};
19+
use crate::utils::create_external_node_forest;
1920
use crate::utils::serde::{
2021
ByteReader,
2122
ByteWriter,
@@ -427,25 +428,6 @@ impl Display for NoteScript {
427428
}
428429
}
429430

430-
// HELPER FUNCTIONS
431-
// ================================================================================================
432-
433-
/// Creates a minimal [MastForest] containing only an external node referencing the given digest.
434-
///
435-
/// This is useful for creating lightweight references to procedures without copying entire
436-
/// libraries. The external reference will be resolved at runtime, assuming the source library
437-
/// is loaded into the VM's MastForestStore.
438-
fn create_external_node_forest(digest: Word) -> (MastForest, MastNodeId) {
439-
let mut nodes: miden_core::utils::IndexVec<MastNodeId, MastNode> =
440-
miden_core::utils::IndexVec::new();
441-
let node_id = nodes
442-
.push(ExternalNodeBuilder::new(digest).build().into())
443-
.expect("adding external node to empty forest should not fail");
444-
let mast = MastForest::from_raw_parts(nodes, vec![node_id], AdviceMap::default())
445-
.expect("single external node forest should be well-formed");
446-
(mast, node_id)
447-
}
448-
449431
// TESTS
450432
// ================================================================================================
451433

crates/miden-protocol/src/transaction/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ pub use outputs::{
3333
pub use partial_blockchain::PartialBlockchain;
3434
pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate};
3535
pub use transaction_id::TransactionId;
36-
pub use tx_args::{TransactionArgs, TransactionScript, TransactionScriptRoot};
36+
pub use tx_args::{
37+
TRANSACTION_SCRIPT_ATTRIBUTE,
38+
TransactionArgs,
39+
TransactionScript,
40+
TransactionScriptRoot,
41+
};
3742
pub use tx_header::TransactionHeader;
3843
pub use tx_summary::TransactionSummary;
3944
pub use verifier::TransactionVerifier;

crates/miden-protocol/src/transaction/tx_args.rs

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloc::collections::BTreeMap;
2-
use alloc::string::String;
2+
use alloc::string::{String, ToString};
33
use alloc::sync::Arc;
44
use alloc::vec::Vec;
55
use core::fmt::Display;
@@ -13,9 +13,11 @@ use miden_processor::LoadedMastForest;
1313

1414
use super::{Felt, Hasher, Word};
1515
use crate::account::auth::{PublicKeyCommitment, Signature};
16+
use crate::assembly::{Library, Path};
1617
use crate::errors::TransactionScriptError;
1718
use crate::note::{NoteId, NoteRecipient};
1819
use crate::package::{loaded_mast_forest, package_debug_info};
20+
use crate::utils::create_external_node_forest;
1921
use crate::utils::serde::{
2022
ByteReader,
2123
ByteWriter,
@@ -321,6 +323,9 @@ impl Deserializable for TransactionScriptRoot {
321323
// TRANSACTION SCRIPT
322324
// ================================================================================================
323325

326+
/// The attribute name used to mark the entrypoint procedure in a transaction script library.
327+
pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script";
328+
324329
/// Transaction script.
325330
///
326331
/// A transaction script is a program that is executed in a transaction after all input notes
@@ -340,6 +345,8 @@ impl TransactionScript {
340345
// --------------------------------------------------------------------------------------------
341346

342347
/// 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).
343350
pub fn new(code: Program) -> Self {
344351
Self::from_parts(code.mast_forest().clone(), code.entrypoint())
345352
}
@@ -376,6 +383,88 @@ impl TransactionScript {
376383
})
377384
}
378385

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+
379468
// PUBLIC ACCESSORS
380469
// --------------------------------------------------------------------------------------------
381470

@@ -503,4 +592,116 @@ mod tests {
503592
let stored = mast.advice_map().get(&key).expect("entry should be present");
504593
assert_eq!(stored.as_ref(), value.as_slice());
505594
}
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+
}
506707
}

crates/miden-protocol/src/utils/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
use alloc::vec;
2+
3+
use miden_core::mast::MastNode;
14
pub use miden_core::utils::*;
25
pub use miden_crypto::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes};
36
pub use miden_utils_sync as sync;
47

8+
use crate::Word;
9+
use crate::assembly::mast::{ExternalNodeBuilder, MastForest, MastNodeId};
10+
use crate::vm::AdviceMap;
11+
512
pub mod serde {
613
pub use miden_crypto::utils::{
714
BudgetedReader,
@@ -17,3 +24,18 @@ pub mod serde {
1724
pub mod strings;
1825

1926
pub(crate) use strings::ShortCapitalString;
27+
28+
/// Creates a minimal [MastForest] containing only an external node referencing the given digest.
29+
///
30+
/// This is useful for creating lightweight references to procedures without copying entire
31+
/// libraries. The external reference will be resolved at runtime, assuming the source library
32+
/// is loaded into the VM's MastForestStore.
33+
pub(crate) fn create_external_node_forest(digest: Word) -> (MastForest, MastNodeId) {
34+
let mut nodes: IndexVec<MastNodeId, MastNode> = IndexVec::new();
35+
let node_id = nodes
36+
.push(ExternalNodeBuilder::new(digest).build().into())
37+
.expect("adding external node to empty forest should not fail");
38+
let mast = MastForest::from_raw_parts(nodes, vec![node_id], AdviceMap::default())
39+
.expect("single external node forest should be well-formed");
40+
(mast, node_id)
41+
}

0 commit comments

Comments
 (0)