Skip to content

Commit a63c913

Browse files
committed
feat: migrate transaction scripts to library and add
`@transaction_script` attribute
1 parent 36a1d09 commit a63c913

40 files changed

Lines changed: 436 additions & 164 deletions

bin/bench-transaction/src/context_setups.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ pub fn tx_create_single_p2id_note() -> Result<TransactionContext> {
5555
use miden::protocol::output_note
5656
use miden::core::sys
5757
58-
begin
58+
@transaction_script
59+
pub proc main
5960
# create an output note with fungible asset
6061
push.{RECIPIENT}
6162
push.{note_type}

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

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

882890
// TRANSACTION INPUT ERROR

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

Lines changed: 133 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;
@@ -11,6 +11,8 @@ use miden_mast_package::Package;
1111

1212
use super::{Felt, Hasher, Word};
1313
use crate::account::auth::{PublicKeyCommitment, Signature};
14+
use crate::assembly::mast::{ExternalNodeBuilder, MastForestContributor};
15+
use crate::assembly::{Library, Path};
1416
use crate::errors::TransactionScriptError;
1517
use crate::note::{NoteId, NoteRecipient};
1618
use crate::utils::serde::{
@@ -318,6 +320,9 @@ impl Deserializable for TransactionScriptRoot {
318320
// TRANSACTION SCRIPT
319321
// ================================================================================================
320322

323+
/// The attribute name used to mark the entrypoint procedure in a transaction script library.
324+
const TRANSACTION_SCRIPT_ATTRIBUTE: &str = "transaction_script";
325+
321326
/// Transaction script.
322327
///
323328
/// A transaction script is a program that is executed in a transaction after all input notes
@@ -336,6 +341,8 @@ impl TransactionScript {
336341
// --------------------------------------------------------------------------------------------
337342

338343
/// Returns a new [TransactionScript] instantiated with the provided code.
344+
// TODO: we can remove this `Program` based constructor once the compiler integrates the
345+
// `@transaction_script` attribute (https://github.com/0xMiden/compiler/issues/1190).
339346
pub fn new(code: Program) -> Self {
340347
Self::from_parts(code.mast_forest().clone(), code.entrypoint())
341348
}
@@ -364,6 +371,76 @@ impl TransactionScript {
364371
Ok(TransactionScript::new(program))
365372
}
366373

374+
/// Returns a new [TransactionScript] instantiated from the provided library.
375+
///
376+
/// The library must contain exactly one procedure with the `@transaction_script` attribute,
377+
/// which will be used as the entrypoint.
378+
///
379+
/// # Errors
380+
/// Returns an error if:
381+
/// - The library does not contain a procedure with the `@transaction_script` attribute.
382+
/// - The library contains multiple procedures with the `@transaction_script` attribute.
383+
pub fn from_library(library: &Library) -> Result<Self, TransactionScriptError> {
384+
let mut entrypoint = None;
385+
386+
for export in library.exports() {
387+
if let Some(proc_export) = export.as_procedure()
388+
&& proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE)
389+
{
390+
if entrypoint.is_some() {
391+
return Err(TransactionScriptError::MultipleProceduresWithAttribute);
392+
}
393+
entrypoint = Some(proc_export.node);
394+
}
395+
}
396+
397+
let entrypoint = entrypoint.ok_or(TransactionScriptError::NoProcedureWithAttribute)?;
398+
399+
Ok(Self {
400+
mast: library.mast_forest().clone(),
401+
entrypoint,
402+
})
403+
}
404+
405+
/// Returns a new [TransactionScript] containing only a reference to a procedure in the provided
406+
/// library.
407+
///
408+
/// This method is useful when a library contains multiple transaction scripts and you need to
409+
/// extract a specific one by its fully qualified path.
410+
///
411+
/// The procedure at the specified path must have the `@transaction_script` attribute.
412+
///
413+
/// Note: This method creates a minimal [MastForest] containing only an external node
414+
/// referencing the procedure's digest, rather than copying the entire library. The actual
415+
/// procedure code will be resolved at runtime via the `MastForestStore`.
416+
///
417+
/// # Errors
418+
/// Returns an error if:
419+
/// - The library does not contain a procedure at the specified path.
420+
/// - The procedure at the specified path does not have the `@transaction_script` attribute.
421+
pub fn from_library_reference(
422+
library: &Library,
423+
path: &Path,
424+
) -> Result<Self, TransactionScriptError> {
425+
let export = library
426+
.exports()
427+
.find(|e| e.path().as_ref() == path)
428+
.ok_or_else(|| TransactionScriptError::ProcedureNotFound(path.to_string().into()))?;
429+
430+
let proc_export = export
431+
.as_procedure()
432+
.ok_or_else(|| TransactionScriptError::ProcedureNotFound(path.to_string().into()))?;
433+
434+
if !proc_export.attributes.has(TRANSACTION_SCRIPT_ATTRIBUTE) {
435+
return Err(TransactionScriptError::ProcedureMissingAttribute(path.to_string().into()));
436+
}
437+
438+
let digest = library.mast_forest()[proc_export.node].digest();
439+
let (mast, entrypoint) = create_external_node_forest(digest);
440+
441+
Ok(Self { mast: Arc::new(mast), entrypoint })
442+
}
443+
367444
// PUBLIC ACCESSORS
368445
// --------------------------------------------------------------------------------------------
369446

@@ -396,6 +473,17 @@ impl TransactionScript {
396473
}
397474
}
398475

476+
/// Creates a minimal [MastForest] containing a single external node referencing the provided
477+
/// digest, returning the forest together with the id of the (root) external node.
478+
fn create_external_node_forest(digest: Word) -> (MastForest, MastNodeId) {
479+
let mut mast = MastForest::new();
480+
let node_id = ExternalNodeBuilder::new(digest)
481+
.add_to_forest(&mut mast)
482+
.expect("adding external node to empty forest should not fail");
483+
mast.make_root(node_id);
484+
(mast, node_id)
485+
}
486+
399487
// SERIALIZATION
400488
// ================================================================================================
401489

@@ -461,4 +549,48 @@ mod tests {
461549
let stored = mast.advice_map().get(&key).expect("entry should be present");
462550
assert_eq!(stored.as_ref(), value.as_slice());
463551
}
552+
553+
#[test]
554+
fn test_transaction_script_from_library() {
555+
use assert_matches::assert_matches;
556+
557+
use super::TransactionScript;
558+
use crate::assembly::Assembler;
559+
use crate::errors::TransactionScriptError;
560+
use crate::utils::serde::{Deserializable, Serializable};
561+
562+
let source = "
563+
@transaction_script
564+
pub proc main
565+
push.1 drop
566+
end
567+
";
568+
let library = Assembler::default().assemble_library([source]).unwrap();
569+
570+
let script = TransactionScript::from_library(&library).unwrap();
571+
572+
// the script must round-trip through serialization unchanged
573+
let bytes = script.to_bytes();
574+
let decoded = TransactionScript::read_from_bytes(&bytes).unwrap();
575+
assert_eq!(script, decoded);
576+
577+
// a library without the attribute is rejected
578+
let no_attr = Assembler::default()
579+
.assemble_library(["pub proc main push.1 drop end"])
580+
.unwrap();
581+
assert_matches!(
582+
TransactionScript::from_library(&no_attr),
583+
Err(TransactionScriptError::NoProcedureWithAttribute)
584+
);
585+
586+
// a library with multiple tagged procedures is rejected
587+
let multiple = Assembler::default()
588+
.assemble_library(["@transaction_script pub proc main_a push.1 drop end
589+
@transaction_script pub proc main_b push.2 drop end"])
590+
.unwrap();
591+
assert_matches!(
592+
TransactionScript::from_library(&multiple),
593+
Err(TransactionScriptError::MultipleProceduresWithAttribute)
594+
);
595+
}
464596
}

crates/miden-standards/src/code_builder/mod.rs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ use crate::standards_lib::StandardsLib;
6363
/// # use miden_protocol::CoreLibrary;
6464
/// # fn example() -> anyhow::Result<()> {
6565
/// # let module_code = "pub proc test push.1 add end";
66-
/// # let script_code = "begin nop end";
66+
/// # let script_code = "@transaction_script pub proc main nop end";
6767
/// # // Create sample libraries for the example
6868
/// # let my_lib: Library = CoreLibrary::default().into(); // Convert CoreLibrary to Library
6969
/// # let fpi_lib: Library = CoreLibrary::default().into();
@@ -293,20 +293,6 @@ impl CodeBuilder {
293293
// PRIVATE HELPERS
294294
// --------------------------------------------------------------------------------------------
295295

296-
/// Applies the advice map to a program if it's non-empty.
297-
///
298-
/// This avoids cloning the MAST forest when there are no advice map entries.
299-
fn apply_advice_map(
300-
advice_map: AdviceMap,
301-
program: miden_protocol::vm::Program,
302-
) -> miden_protocol::vm::Program {
303-
if advice_map.is_empty() {
304-
program
305-
} else {
306-
program.with_advice_map(advice_map)
307-
}
308-
}
309-
310296
/// Applies the advice map to a library if it's non-empty.
311297
///
312298
/// This avoids cloning the MAST forest when there are no advice map entries.
@@ -363,7 +349,8 @@ impl CodeBuilder {
363349
/// The parsed script will have access to all modules that have been added to this builder.
364350
///
365351
/// # Arguments
366-
/// * `tx_script` - The transaction script source code
352+
/// - `tx_script` - the transaction script source code which is expected to have a single public
353+
/// procedure marked with the @transaction_script attribute.
367354
///
368355
/// # Errors
369356
/// Returns an error if:
@@ -374,11 +361,23 @@ impl CodeBuilder {
374361
) -> Result<TransactionScript, CodeBuilderError> {
375362
let CodeBuilder { assembler, advice_map, .. } = self;
376363

377-
let program = assembler.assemble_program(tx_script).map_err(|err| {
378-
CodeBuilderError::build_error_with_report("failed to parse transaction script", err)
364+
let tx_script_lib = assembler.assemble_library([tx_script]).map_err(|err| {
365+
CodeBuilderError::build_error_with_report(
366+
"failed to parse transaction script library",
367+
err,
368+
)
379369
})?;
380370

381-
Ok(TransactionScript::new(Self::apply_advice_map(advice_map, program)))
371+
TransactionScript::from_library(&Self::apply_advice_map_to_library(
372+
advice_map,
373+
Arc::unwrap_or_clone(tx_script_lib),
374+
))
375+
.map_err(|err| {
376+
CodeBuilderError::build_error_with_source(
377+
"failed to create transaction script from library",
378+
err,
379+
)
380+
})
382381
}
383382

384383
/// Compiles the provided MASM code into a [`NoteScript`].
@@ -525,7 +524,7 @@ mod tests {
525524
fn test_code_builder_basic_script_compiling() -> anyhow::Result<()> {
526525
let builder = CodeBuilder::default();
527526
builder
528-
.compile_tx_script("begin nop end")
527+
.compile_tx_script("@transaction_script pub proc main nop end")
529528
.context("failed to parse basic tx script")?;
530529
Ok(())
531530
}
@@ -535,7 +534,8 @@ mod tests {
535534
let script_code = "
536535
use external_contract::counter_contract
537536
538-
begin
537+
@transaction_script
538+
pub proc main
539539
call.counter_contract::increment
540540
end
541541
";
@@ -573,7 +573,8 @@ mod tests {
573573
let script_code = "
574574
use external_contract::counter_contract
575575
576-
begin
576+
@transaction_script
577+
pub proc main
577578
call.counter_contract::increment
578579
end
579580
";
@@ -624,7 +625,8 @@ mod tests {
624625
let script_code = "
625626
use external_contract::counter_contract
626627
627-
begin
628+
@transaction_script
629+
pub proc main
628630
call.counter_contract::increment
629631
end
630632
";
@@ -656,8 +658,7 @@ mod tests {
656658

657659
#[test]
658660
fn test_multiple_chained_modules() -> anyhow::Result<()> {
659-
let script_code =
660-
"use test::lib1 use test::lib2 begin exec.lib1::test1 exec.lib2::test2 end";
661+
let script_code = "use test::lib1 use test::lib2 @transaction_script pub proc main exec.lib1::test1 exec.lib2::test2 end";
661662

662663
// Test chaining multiple modules
663664
let builder = CodeBuilder::default()
@@ -676,7 +677,8 @@ mod tests {
676677
let script_code = "
677678
use contracts::static_contract
678679
679-
begin
680+
@transaction_script
681+
pub proc main
680682
call.static_contract::increment_1
681683
end
682684
";
@@ -732,7 +734,7 @@ mod tests {
732734

733735
let script = CodeBuilder::default()
734736
.with_advice_map_entry(key, value.clone())
735-
.compile_tx_script("begin nop end")
737+
.compile_tx_script("@transaction_script pub proc main nop end")
736738
.context("failed to compile tx script with advice map")?;
737739

738740
let mast = script.mast();
@@ -753,7 +755,7 @@ mod tests {
753755

754756
let script = CodeBuilder::default()
755757
.with_extended_advice_map(advice_map)
756-
.compile_tx_script("begin nop end")
758+
.compile_tx_script("@transaction_script pub proc main nop end")
757759
.context("failed to compile tx script")?;
758760

759761
let mast = script.mast();

crates/miden-standards/src/tx_script/expiration_script.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ use miden::protocol::tx
2222
#! - delta is 0 or not a u32 in the range 1..=0xFFFF (ERR_TX_INVALID_EXPIRATION_DELTA).
2323
#!
2424
#! Invocation: call
25-
begin
25+
@transaction_script
26+
pub proc main
2627
exec.tx::update_expiration_block_delta
2728
# => [pad(16)]
2829
end

crates/miden-standards/src/tx_script/send_notes_script.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ use crate::errors::CodeBuilderError;
3737
/// [`FungibleFaucet`]:
3838
///
3939
/// ```masm
40-
/// begin
40+
/// @transaction_script
41+
/// pub proc main
4142
/// push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta
4243
///
4344
/// push.{note information}
@@ -101,7 +102,8 @@ impl SendNotesTransactionScript {
101102
return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
102103
};
103104

104-
let script = format!("begin\n{expiration_prelude}\n{body}\nend");
105+
let script =
106+
format!("@transaction_script\npub proc main\n{expiration_prelude}\n{body}\nend");
105107

106108
let mut code_builder = CodeBuilder::new();
107109
for note in output_notes {

crates/miden-testing/src/kernel_tests/block/utils.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ fn update_expiration_tx_script(expiration_delta: u16) -> TransactionScript {
103103
"
104104
use miden::protocol::tx
105105
106-
begin
106+
@transaction_script
107+
pub proc main
107108
push.{expiration_delta}
108109
exec.tx::update_expiration_block_delta
109110
end

0 commit comments

Comments
 (0)