Skip to content

feat: add SendNotesTransactionScript to standards package - #3250

Merged
bobbinth merged 34 commits into
nextfrom
tomasarrachea-send-notes-migration
Aug 4, 2026
Merged

feat: add SendNotesTransactionScript to standards package#3250
bobbinth merged 34 commits into
nextfrom
tomasarrachea-send-notes-migration

Conversation

@TomasArrachea

@TomasArrachea TomasArrachea commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #3295.

Migrates the send-notes transaction script to a single, pre-built script.

Before, the script was generated by the CodeBuilder for each set of notes, so every transaction produced a different script. Now there is one fixed script for wallets and one for faucets. The notes to send are passed in as data and validated against a commitment, instead of being baked into the script itself.

@TomasArrachea
TomasArrachea changed the base branch from next to tomasarrachea-migrate-to-package July 8, 2026 21:49
Base automatically changed from tomasarrachea-migrate-to-package to next July 9, 2026 13:20
@TomasArrachea
TomasArrachea force-pushed the tomasarrachea-send-notes-migration branch from 612fce3 to 0067e24 Compare July 23, 2026 20:39
@TomasArrachea
TomasArrachea marked this pull request as ready for review July 23, 2026 20:39
@bobbinth

Copy link
Copy Markdown
Contributor

@TomasArrachea - is this ready for review or still being updated?

@TomasArrachea

Copy link
Copy Markdown
Collaborator Author

@TomasArrachea - is this ready for review or still being updated?

It's ready for review.

Comment thread crates/miden-standards/src/tx_script/send_notes_script.rs Outdated
Comment thread crates/miden-standards/asm/standards/tx_scripts/mod.masm Outdated
Comment on lines +63 to +68
/// ```ignore
/// let script = SendNotesTransactionScript::new(&interface, &notes)?;
/// let context = build_tx_context(/* .. */)
/// .tx_script(script.tx_script().clone())
/// .tx_script_args(script.tx_script_args())
/// .extend_advice_map(script.advice_entries().to_vec());

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.

This example references the deprecated build_tx_context function. Also, I don't think this example really illustrates how to use the API that well, I think we would want to use the new builder function instead?

Comment thread crates/miden-testing/tests/scripts/send_note.rs Outdated
# =================================================================================================

# Number of elements in a word.
pub const WORD_NUM_ELEMENTS = 4

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.

Let's define this in utils.masm instead.

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.

This is blocked for now due to 0xMiden/miden-vm#3429

Comment thread crates/miden-standards/asm/standards/tx_scripts/send_notes_common.masm Outdated

@PhilippGackstatter PhilippGackstatter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! I left a few suggestions.

Comment on lines +24 to +25
# Number of elements in a word.
pub const WORD_NUM_ELEMENTS = 4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe we can import this from miden::protocol::constants.

Comment thread crates/miden-standards/asm/standards/tx_scripts/mod.masm Outdated
#! Inputs:
#! Operand stack: [PAYLOAD_COMMITMENT, pad(12)]
#! Advice map: {
#! PAYLOAD_COMMITMENT: [[payload]]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I think [num_elements, [payload]] might be slightly more accurate here, as it explicitly names the number of elements which the code relies on.

Comment thread crates/miden-standards/asm/standards/tx_scripts/send_notes/mod.masm Outdated
# word 0: RECIPIENT
# word 1: [tag, note_type, num_assets, num_attachments]
# num_assets * 2 words: ASSET_ID, ASSET_VALUE
# num_attachments * 2 words: [attachment_scheme, 0, 0, 0], ATTACHMENT_COMMITMENT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In the code below, I think it might be good to also check that (num_assets+num_attachments) * ITEM_NUM_ELEMENTS == num_elements - ITEMS_OFFSET. This way, the loops that assume num_assets and num_attachments are valid do not accidentally operate on uninitialized memory.

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.

Added a validate_note_records procedure with this check.

Comment thread crates/miden-standards/src/tx_script/send_notes_script.rs
Comment thread crates/miden-testing/tests/scripts/send_note.rs Outdated
Comment thread crates/miden-testing/tests/scripts/send_note.rs Outdated
Comment thread crates/miden-testing/tests/scripts/send_note.rs Outdated
Comment on lines +122 to +129
/// The [`TransactionScriptRoot`] of the canonical wallet script.
pub fn wallet_script_root() -> TransactionScriptRoot {
SEND_NOTES_WALLET_TX_SCRIPT.root()
}

/// The [`TransactionScriptRoot`] of the canonical faucet script.
pub fn faucet_script_root() -> TransactionScriptRoot {
SEND_NOTES_FAUCET_TX_SCRIPT.root()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This works, but it also breaks the convention we've had so far that each *Note and *TransactionScript type maps to exactly one script, and I think it would be good to try to keep that directionally.

Maybe the best solution here is to have dedicated types for each kind of script plus an abstraction type:

pub enum SendNotesTransactionScript {
    Wallet(SendWalletNotesTransactionScript),
    Fungible(SendFungibleFaucetNotesTransactionScript),
    // In the future we'd also have:
    NonFungible(SendNonFungibleFaucetNotesTransactionScript),
}

impl SendNotesTransactionScript {
    pub fn script_roots() -> [TransactionScriptRoot; 3] { ... }
}

SendNotesTransactionScript can hold the abstraction over the concrete account code interface, but we'd still have dedicated types for each concrete script, which should end up being thin wrappers around a shared core.

Comment on lines +28 to +31
# The number of field elements in a word. Mirrors `miden::protocol::constants::WORD_NUM_ELEMENTS`,
# which cannot be used here: the assembler resolves imported constants only in instruction
# immediates, so the offset chain below cannot be derived from one.
pub const WORD_NUM_ELEMENTS = 4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FYI: Opened 0xMiden/miden-vm#3429 for this.

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.

Thanks for looking into it!

@PhilippGackstatter

Copy link
Copy Markdown
Contributor

@TomasArrachea Please re-request reviews from us when this is ready for another round. I see that some comments have been addressed but not all, so I'm assuming it is not yet ready.

@PhilippGackstatter PhilippGackstatter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great, thank you for addressing all the comments!

I left a few suggestions. The main comment to address is probably checking that we only create scripts with notes that contain fungible assets against fungible faucets (and same for non-fungible ones).

Comment thread crates/miden-standards/asm/standards/tx_scripts/send_notes/common.masm Outdated
Comment thread crates/miden-standards/asm/standards/tx_scripts/send_notes/common.masm Outdated
Comment thread crates/miden-standards/asm/standards/tx_scripts/send_notes/common.masm Outdated
Comment thread crates/miden-standards/src/tx_script/send_notes_script.rs Outdated
Comment thread crates/miden-standards/src/tx_script/send_notes_script.rs Outdated
Comment thread crates/miden-standards/src/tx_script/send_notes_script.rs Outdated
Comment thread crates/miden-testing/tests/scripts/send_note.rs Outdated

@PhilippGackstatter PhilippGackstatter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great, thanks!

Comment on lines +180 to +197
if interface.contains([FungibleFaucet::mint_and_send_root()]) {
SendFungibleFaucetNotesTransactionScript::build(
interface,
output_notes,
expiration_delta,
)
.map(Self::Fungible)
} else if interface.contains([NonFungibleFaucet::mint_and_send_root()]) {
SendNonFungibleFaucetNotesTransactionScript::build(
interface,
output_notes,
expiration_delta,
)
.map(Self::NonFungible)
} else {
SendWalletNotesTransactionScript::build(interface, output_notes, expiration_delta)
.map(Self::Wallet)
}

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.

As discussed offline with @igamigo, this probably needs fixing. Here, SendNotesTransactionScript::build() checks which interface the sender implements in order to pick the script, but it does not take into account that a single account can implement multiple interfaces. An account that is both fungible and non-fungible faucet would always fall in the fungible if branch. We could check the asset being sent, and depending on its composition we would pick the script to use.

Also, an account can simultaneously be a faucet and a wallet, so we cannot simply determine if the send notes is supposed to mint the asset or take it from the account vault. The problematic case is one where the faucet holds in its vault the same asset that it mints, so we would have no way of determining which to use. Maybe this edge case can be dismissed? Or we could introduce an enum to specify if the script should mint or get assets from the vault. In any case, this could be left for a follow-up PR.

@PhilippGackstatter PhilippGackstatter Aug 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good point and thanks for bringing this up. I think the current approach matches the logic we have for the MINT note:

# Determine the faucet kind.
procref.fungible::mint_and_send
# => [FUNGIBLE_MINT_AND_SEND_ROOT, pad(16)]
call.code_inspection::has_procedure
# => [is_fungible, pad(19)]
movdn.4 dropw
# => [is_fungible, pad(15)]
if.true
exec.mint_fungible::mint
# => [pad(16)]
else
# require that the account exposes the non-fungible mint_and_send.
procref.non_fungible::mint_and_send
# => [NON_FUNGIBLE_MINT_AND_SEND_ROOT, pad(16)]
call.code_inspection::has_procedure
# => [is_non_fungible, pad(19)]
movdn.4 dropw
# => [is_non_fungible, pad(15)]
assert.err=ERR_MINT_UNSUPPORTED_FAUCET
# => [pad(16)]
exec.mint_non_fungible::mint
# => [pad(16)]

On the other hand, in Rust we could do a few things:

  • be more defensive and opinionated and say that the SendNotesTransactionScript only works for accounts that expose exactly one interface (wallet xor fungible faucet xor non-fungible faucet). I.e. return an error in this case. This is the simplest.
  • we can try to define a precedence, i.e. if an account is both a faucet and a wallet, use the wallet over the faucet or the other way around. There is probably no clear answer here which is better, we'd just need to define something.
  • As you suggested, check compatibility between asset composition and faucet type. In this case, we'd need to ignore or error out if the faucet is also a wallet.

Finally, I would make the assumption that users adhere to this:

It is highly recommended that accounts issue only one type of asset, in order to have a simple 1-to-1 relationship between faucets and asset types.

https://docs.miden.xyz/reference/protocol/asset#issuance

So a faucet can be assumed not to be both fungible and non-fungible (and if so, it's fine to error out).

Overall, I'd probably go with the compatibility check, basically:

  • all_notes_have_one_asset && all_assets_have_composition_fungible && is_fungible_faucet_interface -> fungible faucet mint
  • all_notes_have_one_asset && all_assets_have_composition_none && is_non_fungible_faucet_interface -> non-fungible faucet mint
  • is_wallet_interface -> send from wallet
  • all other cases -> error

So the faucet logic would take precedence over the wallet logic.

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.

Thanks! I agree, implemented it in e1f173f.

@bobbinth bobbinth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! Thank you! Not a super detailed review - but I left a few comments inline. All can be addressed in a follow-up (if needed).

Also, a lot of the complexity here comes from the fact that we want to support minting directly from the faucet account via a tx script. We are moving away from this pattern and I think this could be something we can remove relatively soon. This would simplify structure quite a bit.

#! - the expiration delta is non-zero and not in the range 1..=0xFFFF.
#!
#! Invocation: exec
pub proc load_payload(payload_commitment: word)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I would maybe name this write_payload_to_memory or maybe just write_payload. In general, I think we can adopt a convention of using the "write" verb whenever we write something to memory.

Also, I would have maybe made the PAYLOAD_ADDR be an input parameter (so that the caller specifies the memory address into which to write the payload).

use miden::protocol::asset
use miden::standards::note::note_creator
use miden::standards::tx_scripts::send_notes::common
use miden::standards::wallets::basic

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I would probably alias this as basic_wallet to make the code later in this module a bit easier to read.

# note creation.

use miden::protocol::asset
use miden::standards::faucets::fungible

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar nit as above: I'd probably alias this module as fungible_faucet to make code further in this file a bit easier to read.

# note creation. Unlike the fungible faucet, `non_fungible::mint_and_send` derives the asset from
# the faucet itself, so only the asset's value word (its commitment) is read from the record.

use miden::standards::faucets::non_fungible

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same nit as above.

@bobbinth
bobbinth added this pull request to the merge queue Aug 4, 2026
Merged via the queue into next with commit edf3767 Aug 4, 2026
19 checks passed
@bobbinth
bobbinth deleted the tomasarrachea-send-notes-migration branch August 4, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate the send notes transaction script to project assembly

4 participants