feat: add SendNotesTransactionScript to standards package - #3250
Conversation
612fce3 to
0067e24
Compare
|
@TomasArrachea - is this ready for review or still being updated? |
It's ready for review. |
| /// ```ignore | ||
| /// let script = SendNotesTransactionScript::new(&interface, ¬es)?; | ||
| /// 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()); |
There was a problem hiding this comment.
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?
| # ================================================================================================= | ||
|
|
||
| # Number of elements in a word. | ||
| pub const WORD_NUM_ELEMENTS = 4 |
There was a problem hiding this comment.
Let's define this in utils.masm instead.
There was a problem hiding this comment.
This is blocked for now due to 0xMiden/miden-vm#3429
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good! I left a few suggestions.
| # Number of elements in a word. | ||
| pub const WORD_NUM_ELEMENTS = 4 |
There was a problem hiding this comment.
I believe we can import this from miden::protocol::constants.
| #! Inputs: | ||
| #! Operand stack: [PAYLOAD_COMMITMENT, pad(12)] | ||
| #! Advice map: { | ||
| #! PAYLOAD_COMMITMENT: [[payload]] |
There was a problem hiding this comment.
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.
| # 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Added a validate_note_records procedure with this check.
| /// 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() |
There was a problem hiding this comment.
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.
| # 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 |
There was a problem hiding this comment.
Thanks for looking into it!
|
@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
left a comment
There was a problem hiding this comment.
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).
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks great, thanks!
| 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) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Good point and thanks for bringing this up. I think the current approach matches the logic we have for the MINT note:
protocol/crates/miden-standards/asm/standards/notes/mint.masm
Lines 42 to 70 in 15d0cc8
On the other hand, in Rust we could do a few things:
- be more defensive and opinionated and say that the
SendNotesTransactionScriptonly 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 mintall_notes_have_one_asset && all_assets_have_composition_none && is_non_fungible_faucet_interface-> non-fungible faucet mintis_wallet_interface-> send from wallet- all other cases -> error
So the faucet logic would take precedence over the wallet logic.
There was a problem hiding this comment.
Thanks! I agree, implemented it in e1f173f.
bobbinth
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
Closes #3295.
Migrates the send-notes transaction script to a single, pre-built script.
Before, the script was generated by the
CodeBuilderfor 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.