diff --git a/CHANGELOG.md b/CHANGELOG.md index 82bffbea64..0594f2f3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Added `CodeBuilder::with_warnings_as_errors()` to promote assembler warning diagnostics to errors ([#2558](https://github.com/0xMiden/protocol/pull/2558)). - Added `MockChain::add_pending_batch()` to allow submitting user batches directly ([#2565](https://github.com/0xMiden/protocol/pull/2565)). - Added `create_fungible_key` for construction of fungible asset keys ([#2575](https://github.com/0xMiden/protocol/pull/2575)). +- Implemented the `on_before_asset_added_to_account` asset callback ([#2571](https://github.com/0xMiden/protocol/pull/2571)). - Added `InputNoteCommitment::from_parts()` for construction of input note commitments from a nullifier and optional note header ([#2588](https://github.com/0xMiden/protocol/pull/2588)). - Added `bool` schema type to the type registry and updated ACL auth component to use it for boolean config fields ([#2591](https://github.com/0xMiden/protocol/pull/2591)). - Added `component_metadata()` to all account components to expose their metadata ([#2596](https://github.com/0xMiden/protocol/pull/2596)). @@ -89,6 +90,8 @@ - Explicitly use `get_native_account_active_storage_slots_ptr` in `account::set_item` and `account::set_map_item`. - Added Ownable2Step as an Account Component ([#2572](https://github.com/0xMiden/protocol/pull/2572)) - [BREAKING] Introduced `PrivateNoteHeader` for output notes and removed `RawOutputNote::Header` variant ([#2569](https://github.com/0xMiden/protocol/pull/2569)). +- [BREAKING] Changed `asset::create_fungible_asset` and `faucet::create_fungible_asset` signature to take `enable_callbacks` flag ([#2571](https://github.com/0xMiden/protocol/pull/2571)). + - [BREAKING] Fixed `TokenSymbol::try_from(Felt)` to reject values below `MIN_ENCODED_VALUE`; implemented `Display` for `TokenSymbol` replacing the fallible `to_string()` method; removed `Default` derive ([#2464](https://github.com/0xMiden/protocol/issues/2464)). ## 0.13.3 (2026-01-27) diff --git a/bin/bench-transaction/bench-tx.json b/bin/bench-transaction/bench-tx.json index c1dbe2abc3..67dbed5252 100644 --- a/bin/bench-transaction/bench-tx.json +++ b/bin/bench-transaction/bench-tx.json @@ -1,40 +1,40 @@ { "consume single P2ID note": { - "prologue": 3173, - "notes_processing": 1714, + "prologue": 3487, + "notes_processing": 1831, "note_execution": { - "0xa030091e37d38b506d764d5666f3a13af9e5702a0159974a3bc27053d7a55e01": 1674 + "0x1421e92d0f84f11b3e6f84e4e1d193e648eb820666ffb8c50ea818c25a32990c": 1791 }, "tx_script_processing": 42, "epilogue": { - "total": 63977, - "auth_procedure": 62667, - "after_tx_cycles_obtained": 574 + "total": 71195, + "auth_procedure": 69694, + "after_tx_cycles_obtained": 608 } }, "consume two P2ID notes": { - "prologue": 4131, - "notes_processing": 3431, + "prologue": 4509, + "notes_processing": 3668, "note_execution": { - "0x209ecf97790d4328e60a3b15160760934383ecff02550cb5df72e3f6d459fa70": 1708, - "0x4f9da5658d9f717fdcfa674906e92a7424d86da93f3a21fe0362a220f0e457b7": 1674 + "0x702c078c74683d33b507e16d9fc67f0be0cc943cd94c1f652e3a60e0f4164d9f": 1791, + "0x92cc0c8c208e3b8bad970c23b2c4b4c24cc8d42626b3f56363ce1a6bbf4c7ac2": 1828 }, "tx_script_processing": 42, "epilogue": { - "total": 63949, - "auth_procedure": 62653, - "after_tx_cycles_obtained": 574 + "total": 71143, + "auth_procedure": 69668, + "after_tx_cycles_obtained": 608 } }, "create single P2ID note": { - "prologue": 1681, + "prologue": 1766, "notes_processing": 32, "note_execution": {}, - "tx_script_processing": 1497, + "tx_script_processing": 1682, "epilogue": { - "total": 64803, - "auth_procedure": 62899, - "after_tx_cycles_obtained": 574 + "total": 72099, + "auth_procedure": 69906, + "after_tx_cycles_obtained": 608 } } } \ No newline at end of file diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index d4020aee1d..19c75a36c9 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -813,6 +813,24 @@ pub proc faucet_burn_asset # => [ASSET_VALUE, pad(12)] end +#! Returns whether the active account defines callbacks. +#! +#! Inputs: [pad(16)] +#! Outputs: [has_callbacks, pad(15)] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +#! +#! Invocation: dynexec +pub proc faucet_has_callbacks + exec.account::has_callbacks + # => [has_callbacks, pad(16)] + + # truncate the stack + swap drop + # => [has_callbacks, pad(15)] +end + # INPUT NOTE # ------------------------------------------------------------------------------------------------- @@ -1484,6 +1502,10 @@ pub proc tx_exec_foreign_proc # end the foreign context exec.tx::end_foreign_context # => [foreign_procedure_outputs(16)] + + # clear the foreign procedure ID and root in memory + exec.tx::clear_fpi_memory + # => [foreign_procedure_outputs(16)] end #! Updates the transaction expiration block delta. diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index efa5408911..a14c436e33 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -1,6 +1,8 @@ use $kernel::account_delta use $kernel::account_id use $kernel::asset_vault +use $kernel::callbacks +use $kernel::callbacks::ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT use $kernel::constants::ACCOUNT_PROCEDURE_DATA_LENGTH use $kernel::constants::EMPTY_SMT_ROOT use $kernel::constants::STORAGE_SLOT_TYPE_MAP @@ -425,7 +427,7 @@ pub proc get_item exec.memory::get_account_active_storage_slots_section_ptr # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] - exec.find_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr] # get the item from storage @@ -433,6 +435,43 @@ pub proc get_item # => [VALUE] end +#! Finds an item in the active account's storage by slot ID, returning whether the slot was found +#! along with its value. +#! +#! Unlike `get_item`, this procedure does not panic if the slot does not exist. Instead, it +#! returns `is_found = 0` and the empty word. +#! +#! Inputs: [slot_id_suffix, slot_id_prefix] +#! Outputs: [is_found, VALUE] +#! +#! Where: +#! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - is_found is 1 if the slot was found, 0 otherwise. +#! - VALUE is the value of the item, or the empty word if the slot was not found. +pub proc find_item + # get account storage slots section offset + exec.memory::get_account_active_storage_slots_section_ptr + # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] + + exec.find_storage_slot + # => [is_found, slot_ptr] + + if.true + # slot was found, read its value + exec.get_item_raw + # => [VALUE] + + push.1 + # => [is_found = 1, VALUE] + else + # slot was not found, drop slot_ptr and return empty word + drop padw push.0 + # => [is_found = 0, EMPTY_WORD] + end + # => [is_found, VALUE] +end + #! Gets an item and its slot type from the account storage. #! #! Inputs: [slot_id_suffix, slot_id_prefix] @@ -451,7 +490,7 @@ pub proc get_typed_item exec.memory::get_account_active_storage_slots_section_ptr # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] - exec.find_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr] dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load @@ -479,7 +518,7 @@ pub proc get_initial_item exec.memory::get_account_initial_storage_slots_ptr # => [account_initial_storage_slots_ptr, slot_id_suffix, slot_id_prefix] - exec.find_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr] # get the item from initial storage @@ -508,7 +547,7 @@ pub proc set_item exec.memory::get_native_account_active_storage_slots_ptr # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, VALUE] - exec.find_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr, VALUE] # load the slot type @@ -603,7 +642,7 @@ pub proc set_map_item # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, KEY, NEW_VALUE] # resolve the slot name to its pointer - exec.find_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr, KEY, NEW_VALUE] # load the slot type @@ -666,31 +705,38 @@ end #! added. #! - the vault already contains the same non-fungible asset. pub proc add_asset_to_vault + swapw dupw.1 + # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY] + + exec.callbacks::on_before_asset_added_to_account + swapw + # => [ASSET_KEY, PROCESSED_ASSET_VALUE] + # duplicate the asset for the later event and delta update dupw.1 dupw.1 - # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, ASSET_KEY, PROCESSED_ASSET_VALUE] # push the account vault root ptr exec.memory::get_account_vault_root_ptr movdn.8 - # => [ASSET_KEY, ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, PROCESSED_ASSET_VALUE] # emit event to signal that an asset is going to be added to the account vault emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT - # => [ASSET_KEY, ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, PROCESSED_ASSET_VALUE] # add the asset to the account vault exec.asset_vault::add_asset - # => [ASSET_VALUE', ASSET_KEY, ASSET_VALUE] + # => [PROCESSED_ASSET_VALUE', ASSET_KEY, PROCESSED_ASSET_VALUE] movdnw.2 - # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE'] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, PROCESSED_ASSET_VALUE'] # emit event to signal that an asset is being added to the account vault emit.ACCOUNT_VAULT_AFTER_ADD_ASSET_EVENT - # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE'] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, PROCESSED_ASSET_VALUE'] exec.account_delta::add_asset - # => [ASSET_VALUE'] + # => [PROCESSED_ASSET_VALUE'] end #! Removes the specified asset from the account vault. @@ -1581,7 +1627,7 @@ end #! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. proc get_map_item_raw - exec.find_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr, KEY] emit.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT @@ -1613,20 +1659,18 @@ proc get_map_item_raw # => [VALUE] end -#! Finds the slot identified by the key [slot_id_prefix, slot_id_suffix, 0, 0] (stack order) and -#! returns the pointer to that slot. +#! Finds the slot identified by the key [_, _, slot_id_suffix, slot_id_prefix] and returns a flag +#! indicating whether the slot was found and the pointer to that slot. #! #! Inputs: [storage_slots_ptr, slot_id_suffix, slot_id_prefix] -#! Outputs: [slot_ptr] +#! Outputs: [is_found, slot_ptr] #! #! Where: #! - storage_slots_ptr is the pointer to the storage slots section. +#! - is_found is 1 if the slot was found, 0 otherwise. #! - slot_ptr is the pointer to the resolved storage slot. #! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are #! the first two felts of the hashed slot name. -#! -#! Panics if: -#! - a slot with the provided slot ID does not exist in account storage. proc find_storage_slot # construct the start and end pointers of the storage slot section in which we will search dup exec.memory::get_num_storage_slots mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH add @@ -1635,15 +1679,33 @@ proc find_storage_slot swap movup.3 movup.3 # => [slot_id_suffix, slot_id_prefix, storage_slots_start_ptr, storage_slots_end_ptr] - # find the slot whose slot key matches [_, _, slot_id_suffix, slot_id_prefix] # if the slot key does not exist, this procedure will validate its absence exec.sorted_array::find_half_key_value # => [is_slot_found, slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] - assert.err=ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME - # => [slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] + movup.2 drop movup.2 drop + # => [is_slot_found, slot_ptr] +end + +#! Finds the slot identified by the key [_, _, slot_id_suffix, slot_id_prefix] and returns the +#! pointer to that slot. +#! +#! Inputs: [storage_slots_ptr, slot_id_suffix, slot_id_prefix] +#! Outputs: [slot_ptr] +#! +#! Where: +#! - storage_slots_ptr is the pointer to the storage slots section. +#! - slot_ptr is the pointer to the resolved storage slot. +#! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +proc get_storage_slot_ptr + exec.find_storage_slot + # => [is_found, slot_ptr] - swap.2 drop drop + assert.err=ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME # => [slot_ptr] end @@ -1988,6 +2050,45 @@ pub proc has_procedure # => [is_procedure_available'] end +# CALLBACKS +# ------------------------------------------------------------------------------------------------- + +#! Returns whether the active account defines callbacks. +#! +#! Inputs: [] +#! Outputs: [has_callbacks] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +pub proc has_callbacks + # check if the on_before_asset_added_to_account callback slot exists and is non-empty + push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] + exec.has_non_empty_slot + # => [has_callbacks] +end + +#! Checks whether a storage slot with the given slot ID exists in the active account's storage +#! and has a non-empty value. +#! +#! Inputs: [slot_id_suffix, slot_id_prefix] +#! Outputs: [has_non_empty_value] +#! +#! Where: +#! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - has_non_empty_value is 1 if the slot exists and its value is non-empty, 0 otherwise. +proc has_non_empty_slot + exec.find_item + # => [is_found, VALUE] + + # check if is_found && value is non-empty + movdn.4 exec.word::eqz not + # => [is_non_empty_value, is_found] + + and + # => [has_non_empty_value] +end + #! Returns the key built from the provided account ID for use in the advice map or the account #! tree. #! diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm index 7b9f7d6762..f1a02531af 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm @@ -338,35 +338,35 @@ proc update_fungible_asset_delta movup.8 loc_store.1 # => [KEY, VALUE0, ...] # this stack state is equivalent to: - # => [[0, 0, faucet_id_suffix, faucet_id_prefix], [delta_amount, 0, 0, 0], ...] + # => [[0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix], [delta_amount, 0, 0, 0], ...] swapw - # => [[delta_amount, 0, 0, 0], [0, 0, faucet_id_suffix, faucet_id_prefix], ...] + # => [[delta_amount, 0, 0, 0], [0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # compute the absolute value of delta amount with a flag indicating whether it's positive exec.delta_amount_absolute - # => [is_delta_amount_positive, [delta_amount_abs, 0, 0, 0], [0, 0, faucet_id_suffix, faucet_id_prefix], ...] + # => [is_delta_amount_positive, [delta_amount_abs, 0, 0, 0], [0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # define the was_added value as equivalent to is_delta_amount_positive # this value is 1 if the amount was added and 0 if the amount was removed swap.6 drop - # => [[delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix, faucet_id_prefix], ...] + # => [[delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] dup neq.0 - # => [is_delta_amount_non_zero, [delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix, faucet_id_prefix], ...] + # => [is_delta_amount_non_zero, [delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # if delta amount is non-zero, update the hasher if.true push.DOMAIN_ASSET swap.5 drop - # => [[delta_amount_abs, 0, 0, 0], [domain, was_added, faucet_id_suffix, faucet_id_prefix], ...] + # => [[delta_amount_abs, 0, 0, 0], [domain, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # swap value and metadata words swapw - # => [[domain, was_added, faucet_id_suffix, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], RATE0, RATE1, CAPACITY] + # => [[domain, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], RATE0, RATE1, CAPACITY] # drop previous RATE elements swapdw dropw dropw - # => [[domain, was_added, faucet_id_suffix, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], CAPACITY] + # => [[domain, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], CAPACITY] exec.poseidon2::permute # => [RATE0, RATE1, CAPACITY] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm index 193af534b7..b45b093c61 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm @@ -17,6 +17,7 @@ pub use ::$kernel::util::asset::key_to_faucet_id pub use ::$kernel::util::asset::key_into_faucet_id pub use ::$kernel::util::asset::key_to_asset_id pub use ::$kernel::util::asset::key_into_asset_id +pub use ::$kernel::util::asset::key_to_callbacks_enabled pub use ::$kernel::util::asset::store pub use ::$kernel::util::asset::load diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm new file mode 100644 index 0000000000..8739235d3b --- /dev/null +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -0,0 +1,123 @@ +use $kernel::tx +use $kernel::asset +use $kernel::account +use miden::core::word + +# CONSTANTS +# ================================================================================================== + +# The index of the local memory slot that contains the procedure root of the callback. +const CALLBACK_PROC_ROOT_LOC = 0 + +# The name of the storage slot where the procedure root for the on_before_asset_added_to_account callback +# is stored. +pub const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT = word("miden::protocol::faucet::callback::on_before_asset_added_to_account") + +# PROCEDURES +# ================================================================================================== + +#! Invokes the `on_before_asset_added_to_account` callback on the faucet that issued the asset, +#! if the asset has callbacks enabled. +#! +#! The callback invocation is skipped in these cases: +#! - If the global callback flag in the asset key is `Disabled`. +#! - If the faucet does not have the callback storage slot. +#! - If the callback storage slot contains the empty word. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [PROCESSED_ASSET_VALUE] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset being added. +#! - ASSET_VALUE is the value of the asset being added. +#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original +#! ASSET_VALUE if callbacks are disabled. +pub proc on_before_asset_added_to_account + exec.asset::key_to_callbacks_enabled + # => [callbacks_enabled, ASSET_KEY, ASSET_VALUE] + + if.true + exec.on_before_asset_added_to_account_raw + # => [PROCESSED_ASSET_VALUE] + else + # drop asset key + dropw + # => [ASSET_VALUE] + end + # => [PROCESSED_ASSET_VALUE] +end + +#! Executes the `on_before_asset_added_to_account` callback by starting a foreign context against +#! the faucet, reading the callback procedure root from the faucet's storage, and invoking it via +#! `dyncall`. +#! +#! If the faucet does not have the callback storage slot, or if the slot contains the empty word, +#! the callback is skipped and the original ASSET_VALUE is returned. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [PROCESSED_ASSET_VALUE] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset being added. +#! - ASSET_VALUE is the value of the asset being added. +#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original +#! ASSET_VALUE if no callback is configured. +@locals(4) +proc on_before_asset_added_to_account_raw + exec.start_foreign_callback_context + # => [ASSET_KEY, ASSET_VALUE] + + # try to find the callback procedure root in the faucet's storage + push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] + exec.account::find_item + # => [is_found, PROC_ROOT, ASSET_KEY, ASSET_VALUE] + + movdn.4 exec.word::testz not + # => [is_non_empty_word, PROC_ROOT, is_found, ASSET_KEY, ASSET_VALUE] + + # invoke the callback if is_found && is_non_empty_word + movup.5 and + # => [should_invoke, PROC_ROOT, ASSET_KEY, ASSET_VALUE] + + # only invoke the callback if the procedure root is not the empty word + if.true + # prepare for dyncall by storing procedure root in local memory + loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw + # => [ASSET_KEY, ASSET_VALUE] + + # pad the stack to 16 for the call (excluding the proc root ptr) + padw padw swapdw + locaddr.CALLBACK_PROC_ROOT_LOC + # => [callback_proc_root_ptr, ASSET_KEY, ASSET_VALUE, pad(8)] + + # invoke the callback + dyncall + # => [PROCESSED_ASSET_VALUE, pad(12)] + + # truncate the stack after the call + swapdw dropw dropw swapw dropw + # => [PROCESSED_ASSET_VALUE] + else + # drop proc root and asset key + dropw dropw + # => [ASSET_VALUE] + end + # => [PROCESSED_ASSET_VALUE] + + exec.tx::end_foreign_context + # => [PROCESSED_ASSET_VALUE] +end + +#! Prepares the invocation of a faucet callback by starting a foreign context against the faucet +#! identified by the asset key's faucet ID. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +proc start_foreign_callback_context + exec.asset::key_to_faucet_id + # => [faucet_id_suffix, faucet_id_prefix, ASSET_KEY, ASSET_VALUE] + + # start a foreign context against the faucet + exec.tx::start_foreign_context + # => [ASSET_KEY, ASSET_VALUE] +end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 6b7ba6a647..4ac2b7ceea 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -49,7 +49,7 @@ const SMT_SET_ADDITIONAL_CYCLES=250 # that this includes at least smt::set's best case number of cycles. # This can be _estimated_ using the transaction measurements on ExecutedTransaction and can be set # to the lowest observed value. -const NUM_POST_COMPUTE_FEE_CYCLES=500 +const NUM_POST_COMPUTE_FEE_CYCLES=608 # The number of cycles the epilogue is estimated to take after compute_fee has been executed. const ESTIMATED_AFTER_COMPUTE_FEE_CYCLES=NUM_POST_COMPUTE_FEE_CYCLES+SMT_SET_ADDITIONAL_CYCLES @@ -291,6 +291,11 @@ proc create_native_fee_asset exec.memory::get_native_asset_id # => [native_asset_id_suffix, native_asset_id_prefix, fee_amount] + # assume the fee asset does not have callbacks + # this should be addressed more holistically with a fee construction refactor + push.0 + # => [enable_callbacks, native_asset_id_suffix, native_asset_id_prefix, fee_amount] + # SAFETY: native asset ID should be fungible and amount should not be exceeded exec.fungible_asset::create_unchecked # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm b/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm index 63abdb5f63..aaed2d01fc 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm @@ -205,9 +205,7 @@ end #! Ends a foreign account context. #! -#! This pops the top of the account stack, making the previous account the active account, and -#! resets the foreign procedure info (foreign account ID and foreign procedure root) in the kernel -#! memory. +#! This pops the top of the account stack, making the previous account the active account. #! #! Inputs: [] #! Outputs: [] @@ -219,7 +217,14 @@ end pub proc end_foreign_context exec.memory::pop_ptr_from_account_stack # => [] +end +#! Resets the foreign procedure info (foreign account ID and foreign procedure root) in the kernel +#! memory to zeros. +#! +#! Inputs: [] +#! Outputs: [] +proc clear_fpi_memory # set the upcoming foreign account ID to zero push.0 push.0 exec.memory::set_fpi_account_id # => [] diff --git a/crates/miden-protocol/asm/protocol/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm index 0285954f6c..724700ece3 100644 --- a/crates/miden-protocol/asm/protocol/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -512,6 +512,10 @@ pub proc get_balance assert.err=ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET # => [faucet_id_suffix, faucet_id_prefix] + # TODO(callbacks): This should take ASSET_KEY as input to avoid hardcoding the callbacks flag. + push.0 + # => [enable_callbacks = 0, faucet_id_suffix, faucet_id_prefix] + exec.asset::create_fungible_key # => [ASSET_KEY] @@ -544,6 +548,10 @@ pub proc get_initial_balance assert.err=ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET # => [faucet_id_suffix, faucet_id_prefix] + # TODO(callbacks): This should take ASSET_KEY as input to avoid hardcoding the callbacks flag. + push.0 + # => [enable_callbacks = 0, faucet_id_suffix, faucet_id_prefix] + exec.asset::create_fungible_key # => [ASSET_KEY] @@ -671,3 +679,4 @@ pub proc has_procedure swapdw dropw dropw swapw dropw movdn.3 drop drop drop # => [is_procedure_available] end + diff --git a/crates/miden-protocol/asm/protocol/asset.masm b/crates/miden-protocol/asm/protocol/asset.masm index 04211d61b1..6d78ddad69 100644 --- a/crates/miden-protocol/asm/protocol/asset.masm +++ b/crates/miden-protocol/asm/protocol/asset.masm @@ -11,6 +11,7 @@ pub use ::miden::protocol::util::asset::key_to_faucet_id pub use ::miden::protocol::util::asset::key_into_faucet_id pub use ::miden::protocol::util::asset::key_to_asset_id pub use ::miden::protocol::util::asset::key_into_asset_id +pub use ::miden::protocol::util::asset::key_to_callbacks_enabled pub use ::miden::protocol::util::asset::store pub use ::miden::protocol::util::asset::load pub use ::miden::protocol::util::asset::fungible_value_into_amount @@ -31,10 +32,11 @@ const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! Creates a fungible asset for the specified fungible faucet and amount. #! -#! Inputs: [faucet_id_suffix, faucet_id_prefix, amount] +#! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - faucet_id_{suffix,prefix} are the suffix and prefix felts of the faucet to create the asset #! for. #! - amount is the amount of the asset to create. @@ -44,17 +46,18 @@ const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! Panics if: #! - the provided faucet ID is not a fungible faucet. #! - the provided amount exceeds FUNGIBLE_ASSET_MAX_AMOUNT. +#! - enable_callbacks is not 0 or 1. #! #! Invocation: exec pub proc create_fungible_asset # assert the faucet is a fungible faucet - dup.1 exec.account_id::is_fungible_faucet assert.err=ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID - # => [faucet_id_suffix, faucet_id_prefix, amount] + dup.2 exec.account_id::is_fungible_faucet assert.err=ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] # assert the amount is valid - dup.2 lte.FUNGIBLE_ASSET_MAX_AMOUNT + dup.3 lte.FUNGIBLE_ASSET_MAX_AMOUNT assert.err=ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT - # => [faucet_id_suffix, faucet_id_prefix, amount] + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] # SAFETY: faucet ID and amount were validated exec.asset::create_fungible_asset_unchecked diff --git a/crates/miden-protocol/asm/protocol/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm index f412026a23..ea1135d036 100644 --- a/crates/miden-protocol/asm/protocol/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -2,6 +2,7 @@ use miden::protocol::asset use miden::protocol::active_account use ::miden::protocol::kernel_proc_offsets::FAUCET_MINT_ASSET_OFFSET use ::miden::protocol::kernel_proc_offsets::FAUCET_BURN_ASSET_OFFSET +use ::miden::protocol::kernel_proc_offsets::FAUCET_HAS_CALLBACKS_OFFSET #! Creates a fungible asset for the faucet the transaction is being executed against. #! @@ -22,6 +23,10 @@ pub proc create_fungible_asset exec.active_account::get_id # => [id_suffix, id_prefix, amount] + # check whether the faucet has callbacks defined + exec.has_callbacks + # => [has_callbacks, id_suffix, id_prefix, amount] + # create the fungible asset exec.asset::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE] @@ -124,3 +129,31 @@ pub proc burn swapdw dropw dropw swapw dropw # => [ASSET_VALUE] end + +#! Returns whether the active account defines callbacks. +#! +#! The account defines callbacks if any callback storage slot is present and it contains not the +#! empty word. +#! +#! Inputs: [] +#! Outputs: [has_callbacks] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +#! +#! Invocation: exec +pub proc has_callbacks + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + push.FAUCET_HAS_CALLBACKS_OFFSET + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [has_callbacks, pad(15)] + + # clean the stack + swapdw dropw dropw swapw dropw movdn.3 drop drop drop + # => [has_callbacks] +end diff --git a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm index 7e65b6b089..eeb370179c 100644 --- a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm +++ b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm @@ -47,44 +47,45 @@ pub const ACCOUNT_HAS_PROCEDURE_OFFSET=24 ### Faucet ###################################### pub const FAUCET_MINT_ASSET_OFFSET=25 pub const FAUCET_BURN_ASSET_OFFSET=26 +pub const FAUCET_HAS_CALLBACKS_OFFSET=27 ### Note ######################################## # input notes -pub const INPUT_NOTE_GET_METADATA_OFFSET=27 -pub const INPUT_NOTE_GET_ASSETS_INFO_OFFSET=28 -pub const INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET=29 -pub const INPUT_NOTE_GET_STORAGE_INFO_OFFSET=30 -pub const INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET=31 -pub const INPUT_NOTE_GET_RECIPIENT_OFFSET=32 +pub const INPUT_NOTE_GET_METADATA_OFFSET=28 +pub const INPUT_NOTE_GET_ASSETS_INFO_OFFSET=29 +pub const INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET=30 +pub const INPUT_NOTE_GET_STORAGE_INFO_OFFSET=31 +pub const INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET=32 +pub const INPUT_NOTE_GET_RECIPIENT_OFFSET=33 # output notes -pub const OUTPUT_NOTE_CREATE_OFFSET=33 -pub const OUTPUT_NOTE_GET_METADATA_OFFSET=34 -pub const OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=35 -pub const OUTPUT_NOTE_GET_RECIPIENT_OFFSET=36 -pub const OUTPUT_NOTE_ADD_ASSET_OFFSET=37 -pub const OUTPUT_NOTE_SET_ATTACHMENT_OFFSET=38 +pub const OUTPUT_NOTE_CREATE_OFFSET=34 +pub const OUTPUT_NOTE_GET_METADATA_OFFSET=35 +pub const OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=36 +pub const OUTPUT_NOTE_GET_RECIPIENT_OFFSET=37 +pub const OUTPUT_NOTE_ADD_ASSET_OFFSET=38 +pub const OUTPUT_NOTE_SET_ATTACHMENT_OFFSET=39 ### Tx ########################################## # input notes -pub const TX_GET_NUM_INPUT_NOTES_OFFSET=39 -pub const TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=40 +pub const TX_GET_NUM_INPUT_NOTES_OFFSET=40 +pub const TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=41 # output notes -pub const TX_GET_NUM_OUTPUT_NOTES_OFFSET=41 -pub const TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=42 +pub const TX_GET_NUM_OUTPUT_NOTES_OFFSET=42 +pub const TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=43 # block info -pub const TX_GET_BLOCK_COMMITMENT_OFFSET=43 -pub const TX_GET_BLOCK_NUMBER_OFFSET=44 -pub const TX_GET_BLOCK_TIMESTAMP_OFFSET=45 +pub const TX_GET_BLOCK_COMMITMENT_OFFSET=44 +pub const TX_GET_BLOCK_NUMBER_OFFSET=45 +pub const TX_GET_BLOCK_TIMESTAMP_OFFSET=46 # foreign context -pub const TX_PREPARE_FPI_OFFSET = 46 -pub const TX_EXEC_FOREIGN_PROC_OFFSET = 47 +pub const TX_PREPARE_FPI_OFFSET = 47 +pub const TX_EXEC_FOREIGN_PROC_OFFSET = 48 # expiration data -pub const TX_GET_EXPIRATION_DELTA_OFFSET=48 # accessor -pub const TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=49 # mutator +pub const TX_GET_EXPIRATION_DELTA_OFFSET=49 # accessor +pub const TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=50 # mutator diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index 620f70cf0e..fcf1af1421 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -1,3 +1,8 @@ +# ERRORS +# ================================================================================================= + +const ERR_VAULT_INVALID_ENABLE_CALLBACKS = "enable_callbacks must be 0 or 1" + # CONSTANTS # ================================================================================================= @@ -12,6 +17,12 @@ pub const ASSET_SIZE = 8 # The offset of the asset value in an asset stored in memory. pub const ASSET_VALUE_MEMORY_OFFSET = 4 +# The flag representing disabled callbacks. +pub const CALLBACKS_DISABLED = 0 + +# The flag representing enabled callbacks. +pub const CALLBACKS_ENABLED = 1 + # PROCEDURES # ================================================================================================= @@ -100,9 +111,12 @@ end #! - faucet_id is the account ID in the vault key. #! - ASSET_KEY is the vault key from which to extract the faucet ID. pub proc key_to_faucet_id - # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix, faucet_id_prefix] + # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] dup.3 dup.3 + # => [faucet_id_suffix_and_metadata, faucet_id_prefix, ASSET_KEY] + + exec.split_suffix_and_metadata drop # => [faucet_id_suffix, faucet_id_prefix, ASSET_KEY] end @@ -117,9 +131,12 @@ end #! - faucet_id is the account ID in the vault key. #! - ASSET_KEY is the vault key from which to extract the faucet ID. pub proc key_into_faucet_id - # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix, faucet_id_prefix] + # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] drop drop + # => [faucet_id_suffix_and_metadata, faucet_id_prefix] + + exec.split_suffix_and_metadata drop # => [faucet_id_suffix, faucet_id_prefix] end @@ -153,41 +170,80 @@ pub proc key_into_asset_id # => [asset_id_suffix, asset_id_prefix] end +#! Returns the asset callbacks flag from an asset vault key. +#! +#! Inputs: [ASSET_KEY] +#! Outputs: [callbacks_enabled, ASSET_KEY] +#! +#! Where: +#! - ASSET_KEY is the vault key from which to extract the metadata. +#! - callbacks_enabled is 1 if callbacks are enabled and 0 if disabled. +pub proc key_to_callbacks_enabled + # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] + + dup.2 + # => [faucet_id_suffix_and_metadata, ASSET_KEY] + + exec.split_suffix_and_metadata swap drop + # => [asset_metadata, ASSET_KEY] + + exec.metadata_into_callbacks_enabled + # => [callbacks_enabled, ASSET_KEY] +end + #! Creates a fungible asset vault key for the specified faucet. #! -#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix] #! Outputs: [ASSET_KEY] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - faucet_id_{suffix,prefix} are the suffix and prefix felts of the fungible faucet. #! - ASSET_KEY is the vault key for the fungible asset. #! +#! Panics if: +#! - enable_callbacks is not 0 or 1. +#! #! Invocation: exec pub proc create_fungible_key + exec.create_metadata + # => [asset_metadata, faucet_id_suffix, faucet_id_prefix] + + # merge the asset metadata into the lower 8 bits of the suffix + # this is safe since create_metadata builds only valid metadata + add + # => [faucet_id_suffix_and_metadata, faucet_id_prefix] + push.0.0 - # => [0, 0, faucet_id_suffix, faucet_id_prefix] + # => [0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix] # => [ASSET_KEY] end #! Creates a fungible asset for the specified fungible faucet and amount. #! -#! WARNING: Does not validate its inputs. +#! WARNING: Does not validate the faucet ID or amount. #! -#! Inputs: [faucet_id_suffix, faucet_id_prefix, amount] +#! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - faucet_id_{suffix,prefix} are the suffix and prefix felts of the faucet to create the asset #! for. #! - amount is the amount of the asset to create. #! - ASSET_KEY is the vault key of the created fungible asset #! - ASSET_VALUE is the value of the created fungible asset. #! +#! Panics if: +#! - enable_callbacks is not 0 or 1. +#! #! Invocation: exec pub proc create_fungible_asset_unchecked - # create the key and value - repeat.3 push.0 movdn.3 end - # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] + + # pad amount into ASSET_VALUE + repeat.3 push.0 movdn.4 end + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] exec.create_fungible_key # => [ASSET_KEY, ASSET_VALUE] @@ -215,3 +271,66 @@ pub proc create_non_fungible_asset_unchecked # => [[hash0, hash1, faucet_id_suffix, faucet_id_prefix], DATA_HASH] # => [ASSET_KEY, ASSET_VALUE] end + +#! Splits the merged faucet ID suffix and the asset metadata. +#! +#! Inputs: [faucet_id_suffix_and_metadata] +#! Outputs: [asset_metadata, faucet_id_suffix] +#! +#! Where: +#! - faucet_id_suffix_and_metadata is the faucet ID suffix merged with the asset metadata. +#! - faucet_id_suffix is the suffix of the account ID. +#! - asset_metadata is the asset metadata. +proc split_suffix_and_metadata + u32split + # => [suffix_metadata_lo, suffix_metadata_hi] + + dup movdn.2 + # => [suffix_metadata_lo, suffix_metadata_hi, suffix_metadata_lo] + + # clear lower 8 bits of the lo part to get the actual ID suffix + u32and.0xffffff00 swap + # => [suffix_metadata_hi, suffix_metadata_lo', suffix_metadata_lo] + + # reassemble the ID suffix by multiplying the hi part with 2^32 and adding the lo part + mul.0x0100000000 add + # => [faucet_id_suffix, suffix_metadata_lo] + + # extract lower 8 bits of the lo part to get the metadata + swap u32and.0xff + # => [asset_metadata, faucet_id_suffix] +end + +#! Creates asset metadata from the provided inputs. +#! +#! Inputs: [enable_callbacks] +#! Outputs: [asset_metadata] +#! +#! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether the asset callbacks flag should be set. +#! - asset_metadata is the asset metadata. +#! +#! Panics if: +#! - enable_callbacks is not 0 or 1. +proc create_metadata + u32assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS + dup u32lte.CALLBACKS_ENABLED + assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS + # => [asset_metadata] +end + +#! Extracts the asset callback flag from asset metadata. +#! +#! WARNING: asset_metadata is assumed to be a byte (in particular a valid u32) +#! +#! Inputs: [asset_metadata] +#! Outputs: [callbacks_enabled] +#! +#! Where: +#! - asset_metadata is the asset metadata. +#! - callbacks_enabled is 1 if callbacks are enabled and 0 if disabled. +proc metadata_into_callbacks_enabled + # extract the least significant bit of the metadata + u32and.1 + # => [callbacks_enabled] +end diff --git a/crates/miden-protocol/src/account/delta/mod.rs b/crates/miden-protocol/src/account/delta/mod.rs index be0ab3512c..5fdddc577b 100644 --- a/crates/miden-protocol/src/account/delta/mod.rs +++ b/crates/miden-protocol/src/account/delta/mod.rs @@ -201,7 +201,9 @@ impl AccountDelta { /// - Fungible Asset Delta /// - For each **updated** fungible asset, sorted by its vault key, whose amount delta is /// **non-zero**: - /// - Append `[domain = 1, was_added, faucet_id_suffix, faucet_id_prefix]`. + /// - Append `[domain = 1, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix]` + /// where `faucet_id_suffix_and_metadata` is the faucet ID suffix with asset metadata + /// (including the callbacks flag) encoded in the lower 8 bits. /// - Append `[amount_delta, 0, 0, 0]` where `amount_delta` is the delta by which the /// fungible asset's amount has changed and `was_added` is a boolean flag indicating /// whether the amount was added (1) or subtracted (0). diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index 636dfb236b..d6e466e64d 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -11,7 +11,7 @@ use super::{ DeserializationError, Serializable, }; -use crate::account::{AccountId, AccountType}; +use crate::account::AccountType; use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset}; use crate::{Felt, ONE, ZERO}; @@ -134,8 +134,12 @@ impl AccountVaultDelta { .0 .iter() .filter(|&(_, &value)| value >= 0) - .map(|(&faucet_id, &diff)| { - Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap()) + .map(|(vault_key, &diff)| { + Asset::Fungible( + FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs()) + .unwrap() + .with_callbacks(vault_key.callback_flag()), + ) }) .chain( self.non_fungible @@ -150,8 +154,12 @@ impl AccountVaultDelta { .0 .iter() .filter(|&(_, &value)| value < 0) - .map(|(&faucet_id, &diff)| { - Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap()) + .map(|(vault_key, &diff)| { + Asset::Fungible( + FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs()) + .unwrap() + .with_callbacks(vault_key.callback_flag()), + ) }) .chain( self.non_fungible @@ -185,15 +193,18 @@ impl Deserializable for AccountVaultDelta { // ================================================================================================ /// A binary tree map of fungible asset balance changes in the account vault. +/// +/// The [`AssetVaultKey`] orders the assets in the same way as the in-kernel account delta which +/// uses a link map. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct FungibleAssetDelta(BTreeMap); +pub struct FungibleAssetDelta(BTreeMap); impl FungibleAssetDelta { /// Validates and creates a new fungible asset delta. /// /// # Errors /// Returns an error if the delta does not pass the validation. - pub fn new(map: BTreeMap) -> Result { + pub fn new(map: BTreeMap) -> Result { let delta = Self(map); delta.validate()?; @@ -206,7 +217,7 @@ impl FungibleAssetDelta { /// Returns an error if the delta would overflow. pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> { let amount: i64 = asset.amount().try_into().expect("Amount it too high"); - self.add_delta(asset.faucet_id(), amount) + self.add_delta(asset.vault_key(), amount) } /// Removes a fungible asset from the delta. @@ -215,12 +226,12 @@ impl FungibleAssetDelta { /// Returns an error if the delta would overflow. pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> { let amount: i64 = asset.amount().try_into().expect("Amount it too high"); - self.add_delta(asset.faucet_id(), -amount) + self.add_delta(asset.vault_key(), -amount) } - /// Returns the amount of the fungible asset with the given faucet ID. - pub fn amount(&self, faucet_id: &AccountId) -> Option { - self.0.get(faucet_id).copied() + /// Returns the amount of the fungible asset with the given vault key. + pub fn amount(&self, vault_key: &AssetVaultKey) -> Option { + self.0.get(vault_key).copied() } /// Returns the number of fungible assets affected in the delta. @@ -234,7 +245,7 @@ impl FungibleAssetDelta { } /// Returns an iterator over the (key, value) pairs of the map. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.0.iter() } @@ -250,8 +261,8 @@ impl FungibleAssetDelta { // Track fungible asset amounts - positive and negative. `i64` is not lossy while // fungibles are restricted to 2^63-1. Overflow is still possible but we check for that. - for (&faucet_id, &amount) in other.0.iter() { - self.add_delta(faucet_id, amount)?; + for (&vault_key, &amount) in other.0.iter() { + self.add_delta(vault_key, amount)?; } Ok(()) @@ -265,8 +276,8 @@ impl FungibleAssetDelta { /// /// # Errors /// Returns an error if the delta would overflow. - fn add_delta(&mut self, faucet_id: AccountId, delta: i64) -> Result<(), AccountDeltaError> { - match self.0.entry(faucet_id) { + fn add_delta(&mut self, vault_key: AssetVaultKey, delta: i64) -> Result<(), AccountDeltaError> { + match self.0.entry(vault_key) { Entry::Vacant(entry) => { // Only track non-zero amounts. if delta != 0 { @@ -277,7 +288,7 @@ impl FungibleAssetDelta { let old = *entry.get(); let new = old.checked_add(delta).ok_or( AccountDeltaError::FungibleAssetDeltaOverflow { - faucet_id, + faucet_id: vault_key.faucet_id(), current: old, delta, }, @@ -299,9 +310,9 @@ impl FungibleAssetDelta { /// # Errors /// Returns an error if one or more fungible assets' faucet IDs are invalid. fn validate(&self) -> Result<(), AccountDeltaError> { - for faucet_id in self.0.keys() { - if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { - return Err(AccountDeltaError::NotAFungibleFaucetId(*faucet_id)); + for vault_key in self.0.keys() { + if !matches!(vault_key.faucet_id().account_type(), AccountType::FungibleFaucet) { + return Err(AccountDeltaError::NotAFungibleFaucetId(vault_key.faucet_id())); } } @@ -314,12 +325,12 @@ impl FungibleAssetDelta { /// Note that the order in which elements are appended should be the link map key ordering. This /// is fulfilled here because the link map key's most significant element takes precedence over /// less significant ones. The most significant element in the fungible asset delta is the - /// account ID prefix and the delta happens to be sorted by account IDs. Since the account ID + /// faucet ID prefix and the delta happens to be sorted by vault keys. Since the faucet ID /// prefix is unique, it will always decide on the ordering of a link map key, so less /// significant elements are unimportant. This implicit sort should therefore always match the /// link map key ordering, however this is subtle and fragile. pub(super) fn append_delta_elements(&self, elements: &mut Vec) { - for (faucet_id, amount_delta) in self.iter() { + for (vault_key, amount_delta) in self.iter() { // Note that this iterator is guaranteed to never yield zero amounts, so we don't have // to exclude those explicitly. debug_assert_ne!( @@ -331,11 +342,12 @@ impl FungibleAssetDelta { let amount_delta = Felt::try_from(amount_delta.unsigned_abs()) .expect("amount delta should be less than i64::MAX"); + let key_word = vault_key.to_word(); elements.extend_from_slice(&[ DOMAIN_ASSET, was_added, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), + key_word[2], // faucet_id_suffix_and_metadata + key_word[3], // faucet_id_prefix ]); elements.extend_from_slice(&[amount_delta, ZERO, ZERO, ZERO]); } @@ -348,11 +360,13 @@ impl Serializable for FungibleAssetDelta { // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. // We should update this code (and deserialization as well) once it supports signed // integers. - target.write_many(self.0.iter().map(|(&faucet_id, &delta)| (faucet_id, delta as u64))); + // TODO: If we keep this code, optimize by not serializing asset ID (which is always 0). + target.write_many(self.0.iter().map(|(vault_key, &delta)| (*vault_key, delta as u64))); } fn get_size_hint(&self) -> usize { - self.0.len().get_size_hint() + self.0.len() * FungibleAsset::SERIALIZED_SIZE + const ENTRY_SIZE: usize = AssetVaultKey::SERIALIZED_SIZE + core::mem::size_of::(); + self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE } } @@ -360,13 +374,11 @@ impl Deserializable for FungibleAssetDelta { fn read_from(source: &mut R) -> Result { let num_fungible_assets = source.read_usize()?; // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. - // We should update this code (and serialization as well) once it support signeds - // integers. + // We should update this code (and serialization as well) once it supports signed + // integers. let map = source - .read_many_iter::<(AccountId, u64)>(num_fungible_assets)? - .map(|result| { - result.map(|(account_id, delta_as_u64)| (account_id, delta_as_u64 as i64)) - }) + .read_many_iter::<(AssetVaultKey, u64)>(num_fungible_assets)? + .map(|result| result.map(|(vault_key, delta_as_u64)| (vault_key, delta_as_u64 as i64))) .collect::>()?; Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string())) diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs new file mode 100644 index 0000000000..21c3e05ea9 --- /dev/null +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -0,0 +1,71 @@ +use alloc::vec::Vec; + +use crate::Word; +use crate::account::{StorageSlot, StorageSlotName}; +use crate::utils::sync::LazyLock; + +// CONSTANTS +// ================================================================================================ + +static ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = + LazyLock::new(|| { + StorageSlotName::new("miden::protocol::faucet::callback::on_before_asset_added_to_account") + .expect("storage slot name should be valid") + }); + +// ASSET CALLBACKS +// ================================================================================================ + +/// Configures the callback procedure root for the `on_before_asset_added_to_account` callback. +/// +/// ## Storage Layout +/// +/// - [`Self::on_before_asset_added_to_account_slot()`]: Stores the procedure root of the +/// `on_before_asset_added_to_account` callback. This storage slot is only added if the callback +/// procedure root is not the empty word. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct AssetCallbacks { + on_before_asset_added_to_account: Word, +} + +impl AssetCallbacks { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`AssetCallbacks`] with all callbacks set to the empty word. + pub fn new() -> Self { + Self::default() + } + + /// Sets the `on_before_asset_added_to_account` callback procedure root. + pub fn on_before_asset_added_to_account(mut self, proc_root: Word) -> Self { + self.on_before_asset_added_to_account = proc_root; + self + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where the callback procedure root is stored. + pub fn on_before_asset_added_to_account_slot() -> &'static StorageSlotName { + &ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME + } + + /// Returns the procedure root of the `on_before_asset_added_to_account` callback. + pub fn on_before_asset_added_to_account_proc_root(&self) -> Word { + self.on_before_asset_added_to_account + } + + pub fn into_storage_slots(self) -> Vec { + let mut slots = Vec::new(); + + if !self.on_before_asset_added_to_account.is_empty() { + slots.push(StorageSlot::with_value( + AssetCallbacks::on_before_asset_added_to_account_slot().clone(), + self.on_before_asset_added_to_account, + )); + } + + slots + } +} diff --git a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs new file mode 100644 index 0000000000..c5dfa620e4 --- /dev/null +++ b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs @@ -0,0 +1,68 @@ +use alloc::string::ToString; + +use crate::errors::AssetError; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; + +/// The flag in an [`AssetVaultKey`](super::AssetVaultKey) that indicates whether +/// [`AssetCallbacks`](super::AssetCallbacks) are enabled for this asset. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum AssetCallbackFlag { + #[default] + Disabled = Self::DISABLED, + + Enabled = Self::ENABLED, +} + +impl AssetCallbackFlag { + const DISABLED: u8 = 0; + const ENABLED: u8 = 1; + + /// The serialized size of an [`AssetCallbackFlag`] in bytes. + pub const SERIALIZED_SIZE: usize = core::mem::size_of::(); + + /// Encodes the callbacks setting as a `u8`. + pub const fn as_u8(&self) -> u8 { + *self as u8 + } +} + +impl TryFrom for AssetCallbackFlag { + type Error = AssetError; + + /// Decodes a callbacks setting from a `u8`. + /// + /// # Errors + /// + /// Returns an error if the value is not a valid callbacks encoding. + fn try_from(value: u8) -> Result { + match value { + Self::DISABLED => Ok(Self::Disabled), + Self::ENABLED => Ok(Self::Enabled), + _ => Err(AssetError::InvalidAssetCallbackFlag(value)), + } + } +} + +impl Serializable for AssetCallbackFlag { + fn write_into(&self, target: &mut W) { + target.write_u8(self.as_u8()); + } + + fn get_size_hint(&self) -> usize { + AssetCallbackFlag::SERIALIZED_SIZE + } +} + +impl Deserializable for AssetCallbackFlag { + fn read_from(source: &mut R) -> Result { + Self::try_from(source.read_u8()?) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index ba36e94698..58b5754663 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -2,9 +2,10 @@ use alloc::string::ToString; use core::fmt; use super::vault::AssetVaultKey; -use super::{AccountType, Asset, AssetError, Word}; +use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word}; use crate::Felt; use crate::account::AccountId; +use crate::asset::AssetId; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -19,10 +20,14 @@ use crate::utils::serde::{ /// /// A fungible asset consists of a faucet ID of the faucet which issued the asset as well as the /// asset amount. Asset amount is guaranteed to be 2^63 - 1 or smaller. +/// +/// The fungible asset can have callbacks to the faucet enabled or disabled, depending on +/// [`AssetCallbackFlag`]. See [`AssetCallbacks`](crate::asset::AssetCallbacks) for more details. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct FungibleAsset { faucet_id: AccountId, amount: u64, + callbacks: AssetCallbackFlag, } impl FungibleAsset { @@ -36,8 +41,10 @@ impl FungibleAsset { /// The serialized size of a [`FungibleAsset`] in bytes. /// - /// An account ID (15 bytes) plus an amount (u64). - pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::(); + /// An account ID (15 bytes) plus an amount (u64) plus a callbacks flag (u8). + pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + + core::mem::size_of::() + + AssetCallbackFlag::SERIALIZED_SIZE; // CONSTRUCTOR // -------------------------------------------------------------------------------------------- @@ -58,7 +65,11 @@ impl FungibleAsset { return Err(AssetError::FungibleAssetAmountTooBig(amount)); } - Ok(Self { faucet_id, amount }) + Ok(Self { + faucet_id, + amount, + callbacks: AssetCallbackFlag::default(), + }) } /// Creates a fungible asset from the provided key and value. @@ -80,7 +91,10 @@ impl FungibleAsset { return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value)); } - Self::new(key.faucet_id(), value[0].as_canonical_u64()) + let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?; + asset.callbacks = key.callback_flag(); + + Ok(asset) } /// Creates a fungible asset from the provided key and value. @@ -97,6 +111,12 @@ impl FungibleAsset { Self::from_key_value(vault_key, value) } + /// Returns a copy of this asset with the given [`AssetCallbackFlag`]. + pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self { + self.callbacks = callbacks; + self + } + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -110,14 +130,20 @@ impl FungibleAsset { self.amount } - /// Returns true if this and the other assets were issued from the same faucet. - pub fn is_from_same_faucet(&self, other: &Self) -> bool { - self.faucet_id == other.faucet_id + /// Returns true if this and the other asset were issued from the same faucet. + pub fn is_same(&self, other: &Self) -> bool { + self.vault_key() == other.vault_key() + } + + /// Returns the [`AssetCallbackFlag`] of this asset. + pub fn callbacks(&self) -> AssetCallbackFlag { + self.callbacks } /// Returns the key which is used to store this asset in the account vault. pub fn vault_key(&self) -> AssetVaultKey { - AssetVaultKey::new_fungible(self.faucet_id).expect("faucet ID should be of type fungible") + AssetVaultKey::new(AssetId::default(), self.faucet_id, self.callbacks) + .expect("faucet ID should be of type fungible") } /// Returns the asset's key encoded to a [`Word`]. @@ -143,14 +169,14 @@ impl FungibleAsset { /// /// # Errors /// Returns an error if: - /// - The assets were not issued by the same faucet. + /// - The assets do not have the same vault key (i.e. different faucet or callback flags). /// - The total value of assets is greater than or equal to 2^63. #[allow(clippy::should_implement_trait)] pub fn add(self, other: Self) -> Result { - if self.faucet_id != other.faucet_id { - return Err(AssetError::FungibleAssetInconsistentFaucetIds { - original_issuer: self.faucet_id, - other_issuer: other.faucet_id, + if !self.is_same(&other) { + return Err(AssetError::FungibleAssetInconsistentVaultKeys { + original_key: self.vault_key(), + other_key: other.vault_key(), }); } @@ -162,21 +188,25 @@ impl FungibleAsset { return Err(AssetError::FungibleAssetAmountTooBig(amount)); } - Ok(Self { faucet_id: self.faucet_id, amount }) + Ok(Self { + faucet_id: self.faucet_id, + amount, + callbacks: self.callbacks, + }) } /// Subtracts a fungible asset from another and returns the result. /// /// # Errors /// Returns an error if: - /// - The assets were not issued by the same faucet. + /// - The assets do not have the same vault key (i.e. different faucet or callback flags). /// - The final amount would be negative. #[allow(clippy::should_implement_trait)] pub fn sub(self, other: Self) -> Result { - if self.faucet_id != other.faucet_id { - return Err(AssetError::FungibleAssetInconsistentFaucetIds { - original_issuer: self.faucet_id, - other_issuer: other.faucet_id, + if !self.is_same(&other) { + return Err(AssetError::FungibleAssetInconsistentVaultKeys { + original_key: self.vault_key(), + other_key: other.vault_key(), }); } @@ -187,7 +217,11 @@ impl FungibleAsset { }, )?; - Ok(FungibleAsset { faucet_id: self.faucet_id, amount }) + Ok(FungibleAsset { + faucet_id: self.faucet_id, + amount, + callbacks: self.callbacks, + }) } } @@ -213,10 +247,13 @@ impl Serializable for FungibleAsset { // distinguishable during deserialization. target.write(self.faucet_id); target.write(self.amount); + target.write(self.callbacks); } fn get_size_hint(&self) -> usize { - self.faucet_id.get_size_hint() + self.amount.get_size_hint() + self.faucet_id.get_size_hint() + + self.amount.get_size_hint() + + self.callbacks.get_size_hint() } } @@ -235,8 +272,13 @@ impl FungibleAsset { source: &mut R, ) -> Result { let amount: u64 = source.read()?; - FungibleAsset::new(faucet_id, amount) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let callbacks = source.read()?; + + let asset = FungibleAsset::new(faucet_id, amount) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))? + .with_callbacks(callbacks); + + Ok(asset) } } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 9b9662d950..4d52e6ab1b 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -21,6 +21,12 @@ pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails}; mod token_symbol; pub use token_symbol::TokenSymbol; +mod asset_callbacks; +pub use asset_callbacks::AssetCallbacks; + +mod asset_callbacks_flag; +pub use asset_callbacks_flag::AssetCallbackFlag; + mod vault; pub use vault::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault}; @@ -118,16 +124,9 @@ impl Asset { /// Returns true if this asset is the same as the specified asset. /// - /// Two assets are defined to be the same if: - /// - For fungible assets, if they were issued by the same faucet. - /// - For non-fungible assets, if the assets are identical. + /// Two assets are defined to be the same if their vault keys match. pub fn is_same(&self, other: &Self) -> bool { - use Asset::*; - match (self, other) { - (Fungible(l), Fungible(r)) => l.is_from_same_faucet(r), - (NonFungible(l), NonFungible(r)) => l == r, - _ => false, - } + self.vault_key() == other.vault_key() } /// Returns true if this asset is a fungible asset. diff --git a/crates/miden-protocol/src/asset/nonfungible.rs b/crates/miden-protocol/src/asset/nonfungible.rs index dbdaf3c0c5..77333f4fab 100644 --- a/crates/miden-protocol/src/asset/nonfungible.rs +++ b/crates/miden-protocol/src/asset/nonfungible.rs @@ -112,7 +112,7 @@ impl NonFungibleAsset { let asset_id_prefix = self.value[1]; let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix); - AssetVaultKey::new(asset_id, self.faucet_id) + AssetVaultKey::new_native(asset_id, self.faucet_id) .expect("constructors should ensure account ID is of type non-fungible faucet") } @@ -239,7 +239,7 @@ mod tests { #[test] fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> { - let invalid_key = AssetVaultKey::new( + let invalid_key = AssetVaultKey::new_native( AssetId::new(Felt::from(1u32), Felt::from(2u32)), ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?, )?; diff --git a/crates/miden-protocol/src/asset/vault/mod.rs b/crates/miden-protocol/src/asset/vault/mod.rs index ac1d507231..35c7f1c643 100644 --- a/crates/miden-protocol/src/asset/vault/mod.rs +++ b/crates/miden-protocol/src/asset/vault/mod.rs @@ -179,9 +179,12 @@ impl AssetVault { /// - If the delta contains a non-fungible asset addition that is already stored in the vault. /// - The maximum number of leaves per asset is exceeded. pub fn apply_delta(&mut self, delta: &AccountVaultDelta) -> Result<(), AssetVaultError> { - for (&faucet_id, &delta) in delta.fungible().iter() { - let asset = FungibleAsset::new(faucet_id, delta.unsigned_abs()) - .expect("Not a fungible faucet ID or delta is too large"); + for (vault_key, &delta) in delta.fungible().iter() { + // SAFETY: fungible asset delta should only contain fungible faucet IDs and delta amount + // should be in bounds + let asset = FungibleAsset::new(vault_key.faucet_id(), delta.unsigned_abs()) + .expect("fungible asset delta should be valid") + .with_callbacks(vault_key.callback_flag()); match delta >= 0 { true => self.add_fungible_asset(asset), false => self.remove_fungible_asset(asset), diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 8ccf595d36..290d7d6ba3 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -1,4 +1,5 @@ use alloc::boxed::Box; +use alloc::string::ToString; use core::fmt; use miden_core::LexicographicWord; @@ -7,9 +8,16 @@ use miden_crypto::merkle::smt::LeafIndex; use crate::account::AccountId; use crate::account::AccountType::{self}; use crate::asset::vault::AssetId; -use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; +use crate::asset::{Asset, AssetCallbackFlag, FungibleAsset, NonFungibleAsset}; use crate::crypto::merkle::smt::SMT_DEPTH; use crate::errors::AssetError; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; use crate::{Felt, Word}; /// The unique identifier of an [`Asset`] in the [`AssetVault`](crate::asset::AssetVault). @@ -19,12 +27,10 @@ use crate::{Felt, Word}; /// [ /// asset_id_suffix (64 bits), /// asset_id_prefix (64 bits), -/// faucet_id_suffix (56 bits), +/// [faucet_id_suffix (56 bits) | 7 zero bits | callbacks_enabled (1 bit)], /// faucet_id_prefix (64 bits) /// ] /// ``` -/// -/// See the [`Asset`] documentation for the differences between fungible and non-fungible assets. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct AssetVaultKey { /// The asset ID of the vault key. @@ -32,10 +38,21 @@ pub struct AssetVaultKey { /// The ID of the faucet that issued the asset. faucet_id: AccountId, + + /// Determines whether callbacks are enabled. + callback_flag: AssetCallbackFlag, } impl AssetVaultKey { - /// Creates an [`AssetVaultKey`] from its parts. + /// The serialized size of an [`AssetVaultKey`] in bytes. + /// + /// Serialized as its [`Word`] representation (4 field elements). + pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates an [`AssetVaultKey`] for a native asset with callbacks disabled. /// /// # Errors /// @@ -45,7 +62,25 @@ impl AssetVaultKey { /// [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet) /// - the asset ID limbs are not zero when `faucet_id` is of type /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet). - pub fn new(asset_id: AssetId, faucet_id: AccountId) -> Result { + pub fn new_native(asset_id: AssetId, faucet_id: AccountId) -> Result { + Self::new(asset_id, faucet_id, AssetCallbackFlag::Disabled) + } + + /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbackFlag`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided ID is not of type + /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) or + /// [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet) + /// - the asset ID limbs are not zero when `faucet_id` is of type + /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet). + pub fn new( + asset_id: AssetId, + faucet_id: AccountId, + callback_flag: AssetCallbackFlag, + ) -> Result { if !faucet_id.is_faucet() { return Err(AssetError::InvalidFaucetAccountId(Box::from(format!( "expected account ID of type faucet, found account type {}", @@ -57,14 +92,30 @@ impl AssetVaultKey { return Err(AssetError::FungibleAssetIdMustBeZero(asset_id)); } - Ok(Self { asset_id, faucet_id }) + Ok(Self { asset_id, faucet_id, callback_flag }) } + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + /// Returns the word representation of the vault key. /// /// See the type-level documentation for details. - pub fn to_word(self) -> Word { - vault_key_to_word(self.asset_id, self.faucet_id) + pub fn to_word(&self) -> Word { + let faucet_suffix = self.faucet_id.suffix().as_canonical_u64(); + // The lower 8 bits of the faucet suffix are guaranteed to be zero and so it is used to + // encode the asset metadata. + debug_assert!(faucet_suffix & 0xff == 0, "lower 8 bits of faucet suffix must be zero"); + let faucet_id_suffix_and_metadata = faucet_suffix | self.callback_flag.as_u8() as u64; + let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata) + .expect("highest bit should still be zero resulting in a valid felt"); + + Word::new([ + self.asset_id.suffix(), + self.asset_id.prefix(), + faucet_id_suffix_and_metadata, + self.faucet_id.prefix().as_felt(), + ]) } /// Returns the [`AssetId`] of the vault key that distinguishes different assets issued by the @@ -78,6 +129,11 @@ impl AssetVaultKey { self.faucet_id } + /// Returns the [`AssetCallbackFlag`] flag of the vault key. + pub fn callback_flag(&self) -> AssetCallbackFlag { + self.callback_flag + } + /// Constructs a fungible asset's key from a faucet ID. /// /// Returns `None` if the provided ID is not of type @@ -86,7 +142,7 @@ impl AssetVaultKey { if matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { let asset_id = AssetId::new(Felt::ZERO, Felt::ZERO); Some( - Self::new(asset_id, faucet_id) + Self::new_native(asset_id, faucet_id) .expect("we should have account type fungible faucet"), ) } else { @@ -136,14 +192,19 @@ impl TryFrom for AssetVaultKey { fn try_from(key: Word) -> Result { let asset_id_suffix = key[0]; let asset_id_prefix = key[1]; - let faucet_id_suffix = key[2]; + let faucet_id_suffix_and_metadata = key[2]; let faucet_id_prefix = key[3]; + let raw = faucet_id_suffix_and_metadata.as_canonical_u64(); + let callback_flag = AssetCallbackFlag::try_from((raw & 0xff) as u8)?; + let faucet_id_suffix = Felt::try_from(raw & 0xffff_ffff_ffff_ff00) + .expect("clearing lower bits should not produce an invalid felt"); + let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix); let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; - Self::new(asset_id, faucet_id) + Self::new(asset_id, faucet_id, callback_flag) } } @@ -171,11 +232,63 @@ impl From for AssetVaultKey { } } -fn vault_key_to_word(asset_id: AssetId, faucet_id: AccountId) -> Word { - Word::new([ - asset_id.suffix(), - asset_id.prefix(), - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ]) +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AssetVaultKey { + fn write_into(&self, target: &mut W) { + self.to_word().write_into(target); + } + + fn get_size_hint(&self) -> usize { + Self::SERIALIZED_SIZE + } +} + +impl Deserializable for AssetVaultKey { + fn read_from(source: &mut R) -> Result { + let word: Word = source.read()?; + Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use crate::asset::AssetCallbackFlag; + use crate::testing::account_id::{ + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, + }; + + #[test] + fn asset_vault_key_word_roundtrip() -> anyhow::Result<()> { + let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; + let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?; + + for callback_flag in [AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled] { + // Fungible: asset_id must be zero. + let key = AssetVaultKey::new(AssetId::default(), fungible_faucet, callback_flag)?; + + let roundtripped = AssetVaultKey::try_from(key.to_word())?; + assert_eq!(key, roundtripped); + assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?); + + // Non-fungible: asset_id can be non-zero. + let key = AssetVaultKey::new( + AssetId::new(Felt::from(42u32), Felt::from(99u32)), + nonfungible_faucet, + callback_flag, + )?; + + let roundtripped = AssetVaultKey::try_from(key.to_word())?; + assert_eq!(key, roundtripped); + assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?); + } + + Ok(()) + } } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 55c5ac8062..6d5a19e68b 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -13,7 +13,7 @@ use miden_crypto::utils::HexParseError; use thiserror::Error; use super::account::AccountId; -use super::asset::{FungibleAsset, NonFungibleAsset, TokenSymbol}; +use super::asset::{AssetVaultKey, FungibleAsset, NonFungibleAsset, TokenSymbol}; use super::crypto::merkle::MerkleError; use super::note::NoteId; use super::{MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, Word}; @@ -456,11 +456,11 @@ pub enum AssetError { #[error("subtracting {subtrahend} from fungible asset amount {minuend} would underflow")] FungibleAssetAmountNotSufficient { minuend: u64, subtrahend: u64 }, #[error( - "cannot add fungible asset with issuer {other_issuer} to fungible asset with issuer {original_issuer}" + "cannot combine fungible assets with different vault keys: {original_key} and {other_key}" )] - FungibleAssetInconsistentFaucetIds { - original_issuer: AccountId, - other_issuer: AccountId, + FungibleAssetInconsistentVaultKeys { + original_key: AssetVaultKey, + other_key: AssetVaultKey, }, #[error("faucet account ID in asset is invalid")] InvalidFaucetAccountId(#[source] Box), @@ -488,6 +488,8 @@ pub enum AssetError { NonFungibleFaucetIdTypeMismatch(AccountId), #[error("smt proof in asset witness contains invalid key or value")] AssetWitnessInvalid(#[source] Box), + #[error("invalid native asset callbacks encoding: {0}")] + InvalidAssetCallbackFlag(u8), } // TOKEN SYMBOL ERROR diff --git a/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs b/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs index 29ea41cef6..46cd8eb3a6 100644 --- a/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs +++ b/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs @@ -171,7 +171,7 @@ async fn proposed_block_aggregates_account_state_transition() -> anyhow::Result< assert_matches!(account_update.details(), AccountUpdateDetails::Delta(delta) => { assert_eq!(delta.vault().fungible().num_assets(), 1); - assert_eq!(delta.vault().fungible().amount(&asset.unwrap_fungible().faucet_id()).unwrap(), 300); + assert_eq!(delta.vault().fungible().amount(&asset.unwrap_fungible().vault_key()).unwrap(), 300); }); Ok(()) diff --git a/crates/miden-testing/src/kernel_tests/tx/mod.rs b/crates/miden-testing/src/kernel_tests/tx/mod.rs index 26d8e10b48..e8fa628883 100644 --- a/crates/miden-testing/src/kernel_tests/tx/mod.rs +++ b/crates/miden-testing/src/kernel_tests/tx/mod.rs @@ -23,6 +23,7 @@ mod test_array; mod test_asset; mod test_asset_vault; mod test_auth; +mod test_callbacks; mod test_epilogue; mod test_faucet; mod test_fee; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account.rs b/crates/miden-testing/src/kernel_tests/tx/test_account.rs index b91a873ce0..58a8d2b725 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -31,7 +31,7 @@ use miden_protocol::account::{ use miden_protocol::assembly::diagnostics::NamedSource; use miden_protocol::assembly::diagnostics::reporting::PrintDiagnostic; use miden_protocol::assembly::{DefaultSourceManager, Library}; -use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::asset::{Asset, AssetCallbacks, FungibleAsset}; use miden_protocol::errors::tx_kernel::{ ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO, ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO, @@ -54,6 +54,7 @@ use miden_protocol::testing::account_id::{ use miden_protocol::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1}; use miden_protocol::transaction::{RawOutputNote, TransactionKernel}; use miden_protocol::utils::sync::LazyLock; +use miden_standards::account::faucets::BasicFungibleFaucet; use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::account_component::MockAccountComponent; use miden_standards::testing::mock_account::MockAccountExt; @@ -1677,6 +1678,65 @@ async fn test_has_procedure() -> anyhow::Result<()> { Ok(()) } +/// Tests that the `has_callbacks` faucet procedure correctly reports whether a faucet defines +/// callbacks. +/// +/// - `with_callbacks`: callback slot has a non-empty value -> returns 1 +/// - `with_empty_callback`: callback slot exists but value is the empty word -> returns 0 +/// - `without_callbacks`: no callback slot at all -> returns 0 +#[rstest::rstest] +#[case::with_callbacks( + vec![StorageSlot::with_value( + AssetCallbacks::on_before_asset_added_to_account_slot().clone(), + Word::from([1, 2, 3, 4u32]), + )], + true, +)] +#[case::with_empty_callback( + vec![StorageSlot::with_empty_value( + AssetCallbacks::on_before_asset_added_to_account_slot().clone(), + )], + false, +)] +#[case::without_callbacks(vec![], false)] +#[tokio::test] +async fn test_faucet_has_callbacks( + #[case] callback_slots: Vec, + #[case] expected_has_callbacks: bool, +) -> anyhow::Result<()> { + let basic_faucet = BasicFungibleFaucet::new("CBK".try_into()?, 8, Felt::new(1_000_000))?; + + let account = AccountBuilder::new([1u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(MockAccountComponent::with_slots(callback_slots)) + .with_auth_component(Auth::IncrNonce) + .build_existing()?; + + let tx_script_code = format!( + r#" + use miden::protocol::faucet + + begin + exec.faucet::has_callbacks + push.{has_callbacks} + assert_eq.err="has_callbacks returned unexpected value" + end + "#, + has_callbacks = u8::from(expected_has_callbacks) + ); + let tx_script = CodeBuilder::default().compile_tx_script(&tx_script_code)?; + + TransactionContextBuilder::new(account) + .tx_script(tx_script) + .build()? + .execute() + .await?; + + Ok(()) +} + // ACCOUNT INITIAL STORAGE TESTS // ================================================================================================ diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index bfdddbc0ca..15e49bf6cd 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -1,5 +1,12 @@ use miden_protocol::account::AccountId; -use miden_protocol::asset::{AssetId, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; +use miden_protocol::asset::{ + AssetCallbackFlag, + AssetId, + AssetVaultKey, + FungibleAsset, + NonFungibleAsset, + NonFungibleAssetDetails, +}; use miden_protocol::errors::MasmError; use miden_protocol::errors::tx_kernel::{ ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, @@ -220,3 +227,39 @@ async fn test_validate_fungible_asset( Ok(()) } + +#[rstest::rstest] +#[case::without_callbacks(AssetCallbackFlag::Disabled)] +#[case::with_callbacks(AssetCallbackFlag::Enabled)] +#[tokio::test] +async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbackFlag) -> anyhow::Result<()> { + let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; + let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, callbacks)?; + + let code = format!( + " + use $kernel::asset + + begin + push.{ASSET_KEY} + exec.asset::key_to_callbacks_enabled + # => [callbacks_enabled, ASSET_KEY] + + # truncate stack + swapw dropw swap drop + # => [callbacks_enabled] + end + ", + ASSET_KEY = vault_key.to_word(), + ); + + let exec_output = CodeExecutor::with_default_host().run(&code).await?; + + assert_eq!( + exec_output.get_stack_element(0).as_canonical_u64(), + callbacks.as_u8() as u64, + "MASM key_to_asset_category returned wrong value for {callbacks:?}" + ); + + Ok(()) +} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs index 411c06145f..f551c63db1 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs @@ -714,7 +714,7 @@ async fn test_merge_different_fungible_assets_fails() -> anyhow::Result<()> { // Sanity check that the Rust implementation errors when adding assets from different faucets. assert_matches!( asset0.add(asset1).unwrap_err(), - AssetError::FungibleAssetInconsistentFaucetIds { .. } + AssetError::FungibleAssetInconsistentVaultKeys { .. } ); Ok(()) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs new file mode 100644 index 0000000000..f9f606004b --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -0,0 +1,397 @@ +extern crate alloc; + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{ + AccountBuilder, + AccountComponent, + AccountComponentCode, + AccountId, + AccountStorageMode, + AccountType, + StorageMap, + StorageMapKey, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::{Asset, AssetCallbackFlag, AssetCallbacks, FungibleAsset}; +use miden_protocol::block::account_tree::AccountIdKey; +use miden_protocol::errors::MasmError; +use miden_protocol::note::NoteType; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::BasicFungibleFaucet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::procedure_digest; + +use crate::{AccountState, Auth, assert_transaction_executor_error}; + +// CONSTANTS +// ================================================================================================ + +/// MASM code for the BlockList callback component. +/// +/// This procedure checks whether the native account (the one receiving the asset) is in a +/// block list stored in a storage map. If the account is blocked, the callback panics. +const BLOCK_LIST_MASM: &str = r#" +use miden::protocol::active_account +use miden::protocol::native_account +use miden::core::word + +const BLOCK_LIST_MAP_SLOT = word("miden::testing::callbacks::block_list") +const ERR_ACCOUNT_BLOCKED = "the account is blocked and cannot receive this asset" + +#! Callback invoked when an asset with callbacks enabled is added to an account's vault. +#! +#! Checks whether the receiving account is in the block list. If so, panics. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE, pad(8)] +#! Outputs: [ASSET_VALUE, pad(12)] +#! +#! Invocation: call +pub proc on_before_asset_added_to_account + # Get the native account ID (the account receiving the asset) + exec.native_account::get_id + # => [native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(8)] + + # Build account ID map key: [0, 0, suffix, prefix] + push.0.0 + # => [0, 0, native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(8)] + # => [ACCOUNT_ID_KEY, ASSET_KEY, ASSET_VALUE, pad(8)] + + # Look up in block list storage map + push.BLOCK_LIST_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [IS_BLOCKED, ASSET_KEY, ASSET_VALUE, pad(8)] + + # If IS_BLOCKED is non-zero, account is blocked. + exec.word::eqz + assert.err=ERR_ACCOUNT_BLOCKED + # => [ASSET_KEY, ASSET_VALUE, pad(10)] + + # drop unused asset key + dropw + # => [ASSET_VALUE, pad(12)] +end +"#; + +/// The expected error when a blocked account tries to receive an asset with callbacks. +const ERR_ACCOUNT_BLOCKED: MasmError = + MasmError::from_static_str("the account is blocked and cannot receive this asset"); + +// Initialize the Basic Fungible Faucet library only once. +static BLOCK_LIST_COMPONENT_CODE: LazyLock = LazyLock::new(|| { + CodeBuilder::default() + .compile_component_code(BlockList::NAME, BLOCK_LIST_MASM) + .expect("block list library should be valid") +}); + +static BLOCK_LIST_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::testing::callbacks::block_list") + .expect("storage slot name should be valid") +}); + +procedure_digest!( + BLOCK_LIST_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT, + BlockList::NAME, + BlockList::ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME, + || { BLOCK_LIST_COMPONENT_CODE.as_library() } +); + +// BLOCK LIST +// ================================================================================================ + +/// A test component that implements a block list for the `on_before_asset_added_to_account` +/// callback. +/// +/// When a faucet distributes assets with callbacks enabled, this component checks whether the +/// receiving account is in the block list. If the account is blocked, the transaction fails. +struct BlockList { + blocked_accounts: BTreeSet, +} + +impl BlockList { + const NAME: &str = "miden::testing::callbacks::block_list"; + + const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME: &str = "on_before_asset_added_to_account"; + + /// Creates a new [`BlockList`] with the given set of blocked accounts. + fn new(blocked_accounts: BTreeSet) -> Self { + Self { blocked_accounts } + } + + /// Returns the digest of the `distribute` account procedure. + pub fn on_before_asset_added_to_account_digest() -> Word { + *BLOCK_LIST_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT + } +} + +impl From for AccountComponent { + fn from(block_list: BlockList) -> Self { + // Build the storage map of blocked accounts + let map_entries: Vec<(StorageMapKey, Word)> = block_list + .blocked_accounts + .iter() + .map(|account_id| { + let map_key = StorageMapKey::new(AccountIdKey::new(*account_id).as_word()); + // Non-zero value means the account is blocked + let map_value = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + (map_key, map_value) + }) + .collect(); + + let storage_map = StorageMap::with_entries(map_entries) + .expect("btree set should guarantee no duplicates"); + + // Build storage slots: block list map + asset callbacks value slot + let mut storage_slots = + vec![StorageSlot::with_map(BLOCK_LIST_SLOT_NAME.clone(), storage_map)]; + storage_slots.extend( + AssetCallbacks::new() + .on_before_asset_added_to_account( + BlockList::on_before_asset_added_to_account_digest(), + ) + .into_storage_slots(), + ); + let metadata = + AccountComponentMetadata::new(BlockList::NAME, [AccountType::FungibleFaucet]) + .with_description("block list callback component for testing"); + + AccountComponent::new(BLOCK_LIST_COMPONENT_CODE.clone(), storage_slots, metadata) + .expect("block list should satisfy the requirements of a valid account component") + } +} + +// TESTS +// ================================================================================================ + +/// Tests that the `on_before_asset_added_to_account` callback receives the correct inputs. +#[tokio::test] +async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs() +-> anyhow::Result<()> { + let mut builder = crate::MockChain::builder(); + + // Create wallet first so we know its ID before building the faucet. + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + let wallet_id_suffix = target_account.id().suffix().as_canonical_u64(); + let wallet_id_prefix = target_account.id().prefix().as_u64(); + + let amount: u64 = 100; + + // MASM callback that asserts the inputs match expected values. + let component_name = "miden::testing::callbacks::input_validator"; + let proc_name = "on_before_asset_added_to_account"; + let callback_masm = format!( + r#" + const ERR_WRONG_VALUE = "callback received unexpected asset value element" + + #! Inputs: [ASSET_KEY, ASSET_VALUE, pad(8)] + #! Outputs: [ASSET_VALUE, pad(12)] + pub proc {proc_name} + # Assert native account ID can be retrieved via native_account::get_id + exec.::miden::protocol::native_account::get_id + # => [native_account_suffix, native_account_prefix, ASSET_KEY, ASSET_VALUE, pad(8)] + push.{wallet_id_suffix} assert_eq.err="callback received unexpected native account ID suffix" + push.{wallet_id_prefix} assert_eq.err="callback received unexpected native account ID prefix" + # => [ASSET_KEY, ASSET_VALUE, pad(8)] + + # duplicate the asset value for returning + dupw.1 swapw + # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(8)] + + # build the expected asset + push.{amount} + exec.::miden::protocol::active_account::get_id + push.1 + # => [enable_callbacks, active_account_id_suffix, active_account_id_prefix, amount, ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(8)] + exec.::miden::protocol::asset::create_fungible_asset + # => [EXPECTED_ASSET_KEY, EXPECTED_ASSET_VALUE, ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(8)] + + movupw.2 + assert_eqw.err="callback received unexpected asset key" + # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, ASSET_VALUE, pad(8)] + + assert_eqw.err="callback received unexpected asset value" + # => [ASSET_VALUE, pad(12)] + end + "# + ); + + // Compile the callback code and extract the procedure root. + let callback_code = + CodeBuilder::default().compile_component_code(component_name, callback_masm.as_str())?; + + let proc_root = callback_code + .as_library() + .get_procedure_root_by_path(format!("{component_name}::{proc_name}").as_str()) + .expect("callback should contain the procedure"); + + // Build the faucet with BasicFungibleFaucet + callback component. + let basic_faucet = BasicFungibleFaucet::new("CBK".try_into()?, 8, Felt::new(1_000_000))?; + + let callback_storage_slots = AssetCallbacks::new() + .on_before_asset_added_to_account(proc_root) + .into_storage_slots(); + + let callback_metadata = + AccountComponentMetadata::new(component_name, [AccountType::FungibleFaucet]) + .with_description("input validation callback component for testing"); + + let callback_component = + AccountComponent::new(callback_code, callback_storage_slots, callback_metadata)?; + + let account_builder = AccountBuilder::new([43u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(callback_component); + + let faucet = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + )?; + + // Create a P2ID note with a callbacks-enabled fungible asset. + let fungible_asset = + FungibleAsset::new(faucet.id(), amount)?.with_callbacks(AssetCallbackFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + // Execute the transaction - should succeed because all callback assertions pass. + mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await?; + + Ok(()) +} + +/// Tests that a blocked account cannot receive assets with callbacks enabled. +/// +/// Flow: +/// 1. Create a faucet with BasicFungibleFaucet + BlockList components +/// 2. Create a wallet that is in the block list +/// 3. Create a P2ID note with a callbacks-enabled asset from the faucet to the wallet +/// 4. Attempt to consume the note on the blocked wallet +/// 5. Assert that the transaction fails with ERR_ACCOUNT_BLOCKED +#[tokio::test] +async fn test_blocked_account_cannot_receive_asset() -> anyhow::Result<()> { + let mut builder = crate::MockChain::builder(); + + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let block_list = BlockList::new(BTreeSet::from_iter([target_account.id()])); + let basic_faucet = BasicFungibleFaucet::new("BLK".try_into()?, 8, Felt::new(1_000_000))?; + + let account_builder = AccountBuilder::new([42u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(block_list); + + let faucet = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + )?; + + // Create a P2ID note with a callbacks-enabled asset + let fungible_asset = + FungibleAsset::new(faucet.id(), 100)?.with_callbacks(AssetCallbackFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // Get foreign account inputs for the faucet so the callback's foreign context can access it + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + // Try to consume the note on the blocked wallet - should fail because the callback + // checks the block list and panics. + let result = mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_ACCOUNT_BLOCKED); + + Ok(()) +} + +/// Tests that consuming a callbacks-enabled asset succeeds even when the issuing faucet does not +/// have the callback storage slot. +#[tokio::test] +async fn test_faucet_without_callback_slot_skips_callback() -> anyhow::Result<()> { + let mut builder = crate::MockChain::builder(); + + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let basic_faucet = BasicFungibleFaucet::new("NCB".try_into()?, 8, Felt::new(1_000_000))?; + + // Create a faucet WITHOUT any AssetCallbacks component. + let account_builder = AccountBuilder::new([45u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet); + + let faucet = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + )?; + + // Create a P2ID note with a callbacks-enabled asset from this faucet. + // The faucet does not have the callback slot, but the asset has callbacks enabled. + let fungible_asset = + FungibleAsset::new(faucet.id(), 100)?.with_callbacks(AssetCallbackFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + // Consuming the note should succeed: the callback is gracefully skipped because the + // faucet does not define the callback storage slot. + mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await?; + + Ok(()) +} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs index 211a8731eb..40425fdf7f 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -2,7 +2,13 @@ use alloc::sync::Arc; use miden_protocol::account::{Account, AccountBuilder, AccountComponent, AccountId, AccountType}; use miden_protocol::assembly::DefaultSourceManager; -use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; +use miden_protocol::asset::{ + AssetCallbackFlag, + AssetId, + AssetVaultKey, + FungibleAsset, + NonFungibleAsset, +}; use miden_protocol::errors::tx_kernel::{ ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, @@ -294,6 +300,42 @@ async fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result Ok(()) } +/// Tests minting a fungible asset with callbacks enabled. +#[tokio::test] +async fn test_mint_fungible_asset_with_callbacks_enabled() -> anyhow::Result<()> { + let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let asset = FungibleAsset::new(faucet_id, FUNGIBLE_ASSET_AMOUNT)?; + + // Build a vault key with callbacks enabled. + let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, AssetCallbackFlag::Enabled)?; + + let code = format!( + r#" + use mock::faucet->mock_faucet + use $kernel::prologue + + begin + exec.prologue::prepare_transaction + + push.{FUNGIBLE_ASSET_VALUE} + push.{FUNGIBLE_ASSET_KEY} + call.mock_faucet::mint + + dropw dropw + end + "#, + FUNGIBLE_ASSET_KEY = vault_key.to_word(), + FUNGIBLE_ASSET_VALUE = asset.to_value_word(), + ); + + TransactionContextBuilder::with_fungible_faucet(faucet_id.into()) + .build()? + .execute_code(&code) + .await?; + + Ok(()) +} + // FUNGIBLE FAUCET BURN TESTS // ================================================================================================ diff --git a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs index c8b7a0dbbf..2256a43ab9 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs @@ -99,7 +99,7 @@ async fn num_tx_cycles_after_compute_fee_are_less_than_estimated( // These constants should always be updated together with the equivalent constants in // epilogue.masm. const SMT_SET_ADDITIONAL_CYCLES: usize = 250; - const NUM_POST_COMPUTE_FEE_CYCLES: usize = 500; + const NUM_POST_COMPUTE_FEE_CYCLES: usize = 608; assert!( tx.measurements().after_tx_cycles_obtained diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index ef525d1682..4d19a2b3bb 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -236,7 +236,7 @@ where .account_delta_tracker() .vault_delta() .fungible() - .amount(&initial_fee_asset.faucet_id()) + .amount(&initial_fee_asset.vault_key()) .unwrap_or(0); // SAFETY: Initial native asset faucet ID should be a fungible faucet and amount should diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index 61b3ff2e2a..3076434d5c 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -154,6 +154,7 @@ Faucet procedures allow reading and writing to faucet accounts to mint and burn | `create_non_fungible_asset` | Creates a non-fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | | `mint` | Mint an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[NEW_ASSET_VALUE]` | Native & Account & Faucet | | `burn` | Burn an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[ASSET_VALUE]` | Native & Account & Faucet | +| `has_callbacks` | Returns whether the active account defines callbacks.

**Inputs:** `[]`
**Outputs:** `[has_callbacks]` | Any | ## Asset Procedures (`miden::protocol::asset`) @@ -161,5 +162,5 @@ Asset procedures provide utilities for creating fungible and non-fungible assets | Procedure | Description | Context | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `create_fungible_asset` | Builds a fungible asset for the specified fungible faucet and amount.

**Inputs:** `[faucet_id_suffix, faucet_id_prefix, amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any | +| `create_fungible_asset` | Builds a fungible asset for the specified fungible faucet and amount.

**Inputs:** `[enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any | | `create_non_fungible_asset` | Builds a non-fungible asset for the specified non-fungible faucet and data hash.

**Inputs:** `[faucet_id_suffix, faucet_id_prefix, DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any |