Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
- [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)).
- 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)).
- Updated `AuthRequest` event to carry either signature of TX summary, but not both ([#3157](https://github.com/0xMiden/protocol/pull/3157)).
- [BREAKING] Added `@transaction_script` attribute to mark the script entrypoint. Migrated transaction scripts assembly to `Library` ([#3173](https://github.com/0xMiden/protocol/pull/3173)).
- Added the `account_id::eqz` MASM helper to check whether an account ID is zero ([#3170](https://github.com/0xMiden/protocol/pull/3170)).
- [BREAKING] Moved asset callback flag from asset vault key to account ID, making it immutable ([#3167](https://github.com/0xMiden/protocol/pull/3167)).
- [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)).
Expand Down
3 changes: 2 additions & 1 deletion bin/bench-transaction/src/context_setups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ fn tx_create_single_p2id_note_with_auth(auth_scheme: AuthScheme) -> Result<Trans
use miden::protocol::output_note
use miden::core::sys

begin
@transaction_script
pub proc main
# create an output note with fungible asset
push.{RECIPIENT}
push.{note_type}
Expand Down
8 changes: 8 additions & 0 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,14 @@ pub enum TransactionScriptError {
AssemblyError(Report),
#[error("failed to convert package to transaction script:\n{}", PrintDiagnostic::new(.0))]
PackageNotProgram(Report),
#[error("library does not contain a procedure with @transaction_script attribute")]
NoProcedureWithAttribute,
#[error("library contains multiple procedures with @transaction_script attribute")]
MultipleProceduresWithAttribute,
#[error("procedure at path '{0}' not found in library")]
ProcedureNotFound(Box<str>),
#[error("procedure at path '{0}' does not have @transaction_script attribute")]
ProcedureMissingAttribute(Box<str>),
}

// TRANSACTION INPUT ERROR
Expand Down
24 changes: 3 additions & 21 deletions crates/miden-protocol/src/note/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ use alloc::vec::Vec;
use core::fmt::Display;
use core::num::TryFromIntError;

use miden_core::mast::{MastNode, MastNodeExt};
use miden_core::mast::MastNodeExt;
use miden_core::utils::IndexVec;
use miden_crypto_derive::WordWrapper;
use miden_mast_package::Package;
use miden_mast_package::debug_info::PackageDebugInfo;
use miden_processor::LoadedMastForest;

use super::Felt;
use crate::assembly::mast::{ExternalNodeBuilder, MastForest, MastNodeId};
use crate::assembly::mast::{MastForest, MastNodeId};
use crate::assembly::{Library, Path};
use crate::errors::NoteError;
use crate::package::{loaded_mast_forest, package_debug_info};
use crate::utils::create_external_node_forest;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Expand Down Expand Up @@ -427,25 +428,6 @@ impl Display for NoteScript {
}
}

// HELPER FUNCTIONS
// ================================================================================================

/// Creates a minimal [MastForest] containing only an external node referencing the given digest.
///
/// This is useful for creating lightweight references to procedures without copying entire
/// libraries. The external reference will be resolved at runtime, assuming the source library
/// is loaded into the VM's MastForestStore.
fn create_external_node_forest(digest: Word) -> (MastForest, MastNodeId) {
let mut nodes: miden_core::utils::IndexVec<MastNodeId, MastNode> =
miden_core::utils::IndexVec::new();
let node_id = nodes
.push(ExternalNodeBuilder::new(digest).build().into())
.expect("adding external node to empty forest should not fail");
let mast = MastForest::from_raw_parts(nodes, vec![node_id], AdviceMap::default())
.expect("single external node forest should be well-formed");
(mast, node_id)
}

// TESTS
// ================================================================================================

Expand Down
7 changes: 6 additions & 1 deletion crates/miden-protocol/src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ pub use outputs::{
pub use partial_blockchain::PartialBlockchain;
pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate};
pub use transaction_id::TransactionId;
pub use tx_args::{TransactionArgs, TransactionScript, TransactionScriptRoot};
pub use tx_args::{
TRANSACTION_SCRIPT_ATTRIBUTE,
TransactionArgs,
TransactionScript,
TransactionScriptRoot,
};
pub use tx_header::TransactionHeader;
pub use tx_summary::TransactionSummary;
pub use verifier::TransactionVerifier;
203 changes: 202 additions & 1 deletion crates/miden-protocol/src/transaction/tx_args.rs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point NoteScript and TransactionScript share the majority of the struct's implementation code, with the only difference being TRANSACTION_SCRIPT_ATTRIBUTE vs. NOTE_SCRIPT_ATTRIBUTE. We might want to pull this functionality into something more re-usable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would make sense. Created #3189 for it, so we avoid growing this PR.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt::Display;
Expand All @@ -13,9 +13,11 @@ use miden_processor::LoadedMastForest;

use super::{Felt, Hasher, Word};
use crate::account::auth::{PublicKeyCommitment, Signature};
use crate::assembly::{Library, Path};
use crate::errors::TransactionScriptError;
use crate::note::{NoteId, NoteRecipient};
use crate::package::{loaded_mast_forest, package_debug_info};
use crate::utils::create_external_node_forest;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Expand Down Expand Up @@ -321,6 +323,9 @@ impl Deserializable for TransactionScriptRoot {
// TRANSACTION SCRIPT
// ================================================================================================

/// The attribute name used to mark the entrypoint procedure in a transaction script library.
pub const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script";

/// Transaction script.
///
/// A transaction script is a program that is executed in a transaction after all input notes
Expand All @@ -340,6 +345,8 @@ impl TransactionScript {
// --------------------------------------------------------------------------------------------

/// Returns a new [TransactionScript] instantiated with the provided code.
// TODO: we can remove this `Program` based constructor once the compiler integrates the
// `@transaction_script` attribute (https://github.com/0xMiden/compiler/issues/1190).
pub fn new(code: Program) -> Self {
Self::from_parts(code.mast_forest().clone(), code.entrypoint())
}
Expand Down Expand Up @@ -376,6 +383,88 @@ impl TransactionScript {
})
}

/// Returns a new [TransactionScript] instantiated from the provided library.
///
/// The library must contain exactly one procedure with the `@transaction_script` attribute,
/// which will be used as the entrypoint.
///
/// # Errors
/// Returns an error if:
/// - The library does not contain a procedure with the `@transaction_script` attribute.
/// - The library contains multiple procedures with the `@transaction_script` attribute.
pub fn from_library(library: &Library) -> Result<Self, TransactionScriptError> {
let mut entrypoint = None;

for export in library.manifest.exports() {
if let Some(proc_export) = export.as_procedure()
&& proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE)
{
if entrypoint.is_some() {
return Err(TransactionScriptError::MultipleProceduresWithAttribute);
}
entrypoint =
Some(proc_export.node.ok_or(TransactionScriptError::NoProcedureWithAttribute)?);
}
}

let entrypoint = entrypoint.ok_or(TransactionScriptError::NoProcedureWithAttribute)?;

Ok(Self {
mast: library.mast_forest().clone(),
entrypoint,
package_debug_info: package_debug_info(library),
})
}

/// Returns a new [TransactionScript] containing only a reference to a procedure in the
/// provided library.
///
/// This method is useful when a library contains multiple transaction scripts and you need
/// to extract a specific one by its fully qualified path (e.g.,
/// `::miden::standards::tx_scripts::send_notes::main`).
///
/// The procedure at the specified path must have the `@transaction_script` attribute.
///
/// Note: This method creates a minimal [MastForest] containing only an external node
/// referencing the procedure's digest, rather than copying the entire library. The actual
/// procedure code will be resolved at runtime via the `MastForestStore`.
///
/// # Errors
/// Returns an error if:
/// - The library does not contain a procedure at the specified path.
/// - The procedure at the specified path does not have the `@transaction_script` attribute.
pub fn from_library_reference(
library: &Library,
path: &Path,
) -> Result<Self, TransactionScriptError> {
// Find the export matching the path
let export =
library.manifest.exports().find(|e| e.path().as_ref() == path).ok_or_else(|| {
TransactionScriptError::ProcedureNotFound(path.to_string().into())
})?;

// Get the procedure export and verify it has the @transaction_script attribute
let proc_export = export
.as_procedure()
.ok_or_else(|| TransactionScriptError::ProcedureNotFound(path.to_string().into()))?;

if !proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE) {
return Err(TransactionScriptError::ProcedureMissingAttribute(path.to_string().into()));
}

// Get the digest of the procedure from the library
let digest = proc_export.digest;

// Create a minimal MastForest with just an external node referencing the digest
let (mast, entrypoint) = create_external_node_forest(digest);

Ok(Self {
mast: Arc::new(mast),
entrypoint,
package_debug_info: package_debug_info(library),
})
}

// PUBLIC ACCESSORS
// --------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -503,4 +592,116 @@ mod tests {
let stored = mast.advice_map().get(&key).expect("entry should be present");
assert_eq!(stored.as_ref(), value.as_slice());
}

#[test]
fn test_transaction_script_from_library() {
use assert_matches::assert_matches;

use super::TransactionScript;
use crate::errors::TransactionScriptError;
use crate::testing::assembler::assemble_test_library;
use crate::utils::serde::{Deserializable, Serializable};

let source = "
@transaction_script
pub proc main
push.1 drop
end
";
let library = assemble_test_library("test-tx-script", "test::tx_script", source);

let script = TransactionScript::from_library(&library).unwrap();

// the script must round-trip through serialization unchanged
let bytes = script.to_bytes();
let decoded = TransactionScript::read_from_bytes(&bytes).unwrap();
assert_eq!(script, decoded);

// a library without the attribute is rejected
let no_attr = assemble_test_library(
"test-tx-script-no-attr",
"test::tx_script_no_attr",
"pub proc main push.1 drop end",
);
assert_matches!(
TransactionScript::from_library(&no_attr),
Err(TransactionScriptError::NoProcedureWithAttribute)
);

// a library with multiple tagged procedures is rejected
let multiple = assemble_test_library(
"test-tx-script-multiple",
"test::tx_script_multiple",
"@transaction_script pub proc main_a push.1 drop end
@transaction_script pub proc main_b push.2 drop end",
);
assert_matches!(
TransactionScript::from_library(&multiple),
Err(TransactionScriptError::MultipleProceduresWithAttribute)
);
}

#[test]
fn test_transaction_script_from_library_reference() {
use alloc::string::ToString;

use assert_matches::assert_matches;

use super::TransactionScript;
use crate::Word;
use crate::assembly::Path;
use crate::errors::TransactionScriptError;
use crate::testing::assembler::assemble_test_library;

let source = "
@transaction_script
pub proc main_a
push.1 drop
end

@transaction_script
pub proc main_b
push.2 drop
end

pub proc helper
push.3 drop
end
";
let library =
assemble_test_library("test-tx-script-reference", "test::tx_script_reference", source);

// each tagged procedure can be extracted selectively, and the resulting script's root
// matches the digest of the referenced procedure
for proc_name in ["main_a", "main_b"] {
let export = library
.manifest
.exports()
.find(|e| e.path().as_ref().to_string().ends_with(proc_name))
.unwrap();
let digest = export.as_procedure().unwrap().digest;

let script =
TransactionScript::from_library_reference(&library, export.path().as_ref())
.unwrap();
assert_eq!(Word::from(script.root()), digest);
}

// an unknown path is rejected
assert_matches!(
TransactionScript::from_library_reference(&library, Path::new("::foo::bar::main")),
Err(TransactionScriptError::ProcedureNotFound(_))
);

// a procedure without the attribute is rejected
let helper = library
.manifest
.exports()
.find(|e| e.path().as_ref().to_string().ends_with("helper"))
.unwrap();
assert_matches!(
TransactionScript::from_library_reference(&library, helper.path().as_ref()),
Err(TransactionScriptError::ProcedureMissingAttribute(_))
);
}
}
22 changes: 22 additions & 0 deletions crates/miden-protocol/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use alloc::vec;

use miden_core::mast::MastNode;
pub use miden_core::utils::*;
pub use miden_crypto::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes};
pub use miden_utils_sync as sync;

use crate::Word;
use crate::assembly::mast::{ExternalNodeBuilder, MastForest, MastNodeId};
use crate::vm::AdviceMap;

pub mod serde {
pub use miden_crypto::utils::{
BudgetedReader,
Expand All @@ -17,3 +24,18 @@ pub mod serde {
pub mod strings;

pub(crate) use strings::ShortCapitalString;

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