diff --git a/CHANGELOG.md b/CHANGELOG.md index 677a213337..9bcc173279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ - [BREAKING] Refactored assets in the tx kernel and `miden::protocol` from one to two words, i.e. `ASSET` becomes `ASSET_KEY` and `ASSET_VALUE` ([#2396](https://github.com/0xMiden/miden-base/pull/2396), [#2410](https://github.com/0xMiden/miden-base/pull/2410)). - [BREAKING] Rename `miden::protocol::asset::build_fungible_asset` to `miden::protocol::asset::create_fungible_asset` ([#2410](https://github.com/0xMiden/miden-base/pull/2410)). - [BREAKING] Rename `miden::protocol::asset::build_non_fungible_asset` to `miden::protocol::asset::create_non_fungible_asset` ([#2410](https://github.com/0xMiden/miden-base/pull/2410)). +- [BREAKING] Change the layout of fungible and non-fungible assets ([#2437](https://github.com/0xMiden/miden-base/pull/2437)). - [BREAKING] Refactored assets in the tx kernel from one to two words, i.e. `ASSET` becomes `ASSET_KEY` and `ASSET_VALUE` ([#2396](https://github.com/0xMiden/miden-base/pull/2396)). - Unified the underlying representation of `ExitRoot` and `SmtNode` and use type aliases ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). - [BREAKING] Moved padding to the end of `CLAIM` `NoteStorage` layout ([#2405](https://github.com/0xMiden/miden-base/pull/2405)). diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 6c1ec77097..9f0ae30060 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -215,26 +215,25 @@ end #! #! Invocation: exec proc convert_asset - # => [faucet_id_prefix, faucet_id_suffix, 0, 0, faucet_id_prefix, faucet_id_suffix, 0, amount] - # --- Step 1: Assert faucet is registered --- + swapw + exec.asset::get_balance_from_fungible_asset + movdn.4 + # => [ASSET_KEY, amount] + + exec.asset::key_into_faucet_id + # => [faucet_id_prefix, faucet_id_suffix, amount] + dup.1 dup.1 exec.bridge_config::assert_faucet_registered - dropw - # => [faucet_id_prefix, faucet_id_suffix, 0, amount] + # => [faucet_id_prefix, faucet_id_suffix, amount] # --- Step 2: FPI to faucet's asset_to_origin_asset --- - # Drop the zero padding between faucet_id and amount. - # TODO(expand_assets): Use asset::get_faucet_id helper. - movup.2 drop - # => [faucet_id_prefix, faucet_id_suffix, amount] - procref.agglayer_faucet::asset_to_origin_asset # => [PROC_MAST_ROOT(4), faucet_id_prefix, faucet_id_suffix, amount] # Move faucet_id above PROC_MAST_ROOT - # TODO(expand_assets): This needs an update in 2437. movup.5 movup.5 # => [faucet_id_prefix, faucet_id_suffix, PROC_MAST_ROOT(4), amount] @@ -466,8 +465,7 @@ proc create_burn_note exec.asset::mem_store # => [ASSET_KEY] - # TODO(expand_assets): Use asset::get_faucet_id helper. - dupw movup.2 drop movup.2 drop + dupw exec.asset::key_to_faucet_id # => [faucet_id_prefix, faucet_id_suffix, ASSET_KEY] # Create NetworkAccountTarget attachment for the faucet @@ -531,6 +529,6 @@ proc create_burn_note # TODO: Check where the extra elements are coming from by adding pad comments throughout the # procedure. - drop + dropw drop # => [] end diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index 02b489a402..1767870a74 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -95,7 +95,7 @@ begin push.STORAGE_START_PTR add.4 mem_loadw_le swapw mem_loadw_le.STORAGE_START_PTR # => [dest_network, dest_address(5), pad(10)] - # Load ASSET_VALUE onto the stack from ASSET_PTR + ASSET_VALUE_MEMORY_OFFSET + # Load asset onto the stack from ASSET_PTR push.ASSET_PTR exec.asset::mem_load # => [ASSET_KEY, ASSET_VALUE, dest_network, dest_address(5), pad(10)] diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index cf28511634..ff8946871a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -628,7 +628,8 @@ end #! #! Invocation: dynexec pub proc account_get_asset - # TODO(expand_assets): Validate ASSET_KEY once validation logic exists. + exec.asset::validate_key + # => [ASSET_KEY, pad(12)] exec.account::get_asset # => [ASSET_VALUE, pad(12)] @@ -646,7 +647,8 @@ end #! #! Invocation: dynexec pub proc account_get_initial_asset - # TODO(expand_assets): Validate ASSET_KEY once validation logic exists. + exec.asset::validate_key + # => [ASSET_KEY, pad(12)] exec.account::get_initial_asset # => [ASSET_VALUE, pad(12)] 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 8eaab8ab1f..a65746bd6e 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm @@ -5,6 +5,8 @@ use $kernel::constants::STORAGE_SLOT_TYPE_VALUE use $kernel::fungible_asset use $kernel::link_map use $kernel::memory +use $kernel::memory::ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR +use $kernel::memory::ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR use $kernel::util::asset::FUNGIBLE_ASSET_MAX_AMOUNT use miden::core::crypto::hashes::rpo256 use miden::core::word @@ -326,7 +328,7 @@ end #! Outputs: [RATE, RATE, PERM] @locals(2) proc update_fungible_asset_delta - exec.memory::get_account_delta_fungible_asset_ptr + push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR # => [account_delta_fungible_asset_ptr, RATE, RATE, PERM] exec.link_map::iter @@ -352,32 +354,27 @@ proc update_fungible_asset_delta # 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], ...] + # => [is_delta_amount_positive, [delta_amount_abs, 0, 0, 0], [faucet_id_prefix, faucet_id_suffix, 0, 0], ...] - # rename is_delta_amount_positive to was_added - swap.3 drop - # => [[delta_amount_abs, 0, was_added, 0], ...] + # 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.7 drop + # => [[delta_amount_abs, 0, 0, 0], [faucet_id_prefix, faucet_id_suffix, was_added, 0], ...] dup neq.0 - # => [is_delta_amount_non_zero, [delta_amount_abs, 0, was_added, 0], [faucet_id_prefix, faucet_id_suffix, 0, 0], ...] + # => [is_delta_amount_non_zero, [delta_amount_abs, 0, 0, 0], [faucet_id_prefix, faucet_id_suffix, was_added, 0], ...] # if delta amount is non-zero, update the hasher if.true - swap.7 - # => [[0, 0, was_added, 0], [faucet_id_prefix, faucet_id_suffix, 0, delta_amount_abs], ...] - - drop push.DOMAIN_ASSET - # => [[domain, 0, was_added, 0], [faucet_id_prefix, faucet_id_suffix, 0, delta_amount_abs], ...] + push.DOMAIN_ASSET swap.8 drop + # => [[delta_amount_abs, 0, 0, 0], [faucet_id_prefix, faucet_id_suffix, was_added, domain], ...] swap.3 - # => [[0, 0, was_added, domain], [faucet_id_prefix, faucet_id_suffix, 0, delta_amount_abs], ...] - - swapw - # => [[faucet_id_prefix, faucet_id_suffix, 0, delta_amount_abs], [0, 0, was_added, domain], RATE, RATE, PERM] + # => [[0, 0, 0, delta_amount_abs], [faucet_id_prefix, faucet_id_suffix, was_added, domain], RATE, RATE, PERM] # drop previous RATE elements swapdw dropw dropw - # => [[faucet_id_prefix, faucet_id_suffix, 0, delta_amount_abs], [0, 0, was_added, domain], PERM] + # => [[0, 0, 0, delta_amount_abs], [faucet_id_prefix, faucet_id_suffix, was_added, domain], PERM] exec.rpo256::permute # => [RATE, RATE, PERM] @@ -407,7 +404,7 @@ end #! Outputs: [RATE, RATE, PERM] @locals(2) proc update_non_fungible_asset_delta - exec.memory::get_account_delta_non_fungible_asset_ptr + push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR # => [account_delta_non_fungible_asset_ptr, RATE, RATE, PERM] exec.link_map::iter @@ -415,47 +412,52 @@ proc update_non_fungible_asset_delta # enter loop if the link map is not empty while.true - exec.link_map::next_key_value - # => [KEY, VALUE0, has_next, iter, ...] + exec.link_map::next_key_double_value + # => [KEY, VALUE0, VALUE1, has_next, iter, ...] # store has_next - movup.8 loc_store.0 - # => [KEY, VALUE0, iter, ...] + movup.12 loc_store.0 + # => [KEY, VALUE0, VALUE1, iter, ...] # store iter - movup.8 loc_store.1 - # => [KEY, VALUE0, ...] + movup.12 loc_store.1 + # => [KEY, VALUE0, VALUE1, ...] # this stack state is equivalent to: - # => [ASSET_VALUE, [was_added, 0, 0, 0], ...] + # => [ASSET_KEY, [was_added, 0, 0, 0], ASSET_VALUE, ...] dup.4 neq.0 - # => [was_added_or_removed, ASSET_VALUE, [was_added, 0, 0, 0], ...] + # => [was_added_or_removed, ASSET_KEY, [was_added, 0, 0, 0], ASSET_VALUE, ...] # if the asset was added or removed (i.e. if was_added != 0), update the hasher if.true - movup.4 - # => [was_added, ASSET_VALUE, [0, 0, 0], ...] + swapw + # => [[was_added, 0, 0, 0], ASSET_KEY, ASSET_VALUE, ...] # convert was_added to a boolean # was_added is 1 if the asset was added and 0 - 1 if it was removed eq.1 - # => [was_added, ASSET_VALUE, [0, 0, 0], ...] + # => [[was_added, 0, 0, 0], [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix], ASSET_VALUE, ...] - movdn.6 - # => [ASSET_VALUE, [0, 0, was_added, 0], ...] + # replace asset_id_prefix with was_added and drop the remaining word + swap.6 dropw + # => [[faucet_id_prefix, faucet_id_suffix, was_added, asset_id_suffix], ASSET_VALUE, ...] - push.DOMAIN_ASSET swap.8 drop - # => [ASSET_VALUE, [0, 0, was_added, domain], RATE, RATE, PERM] + # replace asset_id_suffix with domain + push.DOMAIN_ASSET swap.4 drop + # => [[faucet_id_prefix, faucet_id_suffix, was_added, domain], ASSET_VALUE, ...] + + swapw + # => [ASSET_VALUE, [faucet_id_prefix, faucet_id_suffix, was_added, domain], RATE, RATE, PERM] # drop previous RATE elements swapdw dropw dropw - # => [ASSET_VALUE, [0, 0, was_added, domain], PERM] + # => [ASSET_VALUE, [faucet_id_prefix, faucet_id_suffix, was_added, domain], PERM] exec.rpo256::permute # => [RATE, RATE, PERM] else - # discard the two key and value words loaded from the map - dropw dropw + # discard the key, value0 and value1 words loaded from the map + dropw dropw dropw # => [RATE, RATE, PERM] end # => [RATE, RATE, PERM] @@ -506,7 +508,7 @@ end #! - ASSET_VALUE is the value of the asset that is added. pub proc add_asset # check if the asset is a fungible asset - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE] if.true @@ -520,10 +522,6 @@ pub proc add_asset exec.add_fungible_asset # => [] else - # asset key is unused for now - dropw - # => [ASSET_VALUE] - exec.add_non_fungible_asset # => [] end @@ -542,7 +540,7 @@ end #! - ASSET_VALUE is the value of the asset that is removed. pub proc remove_asset # check if the asset is a fungible asset - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE] if.true @@ -556,10 +554,6 @@ pub proc remove_asset exec.remove_fungible_asset # => [] else - # asset key is unused for now - dropw - # => [ASSET_VALUE] - exec.remove_non_fungible_asset # => [] end @@ -574,7 +568,7 @@ end #! - ASSET_KEY is the asset key of the fungible asset. #! - amount is the amount by which the fungible asset's amount increases. pub proc add_fungible_asset - dupw exec.memory::get_account_delta_fungible_asset_ptr + dupw push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR # => [fungible_delta_map_ptr, ASSET_KEY, ASSET_KEY, amount] # retrieve the current delta amount @@ -595,7 +589,7 @@ pub proc add_fungible_asset swapw padw movdnw.2 # => [ASSET_KEY, delta_amount, 0, 0, 0, EMPTY_WORD] - exec.memory::get_account_delta_fungible_asset_ptr + push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR # => [fungible_delta_map_ptr, ASSET_KEY, delta_amount, 0, 0, 0, EMPTY_WORD] exec.link_map::set drop @@ -611,7 +605,7 @@ end #! - ASSET_KEY is the asset key of the fungible asset. #! - amount is the amount by which the fungible asset's amount decreases. pub proc remove_fungible_asset - dupw exec.memory::get_account_delta_fungible_asset_ptr + dupw push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR # => [fungible_delta_map_ptr, ASSET_KEY, ASSET_KEY, amount] # retrieve the current delta amount @@ -632,7 +626,7 @@ pub proc remove_fungible_asset swapw padw movdnw.2 # => [ASSET_KEY, delta_amount, 0, 0, 0, EMPTY_WORD] - exec.memory::get_account_delta_fungible_asset_ptr + push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR # => [fungible_delta_map_ptr, ASSET_KEY, delta_amount, 0, 0, 0, EMPTY_WORD] exec.link_map::set drop @@ -655,29 +649,48 @@ end #! 0 -> no change to the asset #! +1 -> asset was added #! -#! Inputs: [ASSET_VALUE] +#! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [] #! #! Where: -#! - ASSET_VALUE is the non-fungible asset to be added. +#! - ASSET_KEY is the vault key of the non-fungible asset to be added. +#! - ASSET_VALUE is the value of the non-fungible asset to be added. pub proc add_non_fungible_asset - dupw exec.memory::get_account_delta_non_fungible_asset_ptr - # => [non_fungible_delta_map_ptr, ASSET_VALUE, ASSET_VALUE] + dupw push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR + # => [non_fungible_delta_map_ptr, ASSET_KEY, ASSET_KEY, ASSET_VALUE] # retrieve the current delta - # contains_key can be ignored because the default value is an empty word and the - # was_added value is therefore 0 + # contains_key can be ignored because the asset vault ensures each asset key is only added to + # the delta once + # if no entry exists, the default value is an empty word and so the was_added value is 0 exec.link_map::get drop - # => [was_added, 0, 0, 0, EMPTY_WORD, ASSET_VALUE] + # => [was_added, 0, 0, 0, PREV_ASSET_VALUE, ASSET_KEY, ASSET_VALUE] + + dupw.3 movupw.2 + # => [PREV_ASSET_VALUE, ASSET_VALUE, was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] + + # the asset vault guarantees that this procedure is only called when the asset was not yet + # _added_ to the vault, so it can either be absent or it could have been removed + # absent means PREV_ASSET_VALUE is the EMPTY_WORD + # removal means PREV_ASSET_VALUE is equal to ASSET_VALUE + # sanity check that this assumption is true + exec.word::testz movdn.8 + # => [PREV_ASSET_VALUE, ASSET_VALUE, is_empty_word, was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] + + exec.word::eq or + assert.err="add: prev_asset_value must be empty or equal to asset_value for non-fungible assets" + # => [was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] + + # add 1 to cancel out a previous removal (was_added = 0) or mark the asset as added (was_added = 1) add.1 - # => [was_added, 0, 0, 0, EMPTY_WORD, ASSET_VALUE] + # => [was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] - movupw.2 - # => [ASSET_VALUE, was_added, 0, 0, 0, EMPTY_WORD] + swapw + # => [ASSET_KEY, was_added, 0, 0, 0, ASSET_VALUE] - exec.memory::get_account_delta_non_fungible_asset_ptr - # => [non_fungible_delta_map_ptr, ASSET_VALUE, was_added, 0, 0, 0, EMPTY_WORD] + push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR + # => [non_fungible_delta_map_ptr, ASSET_KEY, was_added, 0, 0, 0, ASSET_VALUE] exec.link_map::set drop # => [] @@ -689,29 +702,48 @@ end #! #! See add_non_fungible_asset for documentation. #! -#! Inputs: [ASSET_VALUE] +#! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [] #! #! Where: -#! - ASSET_VALUE is the value of the asset that is removed. +#! - ASSET_KEY is the vault key of the non-fungible asset to be removed. +#! - ASSET_VALUE is the value of the non-fungible asset to be removed. pub proc remove_non_fungible_asset - dupw exec.memory::get_account_delta_non_fungible_asset_ptr - # => [non_fungible_delta_map_ptr, ASSET_VALUE, ASSET_VALUE] + dupw push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR + # => [non_fungible_delta_map_ptr, ASSET_KEY, ASSET_KEY, ASSET_VALUE] # retrieve the current delta - # contains_key can be ignored because the default value is an empty word and the - # was_added value is therefore 0 + # contains_key can be ignored because the asset vault ensures each asset key is only removed + # from the delta once + # if no entry exists, the default value is an empty word and so the was_added value is 0 exec.link_map::get drop - # => [was_added, 0, 0, 0, EMPTY_WORD, ASSET_VALUE] + # => [was_added, 0, 0, 0, PREV_ASSET_VALUE, ASSET_KEY, ASSET_VALUE] + + dupw.3 movupw.2 + # => [PREV_ASSET_VALUE, ASSET_VALUE, was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] + + # the asset vault guarantees that this procedure is only called when the asset was not yet + # _removed_ from the vault, so it can either be present or it could have been removed + # absent means PREV_ASSET_VALUE is the EMPTY_WORD + # addition means PREV_ASSET_VALUE is equal to ASSET_VALUE + # sanity check that this assumption is true + + exec.word::testz movdn.8 + # => [PREV_ASSET_VALUE, ASSET_VALUE, is_empty_word, was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] + + exec.word::eq or + assert.err="remove: prev_asset_value must be empty or equal to asset_value for non-fungible assets" + # => [was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] + # sub 1 to cancel out a previous addition (was_added = 1) or mark the asset as removed (was_added = -1) sub.1 - # => [was_added, 0, 0, 0, EMPTY_WORD, ASSET_VALUE] + # => [was_added, 0, 0, 0, ASSET_KEY, ASSET_VALUE] - movupw.2 - # => [ASSET_VALUE, was_added, 0, 0, 0, EMPTY_WORD] + swapw + # => [ASSET_KEY, was_added, 0, 0, 0, ASSET_VALUE] - exec.memory::get_account_delta_non_fungible_asset_ptr - # => [non_fungible_delta_map_ptr, ASSET_VALUE, was_added, 0, 0, 0, EMPTY_WORD] + push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR + # => [non_fungible_delta_map_ptr, ASSET_KEY, was_added, 0, 0, 0, ASSET_VALUE] exec.link_map::set drop # => [] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm index 2c94a70f1a..0868650673 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm @@ -1,24 +1,10 @@ use $kernel::account_id +use $kernel::fungible_asset +use $kernel::non_fungible_asset # ERRORS # ================================================================================================= -const ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE = "fungible asset vault key's account ID must be of type fungible faucet" - -const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO="malformed fungible asset: `ASSET[1]` must be 0" - -const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID="malformed fungible asset: `ASSET[2]` and `ASSET[3]` must be a valid fungible faucet id" - -const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS="malformed fungible asset: `ASSET[0]` exceeds the maximum allowed amount" - -const ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the fungible asset is not this faucet" - -const ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE = "non-fungible asset vault key's account ID must be of type non-fungible faucet" - -const ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID="malformed non-fungible asset: `ASSET[3]` is not a valid non-fungible faucet id" - -const ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the non-fungible asset is not this faucet" - const ERR_VAULT_ASSET_KEY_ACCOUNT_ID_MUST_BE_FAUCET="account ID in asset vault key must be either of type fungible or non-fungible faucet" # CONSTANT ACCESSORS @@ -27,81 +13,16 @@ const ERR_VAULT_ASSET_KEY_ACCOUNT_ID_MUST_BE_FAUCET="account ID in asset vault k pub use ::$kernel::util::asset::FUNGIBLE_ASSET_MAX_AMOUNT pub use ::$kernel::util::asset::ASSET_SIZE pub use ::$kernel::util::asset::ASSET_VALUE_MEMORY_OFFSET +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::mem_store pub use ::$kernel::util::asset::mem_load # PROCEDURES # ================================================================================================= -#! Loads an asset key and value from memory given a pointer to the asset. -#! -#! Inputs: [ASSET_KEY, ASSET_VALUE] -#! Outputs: [ASSET_KEY, ASSET_VALUE] -#! -#! Where: -#! - ASSET_KEY is the vault key of the asset to validate. -#! - ASSET_VALUE is the value of the asset to validate. -#! -#! Panics if: -#! - the asset key or value are not well formed. -pub proc validate_fungible_asset - exec.validate_fungible_asset_key - # => [ASSET_KEY, ASSET_VALUE] - - swapw exec.validate_fungible_asset_value swapw - # => [ASSET_KEY, ASSET_VALUE] -end - -#! Validates that a fungible asset value is well formed. -#! -#! Inputs: [ASSET_VALUE] -#! Outputs: [ASSET_VALUE] -#! -#! Where: -#! - ASSET_VALUE is the value of the asset to validate. -#! -#! Panics if: -#! - the asset value is not well formed. -proc validate_fungible_asset_value - # assert that ASSET_VALUE[1] == ZERO - dup.2 eq.0 assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO - # => [ASSET_VALUE] - - # assert that the tuple (ASSET_VALUE[3], ASSET_VALUE[2]) forms a valid account ID - dup.1 dup.1 exec.account_id::validate - # => [ASSET_VALUE] - - # assert that the prefix (ASSET_VALUE[3]) of the account ID is of type fungible faucet - dup exec.account_id::is_fungible_faucet - assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID - # => [ASSET_VALUE] - - # assert that the max amount (ASSET_VALUE[0]) of a fungible asset is not exceeded - dup.3 lte.FUNGIBLE_ASSET_MAX_AMOUNT - assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS - # => [ASSET_VALUE] -end - -#! Validates that a fungible asset's vault key is well formed. -#! -#! Inputs: [ASSET_KEY] -#! Outputs: [ASSET_KEY] -#! -#! Where: -#! - ASSET_KEY is the vault key of the asset to validate. -#! -#! Panics if: -#! - the asset key's account ID is not valid. -#! - the asset key's faucet ID is not a fungible one. -pub proc validate_fungible_asset_key - exec.validate_asset_key - # => [ASSET_KEY] - - exec.is_fungible_asset - assert.err=ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE - # => [ASSET_KEY] -end - #! Returns a boolean indicating whether the asset is fungible. #! #! Inputs: [ASSET_KEY] @@ -110,88 +31,14 @@ end #! Where: #! - ASSET_KEY is the vault key of the asset to check. #! - is_fungible_asset is a boolean indicating whether the asset is fungible. -pub proc is_fungible_asset +pub proc is_fungible_asset_key # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] dup exec.account_id::is_fungible_faucet # => [is_fungible_asset, ASSET_KEY] end -#! Validates that a non fungible asset is well formed. -#! -#! Inputs: [ASSET_KEY, ASSET_VALUE] -#! Outputs: [ASSET_KEY, ASSET_VALUE] -#! -#! Where: -#! - ASSET_KEY is the vault key of the asset to validate. -#! - ASSET_VALUE is the value of the asset to validate. -#! -#! Panics if: -#! - the asset key or value are not well formed. -pub proc validate_non_fungible_asset - exec.validate_non_fungible_asset_key - swapw - # => [ASSET_VALUE, ASSET_KEY] - - exec.validate_non_fungible_asset_value - swapw - # => [ASSET_KEY, ASSET_VALUE] -end - -#! Validates that a non fungible asset is well formed. -#! -#! Inputs: [ASSET_VALUE] -#! Outputs: [ASSET_VALUE] -#! -#! Where: -#! - ASSET_VALUE is the value of the asset to validate. -#! -#! Panics if: -#! - the asset key or value are not well formed. -proc validate_non_fungible_asset_value - # assert that ASSET_VALUE[3] is a valid account ID prefix - # hack: because we only have the prefix we add a 0 as the suffix which is always valid - push.0 dup.1 exec.account_id::validate - # => [ASSET_VALUE] - - # assert that the account ID prefix ASSET_VALUE[3] is of type non fungible faucet - dup exec.account_id::is_non_fungible_faucet - assert.err=ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID - # => [ASSET_VALUE] -end - -#! Validates that a non-fungible asset's vault key is well formed. -#! -#! Inputs: [ASSET_KEY] -#! Outputs: [ASSET_KEY] -#! -#! Where: -#! - ASSET_KEY is the vault key of the asset to validate. -#! -#! Panics if: -#! - the asset key's account ID is not valid. -#! - the asset key's faucet ID is not a non-fungible one. -proc validate_non_fungible_asset_key - # => [ASSET_KEY] - # => [hash0', hash2, hash1, faucet_id_prefix] - - # hack: because we only have the prefix we add a 0 as the suffix which is always valid - push.0 dup.4 - # => [faucet_id_prefix, 0, ASSET_KEY] - - exec.account_id::validate - # => [ASSET_KEY] - - exec.is_non_fungible_asset - assert.err=ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE - - # => [ASSET_KEY] -end - -#! Validates that a fungible asset's vault key is well formed. -#! -#! TODO(expand_assets): -#! WARNING: For now, this only works for keys from fungible assets. +#! Validates that an asset's vault key is well formed. #! #! Inputs: [ASSET_KEY] #! Outputs: [ASSET_KEY] @@ -200,26 +47,20 @@ end #! - ASSET_KEY is the vault key of the asset to validate. #! #! Panics if: -#! - the asset key's account ID is not valid. -#! - the asset key's account ID is neither of type fungible nor of type non-fungible. -proc validate_asset_key - # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - - dup.1 dup.1 - # => [faucet_id_prefix, faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - - exec.account_id::validate - # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - - dup exec.account_id::is_fungible_faucet - # => [is_fungible_faucet, faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - - dup.1 exec.account_id::is_non_fungible_faucet - # => [is_fungible_faucet, is_non_fungible_faucet, faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] +#! - the asset key is not a valid fungible or non-fungible asset key (see +#! fungible_asset::validate_key and non_fungible_asset::validate_key). +pub proc validate_key + # check if the asset key is fungible + exec.is_fungible_asset_key + # => [is_fungible_asset, ASSET_KEY] - # assert the asset key's account ID is either of type fungible or type non-fungible - or assert.err=ERR_VAULT_ASSET_KEY_ACCOUNT_ID_MUST_BE_FAUCET - # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] + if.true + exec.fungible_asset::validate_key + # => [ASSET_KEY] + else + exec.non_fungible_asset::validate_key + # => [ASSET_KEY] + end # => [ASSET_KEY] end @@ -231,14 +72,10 @@ end #! Where: #! - ASSET_KEY is the vault key of the asset to check. #! - is_non_fungible_asset is a boolean indicating whether the asset is non-fungible. -pub proc is_non_fungible_asset - # TODO(expand_assets): Eventually do: +pub proc is_non_fungible_asset_key # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - # dup exec.account_id::is_non_fungible_faucet - # => [is_non_fungible_asset, ASSET_KEY] - # => [ASSET_KEY] - dup.3 exec.account_id::is_non_fungible_faucet + dup exec.account_id::is_non_fungible_faucet # => [is_non_fungible_asset, ASSET_KEY] end @@ -252,86 +89,21 @@ end #! - ASSET_VALUE is the value of the asset to validate. #! #! Panics if: -#! - the asset key or value are not well formed. -pub proc validate_asset +#! - the asset is not a valid fungible or non-fungible asset (see fungible_asset::validate and +#! non_fungible_asset::validate_key). +pub proc validate # check if the asset is fungible - exec.is_fungible_asset + exec.is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE] # if the asset is fungible, validate the fungible asset if.true - exec.validate_fungible_asset + exec.fungible_asset::validate # => [ASSET_KEY, ASSET_VALUE] else # if the asset is non fungible, validate the non fungible asset - exec.validate_non_fungible_asset + exec.non_fungible_asset::validate # => [ASSET_KEY, ASSET_VALUE] end # => [ASSET_KEY, ASSET_VALUE] end - -#! Validates that a fungible asset is associated with the provided faucet_id. -#! -#! Inputs: [faucet_id_prefix, faucet_id_suffix, ASSET_KEY, ASSET_VALUE] -#! Outputs: [ASSET_KEY, ASSET_VALUE] -#! -#! Where: -#! - faucet_id_prefix is the prefix of the faucet's account ID. -#! - ASSET_KEY is the vault key of the asset to validate. -#! - ASSET_VALUE is the value of the asset to validate. -pub proc validate_fungible_asset_origin - movdn.9 movdn.9 - # => [ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] - - # assert the fungible asset key and value are valid - exec.validate_fungible_asset - # => [ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] - - # assert the origin of the asset is the faucet_id provided via the stack - exec.get_vault_key_faucet_id - # => [key_faucet_id_prefix, key_faucet_id_suffix, ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] - - movup.11 movup.11 - # => [faucet_id_prefix, faucet_id_suffix, key_faucet_id_prefix, key_faucet_id_suffix, ASSET_KEY, ASSET_VALUE] - - exec.account_id::is_equal assert.err=ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN - # => [ASSET_KEY, ASSET_VALUE] -end - -#! Validates that a non-fungible asset is associated with the provided faucet_id. -#! -#! Inputs: [faucet_id_prefix, ASSET_KEY, ASSET_VALUE] -#! Outputs: [ASSET_KEY, ASSET_VALUE] -#! -#! Where: -#! - faucet_id_prefix is the prefix of the faucet's account ID. -#! - ASSET_KEY is the vault key of the asset to validate. -#! - ASSET_VALUE is the value of the asset to validate. -pub proc validate_non_fungible_asset_origin - # assert the origin of the asset is the faucet_id prefix provided via the stack - dup.4 assert_eq.err=ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN - # => [ASSET_KEY, ASSET_VALUE] - - # assert the non-fungible asset is valid - exec.validate_non_fungible_asset - # => [ASSET_KEY, ASSET_VALUE] -end - -#! Returns the faucet ID from an asset vault key. -#! -#! TODO(expand_assets): -#! WARNING: For now, this only works for keys from fungible assets. -#! WARNING: The faucet ID is not validated. -#! -#! Inputs: [ASSET_KEY] -#! Outputs: [faucet_id_prefix, faucet_id_suffix, ASSET_KEY] -#! -#! Where: -#! - faucet_id is the account ID in the vault key. -#! - ASSET_KEY is the vault key from which to extract the faucet ID. -proc get_vault_key_faucet_id - # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - - dup.1 dup.1 - # => [faucet_id_prefix, faucet_id_suffix, ASSET_KEY] -end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm index e9e5bd6aeb..b5c4ba065f 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset_vault.masm @@ -5,6 +5,7 @@ use $kernel::account_id use $kernel::asset use $kernel::fungible_asset use $kernel::memory +use $kernel::non_fungible_asset use $kernel::util::asset::FUNGIBLE_ASSET_MAX_AMOUNT # ERRORS @@ -14,20 +15,10 @@ const ERR_VAULT_ADD_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to add f const ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS="the non-fungible asset already exists in the asset vault" -const ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW="failed to remove the fungible asset from the vault since the amount of the asset in the vault is less than the amount to remove" - const ERR_VAULT_REMOVE_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID="failed to remove fungible asset from the asset vault due to the initial value being invalid" const ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND="failed to remove non-existent non-fungible asset from the vault" -# RE-EXPORTS -# ================================================================================================= - -# Re-export the shared utility procedures -pub use $kernel::util::asset::build_non_fungible_asset_vault_key -pub use $kernel::util::asset::build_fungible_asset_vault_key -pub use $kernel::util::asset::build_asset_vault_key - # ACCESSORS # ================================================================================================= @@ -139,14 +130,8 @@ pub proc add_fungible_asset # since we have peeked the value, we need to later assert that the actual value matches this # one, so we'll keep a copy for later + # set the current asset value equal to the current vault value swapw dupw.1 - # => [CUR_VAULT_VALUE, ASSET_KEY, CUR_VAULT_VALUE, ASSET_VALUE] - - # the current vault value may be the empty word if it does not exist and so its faucet id would - # be zeros. We therefore overwrite the faucet id with the faucet id from ASSET_VALUE to account - # for this edge case - drop drop - dup.11 dup.11 # => [CURRENT_ASSET_VALUE, ASSET_KEY, CUR_VAULT_VALUE, ASSET_VALUE] movupw.3 @@ -256,20 +241,20 @@ end #! - the vault already contains the same non-fungible asset. pub proc add_asset # check if the asset is a fungible asset - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE, vault_root_ptr] # add the asset to the asset vault if.true # validate the fungible asset - exec.asset::validate_fungible_asset + exec.fungible_asset::validate # => [ASSET_KEY, ASSET_VALUE, vault_root_ptr] exec.add_fungible_asset # => [ASSET_VALUE'] else # validate the non-fungible asset - exec.asset::validate_non_fungible_asset + exec.non_fungible_asset::validate # => [ASSET_KEY, ASSET_VALUE, vault_root_ptr] exec.add_non_fungible_asset @@ -316,41 +301,29 @@ pub proc remove_fungible_asset loc_storew_be.0 # => [ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - dup.3 dup.12 - # => [peeked_amount, amount, ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - - # assert amount <= peeked_amount - lte assert.err=ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW - # => [ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - # => [[faucet_id_prefix, faucet_id_suffix, 0, amount], ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - - dup.11 movup.4 - # => [amount, peeked_amount, [faucet_id_prefix, faucet_id_suffix, 0], ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - - # compute peeked_amount - amount - sub - # => [new_amount, [faucet_id_prefix, faucet_id_suffix, 0], ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] + dupw.2 swapw + # => [ASSET_VALUE, PEEKED_ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - movdn.3 - # => [[faucet_id_prefix, faucet_id_suffix, new_amount], ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - # => [ASSET_VALUE', ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] + # compute PEEKED_ASSET_VALUE - ASSET_VALUE + exec.fungible_asset::split + # => [NEW_ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] - padw dup.7 - # => [new_amount, EMPTY_WORD, ASSET_VALUE', ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] + padw dupw.1 exec.fungible_asset::get_amount + # => [new_amount, EMPTY_WORD, NEW_ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] eq.0 - # => [is_new_amount_zero, EMPTY_WORD, ASSET_VALUE', ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] + # => [is_new_amount_zero, EMPTY_WORD, NEW_ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] # If is_new_amount_zero EMPTY_WORD remains. - # If !is_new_amount_zero ASSET_VALUE' remains. + # If !is_new_amount_zero NEW_ASSET_VALUE remains. cdropw - # => [EMPTY_WORD_OR_ASSET', ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] + # => [EMPTY_WORD_OR_NEW_ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] dup.12 padw movup.4 mem_loadw_be - # => [VAULT_ROOT, EMPTY_WORD_OR_ASSET', ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] + # => [VAULT_ROOT, EMPTY_WORD_OR_NEW_ASSET_VALUE, ASSET_KEY, PEEKED_ASSET_VALUE, vault_root_ptr] movdnw.2 - # => [EMPTY_WORD_OR_ASSET', ASSET_KEY, VAULT_ROOT, PEEKED_ASSET_VALUE, vault_root_ptr] + # => [EMPTY_WORD_OR_NEW_ASSET_VALUE, ASSET_KEY, VAULT_ROOT, PEEKED_ASSET_VALUE, vault_root_ptr] # update asset in vault and assert the old value is equivalent to the peeked value provided # via peek_asset @@ -421,7 +394,7 @@ end #! - the non-fungible asset is not found in the vault. pub proc remove_asset # check if the asset is a fungible asset - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE, vault_root_ptr] # remove the asset from the asset vault diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 2d3f58e752..a57c0be30f 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -1,10 +1,11 @@ use $kernel::account use $kernel::account_delta +use $kernel::asset use $kernel::asset::ASSET_SIZE use $kernel::asset::ASSET_VALUE_MEMORY_OFFSET -use $kernel::asset use $kernel::asset_vault use $kernel::constants::NOTE_MEM_SIZE +use $kernel::fungible_asset use $kernel::memory use $kernel::note @@ -276,7 +277,7 @@ proc compute_fee # => [verification_cost] end -#! Builds the fee asset with the provided fee amount and the native asset ID of the transaction's +#! Creates the fee asset with the provided fee amount and the native asset ID of the transaction's #! reference block as the faucet ID. #! #! Inputs: [fee_amount] @@ -287,15 +288,12 @@ end #! - FEE_ASSET_KEY is the asset vault key of the fee asset. #! - FEE_ASSET_VALUE is the fungible asset with amount set to fee_amount and the faucet ID set to #! the native asset. -proc build_native_fee_asset +proc create_native_fee_asset exec.memory::get_native_asset_id # => [native_asset_id_prefix, native_asset_id_suffix, fee_amount] - push.0 movdn.2 - # => [native_asset_id_prefix, native_asset_id_suffix, 0, fee_amount] - # => [FEE_ASSET_VALUE] - - exec.asset_vault::build_fungible_asset_vault_key + # SAFETY: native asset ID should be fungible and amount should not be exceeded + exec.fungible_asset::create_unchecked # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] end @@ -307,25 +305,33 @@ end #! check. That's okay, because the logic is entirely determined by the transaction kernel. #! #! Inputs: [] -#! Outputs: [FEE_ASSET_VALUE] +#! Outputs: [native_asset_id_prefix, native_asset_id_suffix, fee_amount] #! #! Where: #! - fee_amount is the computed fee amount of the transaction in the native asset. -#! - FEE_ASSET_VALUE is the fee asset with the faucet ID set to the native asset. +#! - native_asset_id_{prefix,suffix} are the prefix and suffix felts of the faucet that issues the +#! native asset. #! #! Panics if: -#! - the account vault does not contain the computed fee. +#! - the account vault contains less than the computed fee. proc compute_and_remove_fee # compute the fee the tx needs to pay - exec.compute_fee - # => [fee_amount] + exec.compute_fee dup + # => [fee_amount, fee_amount] # build the native asset from the fee amount - exec.build_native_fee_asset - # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] + exec.create_native_fee_asset + # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, fee_amount] emit.EPILOGUE_BEFORE_TX_FEE_REMOVED_FROM_ACCOUNT_EVENT - # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] + # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, fee_amount] + + # prepare the return value + exec.asset::key_to_faucet_id + # => [native_asset_id_prefix, native_asset_id_suffix, FEE_ASSET_KEY, FEE_ASSET_VALUE, fee_amount] + + movdn.9 movdn.9 + # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, native_asset_id_prefix, native_asset_id_suffix, fee_amount] # remove the fee from the native account's vault # note that this deliberately does not use account::remove_asset_from_vault, because that @@ -337,11 +343,11 @@ proc compute_and_remove_fee # fetch the vault root ptr exec.memory::get_account_vault_root_ptr movdn.8 - # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, acct_vault_root_ptr] + # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, account_vault_root_ptr, native_asset_id_prefix, native_asset_id_suffix, fee_amount] # remove the asset from the account vault - exec.asset_vault::remove_fungible_asset - # => [FEE_ASSET_VALUE] + exec.asset_vault::remove_fungible_asset dropw + # => [native_asset_id_prefix, native_asset_id_suffix, fee_amount] end # TRANSACTION EPILOGUE PROCEDURE @@ -363,19 +369,21 @@ end #! Inputs: [] #! Outputs: [ #! OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, -#! FEE_ASSET_VALUE, tx_expiration_block_num +#! native_asset_id_prefix, native_asset_id_suffix, fee_amount, tx_expiration_block_num #! ] #! #! Where: #! - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes. #! - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account #! delta commitment. -#! - FEE_ASSET_VALUE is the fungible asset used as the transaction fee. +#! - fee_amount is the computed fee amount of the transaction denominated in the native asset. +#! - native_asset_id_{prefix,suffix} are the prefix and suffix felts of the faucet that issues the +#! native asset. #! - tx_expiration_block_num is the transaction expiration block number. #! #! Locals: #! - 0..4: OUTPUT_NOTES_COMMITMENT -#! - 4..8: FEE_ASSET_VALUE +#! - 4..8: FEE_ASSET_INFO #! - 8..12: ACCOUNT_DELTA_COMMITMENT #! #! Panics if: @@ -459,10 +467,13 @@ pub proc finalize_transaction # ------ Compute fees ------ exec.compute_and_remove_fee - # => [FEE_ASSET_VALUE] + # => [native_asset_id_prefix, native_asset_id_suffix, fee_amount] - # TODO(programmable_assets): Decide what the public tx output of the fee asset looks like. - # store fee asset in local + # pad to word size so we can store the info as a word + push.0 movdn.3 + # => [native_asset_id_prefix, native_asset_id_suffix, fee_amount, 0] + + # store fee info in local memory loc_storew_be.4 dropw # => [] @@ -504,14 +515,16 @@ pub proc finalize_transaction # ------ Build output stack ------ - exec.memory::get_expiration_block_num movdn.4 - # => [ACCOUNT_UPDATE_COMMITMENT, tx_expiration_block_num] - # load fee asset from local padw loc_loadw_be.4 swapw - # => [ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET_VALUE, tx_expiration_block_num] + # => [ACCOUNT_UPDATE_COMMITMENT, [native_asset_id_prefix, native_asset_id_suffix, fee_amount, 0]] + + # replace 0 with expiration block num + exec.memory::get_expiration_block_num swap.8 drop + # => [ACCOUNT_UPDATE_COMMITMENT, [native_asset_id_prefix, native_asset_id_suffix, fee_amount, tx_expiration_block_num]] # load output notes commitment from local padw loc_loadw_be.0 - # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET_VALUE, tx_expiration_block_num] + # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, + # native_asset_id_prefix, native_asset_id_suffix, fee_amount, tx_expiration_block_num] end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm b/crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm index 5fce2731be..51cde49e6e 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/faucet.masm @@ -2,6 +2,8 @@ use $kernel::account use $kernel::account_id use $kernel::asset use $kernel::asset_vault +use $kernel::fungible_asset +use $kernel::non_fungible_asset use $kernel::memory # FUNGIBLE ASSETS @@ -26,7 +28,7 @@ pub proc mint_fungible_asset # assert that the asset was issued by the faucet the transaction is being executed against and # that the asset is valid exec.account::get_id - exec.asset::validate_fungible_asset_origin + exec.fungible_asset::validate_origin # => [ASSET_KEY, ASSET_VALUE] exec.memory::get_input_vault_root_ptr @@ -57,7 +59,7 @@ proc burn_fungible_asset # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid exec.account::get_id - exec.asset::validate_fungible_asset_origin + exec.fungible_asset::validate_origin # => [ASSET_KEY, ASSET_VALUE] exec.memory::get_input_vault_root_ptr @@ -92,10 +94,7 @@ proc mint_non_fungible_asset # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid exec.account::get_id - swap drop - # => [faucet_id_prefix, ASSET_KEY, ASSET_VALUE] - - exec.asset::validate_non_fungible_asset_origin + exec.non_fungible_asset::validate_origin # => [ASSET_KEY, ASSET_VALUE] exec.memory::get_input_vault_root_ptr @@ -125,10 +124,7 @@ proc burn_non_fungible_asset # assert that the asset was issued by the faucet the transaction is being executed against and # that the asset is valid exec.account::get_id - swap drop - # => [faucet_id_prefix, ASSET_KEY, ASSET_VALUE] - - exec.asset::validate_non_fungible_asset_origin + exec.non_fungible_asset::validate_origin # => [ASSET_KEY, ASSET_VALUE] # remove the non-fungible asset from the input vault for asset preservation @@ -165,7 +161,7 @@ end #! - For non-fungible faucets if the non-fungible asset being minted already exists. pub proc mint # check if the asset is a fungible asset - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE] if.true @@ -199,7 +195,7 @@ end #! provided as input to the transaction via a note or the accounts vault. pub proc burn # check if the asset is a fungible asset - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key # => [is_fungible_asset, ASSET_KEY, ASSET_VALUE] if.true diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/fungible_asset.masm b/crates/miden-protocol/asm/kernels/transaction/lib/fungible_asset.masm index 240470cc77..6ec79358b7 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/fungible_asset.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/fungible_asset.masm @@ -1,12 +1,31 @@ # Contains procedures for the built-in fungible asset. +use $kernel::account_id use $kernel::util::asset::FUNGIBLE_ASSET_MAX_AMOUNT +use $kernel::asset + +# RE-EXPORTS +# ================================================================================================= + +pub use $kernel::util::asset::create_fungible_asset_unchecked->create_unchecked # ERRORS # ================================================================================================= const ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding the fungible asset to the vault would exceed the max amount" +const ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE = "fungible asset vault key's account ID must be of type fungible faucet" + +const ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the fungible asset is not this faucet" + +const ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT="fungible asset amount exceeds the maximum allowed amount" + +const ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO="fungible asset value elements 1, 2 and 3 must be zeros" + +const ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO="fungible asset key asset ID prefix and suffix must be zero" + +const ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW="failed to remove the fungible asset from the vault since the amount of the asset in the vault is less than the amount to remove" + # PROCEDURES # ================================================================================================= @@ -14,9 +33,6 @@ const ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding the fungible asset to the v #! #! WARNING: This procedure assumes the assets have been validated. #! -#! NOTE: we do not assert that faucet IDs match since this asset layout will soon no longer contain -#! them anyway -#! #! Inputs: [ASSET_VALUE_0, ASSET_VALUE_1] #! Outputs: [MERGED_ASSET_VALUE] #! @@ -27,27 +43,63 @@ const ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED="adding the fungible asset to the v #! Panics if: #! - adding the two asset values would exceed FUNGIBLE_ASSET_MAX_AMOUNT. pub proc merge - # extract amount from asset 1 - dup.7 - # => [amount_1, ASSET_VALUE_0, ASSET_VALUE_1] + # extract amounts from assets + exec.get_amount movdn.4 exec.get_amount + # => [amount_1, amount_0] - # compute max_add_amount = FUNGIBLE_ASSET_MAX_AMOUNT - current_amount - # this is the amount that can at most be added to still have a valid asset - push.FUNGIBLE_ASSET_MAX_AMOUNT dup.5 sub - # => [max_add_amount, amount_1, ASSET_VALUE_0, ASSET_VALUE_1] + # compute max_add_amount = FUNGIBLE_ASSET_MAX_AMOUNT - amount_0 + # this is the amount that can at most be added to amount_0 still have a valid asset + dup push.FUNGIBLE_ASSET_MAX_AMOUNT dup.3 sub + # => [max_add_amount, amount_1, amount_1, amount_0] # assert it is safe to add the amounts together, i.e. amount_1 <= max_add_amount lte assert.err=ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED - # => [ASSET_VALUE_0, ASSET_VALUE_1] - - exec.get_amount - # => [current_amount, ASSET_VALUE_1] + # => [amount_1, amount_0] # add the amounts - movup.4 add movdn.3 + add + # => [merged_amount] + + # reconstruct the asset value + push.0.0.0 # => [MERGED_ASSET_VALUE] end +#! Computes ASSET_VALUE_0 - ASSET_VALUE_1 and returns the result. +#! +#! For instance, split(40, 100) returns 60. The operand order matches the `sub` instruction. +#! +#! WARNING: This procedure assumes the assets have been validated. +#! +#! Inputs: [ASSET_VALUE_1, ASSET_VALUE_0] +#! Outputs: [NEW_ASSET_VALUE_0] +#! +#! Where: +#! - ASSET_VALUE_{0, 1} are the assets to split. +#! - NEW_ASSET_VALUE_0 is the result of the split computation. +#! +#! Panics if: +#! - ASSET_VALUE_0 does not contain at least the amount of ASSET_VALUE_1. +pub proc split + # extract amounts from assets + exec.get_amount movdn.4 exec.get_amount swap + # => [amount_1, amount_0] + + # assert amount_1 <= amount_0 so we can safely subtract + dup dup.2 + # => [amount_0, amount_1, amount_1, amount_0] + + lte assert.err=ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW + # => [amount_1, amount_0] + + sub + # => [new_amount] + + # reconstruct the asset value + push.0.0.0 + # => [NEW_ASSET_VALUE] +end + #! Extracts the amount of a fungible asset. #! #! WARNING: This procedure assumes the asset has been validated. @@ -61,3 +113,94 @@ end pub proc get_amount exec.::$kernel::util::asset::get_balance_from_fungible_asset end + +#! Validates that a fungible asset is well formed. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset to validate. +#! - ASSET_VALUE is the value of the asset to validate. +#! +#! Panics if: +#! - the asset key is invalid (see validate_key). +#! - the three most significant elements in the value are not 0. +#! - the amount exceeds FUNGIBLE_ASSET_MAX_AMOUNT. +pub proc validate + exec.validate_key + # => [ASSET_KEY, ASSET_VALUE] + + dupw.1 + # => [ASSET_VALUE, ASSET_KEY, ASSET_VALUE] + + # assuming the asset is valid, its layout is: + # => [0, 0, 0, amount, ASSET_KEY, ASSET_VALUE] + + # assert the first three elements are zeros + eq.0 assert.err=ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO + eq.0 assert.err=ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO + eq.0 assert.err=ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO + # => [amount, ASSET_KEY, ASSET_VALUE] + + # assert amount <= FUNGIBLE_ASSET_MAX_AMOUNT + lte.FUNGIBLE_ASSET_MAX_AMOUNT + assert.err=ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT + # => [ASSET_KEY, ASSET_VALUE] +end + +#! Validates that a fungible asset's vault key is well formed. +#! +#! Inputs: [ASSET_KEY] +#! Outputs: [ASSET_KEY] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset to validate. +#! +#! Panics if: +#! - the asset key's account ID is not valid. +#! - the asset key's faucet ID is not a fungible one. +pub proc validate_key + exec.asset::key_to_faucet_id + exec.account_id::validate + # => [ASSET_KEY] + + exec.asset::is_fungible_asset_key + assert.err=ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE + # => [ASSET_KEY] + + exec.asset::key_to_asset_id + # => [asset_id_prefix, asset_id_suffix, ASSET_KEY] + + eq.0 assert.err=ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO + eq.0 assert.err=ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO + # => [ASSET_KEY] +end + +#! Validates that a fungible asset is associated with the provided faucet_id. +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix, ASSET_KEY, ASSET_VALUE] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! +#! Where: +#! - faucet_id_prefix is the prefix of the faucet's account ID. +#! - ASSET_KEY is the vault key of the asset to validate. +#! - ASSET_VALUE is the value of the asset to validate. +pub proc validate_origin + movdn.9 movdn.9 + # => [ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] + + # assert the fungible asset key and value are valid + exec.validate + # => [ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] + + # assert the origin of the asset is the faucet_id provided via the stack + exec.asset::key_to_faucet_id + # => [key_faucet_id_prefix, key_faucet_id_suffix, ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] + + movup.11 movup.11 + # => [faucet_id_prefix, faucet_id_suffix, key_faucet_id_prefix, key_faucet_id_suffix, ASSET_KEY, ASSET_VALUE] + + exec.account_id::is_equal assert.err=ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN + # => [ASSET_KEY, ASSET_VALUE] +end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index c80d65e52f..abb5924d63 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -223,10 +223,10 @@ const ACCT_ACTIVE_STORAGE_SLOTS_SECTION_OFFSET=2340 # ------------------------------------------------------------------------------------------------- # The link map pointer at which the delta of the fungible asset vault is stored. -const ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR=532480 +pub const ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR=532480 # The link map pointer at which the delta of the non-fungible asset vault is stored. -const ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR=ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR+4 +pub const ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR=ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR+4 # The section of link map pointers where storage map deltas are stored. # This section is offset by `slot index` to get the link map ptr for the storage map @@ -1504,28 +1504,6 @@ end ### ACCOUNT DELTA ################################################# -#! Returns the link map pointer to the fungible asset vault delta. -#! -#! Inputs: [] -#! Outputs: [account_delta_fungible_asset_ptr] -#! -#! Where: -#! - account_delta_fungible_asset_ptr is the link map pointer to the fungible asset vault delta. -pub proc get_account_delta_fungible_asset_ptr - push.ACCOUNT_DELTA_FUNGIBLE_ASSET_PTR -end - -#! Returns the link map pointer to the non-fungible asset vault delta. -#! -#! Inputs: [] -#! Outputs: [account_delta_non_fungible_asset_ptr] -#! -#! Where: -#! - account_delta_non_fungible_asset_ptr is the link map pointer to the non-fungible asset vault delta. -pub proc get_account_delta_non_fungible_asset_ptr - push.ACCOUNT_DELTA_NON_FUNGIBLE_ASSET_PTR -end - #! Returns the link map pointer to the storage map delta of the storage map in the given slot index. #! #! Inputs: [slot_idx] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/non_fungible_asset.masm b/crates/miden-protocol/asm/kernels/transaction/lib/non_fungible_asset.masm new file mode 100644 index 0000000000..9b4693b4ac --- /dev/null +++ b/crates/miden-protocol/asm/kernels/transaction/lib/non_fungible_asset.masm @@ -0,0 +1,92 @@ +use $kernel::account_id +use $kernel::asset + +# ERRORS +# ================================================================================================= + +const ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE = "non-fungible asset vault key's account ID must be of type non-fungible faucet" + +const ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN="the origin of the non-fungible asset is not this faucet" + +const ERR_NON_FUNGIBLE_ASSET_ID_SUFFIX_MUST_MATCH_HASH0="the asset ID suffix in a non-fungible asset's vault key must match hash0 of the asset value" + +const ERR_NON_FUNGIBLE_ASSET_ID_PREFIX_MUST_MATCH_HASH1="the asset ID prefix in a non-fungible asset's vault key must match hash1 of the asset value" + +# PROCEDURES +# ================================================================================================= + +#! Validates that a non fungible asset is well formed. +#! +#! The value itself is not validated since any value is valid. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset to validate. +#! - ASSET_VALUE is the value of the asset to validate. +#! +#! Panics if: +#! - the asset key's account ID is not valid. +#! - the asset key's faucet ID is not a non-fungible one. +#! - the asset ID suffix of the key does not match hash0 of the value. +#! - the asset ID prefix of the key does not match hash1 of the value. +pub proc validate + exec.validate_key + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix, hash3, hash2, hash1, hash0] + + # assert that hash0 matches asset_id_suffix and hash1 matches asset_id_prefix + dup.7 dup.4 assert_eq.err=ERR_NON_FUNGIBLE_ASSET_ID_SUFFIX_MUST_MATCH_HASH0 + dup.6 dup.3 assert_eq.err=ERR_NON_FUNGIBLE_ASSET_ID_PREFIX_MUST_MATCH_HASH1 + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix, ASSET_VALUE] +end + +#! Validates that a non fungible asset's key is well formed. +#! +#! Inputs: [ASSET_KEY] +#! Outputs: [ASSET_KEY] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset to validate. +#! +#! Panics if: +#! - the asset key's account ID is not valid. +#! - the asset key's faucet ID is not a non-fungible one. +pub proc validate_key + exec.asset::key_to_faucet_id + exec.account_id::validate + # => [ASSET_KEY] + + exec.asset::is_non_fungible_asset_key + assert.err=ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE + # => [ASSET_KEY] +end + +#! Validates that a non-fungible asset is associated with the provided faucet_id. +#! +#! The value itself is not validated since any value is valid. +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix, ASSET_KEY, ASSET_VALUE] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! +#! Where: +#! - faucet_id_prefix is the prefix of the faucet's account ID. +#! - ASSET_KEY is the vault key of the asset to validate. +pub proc validate_origin + movdn.9 movdn.9 + # => [ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] + + # assert the non-fungible asset key is valid + exec.validate + # => [ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] + + # assert the origin of the asset is the faucet_id provided via the stack + exec.asset::key_to_faucet_id + # => [key_faucet_id_prefix, key_faucet_id_suffix, ASSET_KEY, ASSET_VALUE, faucet_id_prefix, faucet_id_suffix] + + movup.11 movup.11 + # => [faucet_id_prefix, faucet_id_suffix, key_faucet_id_prefix, key_faucet_id_suffix, ASSET_KEY, ASSET_VALUE] + + exec.account_id::is_equal assert.err=ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN + # => [ASSET_KEY, ASSET_VALUE] +end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm index a785c43669..3c83e670ec 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/output_note.masm @@ -206,7 +206,7 @@ pub proc add_asset # => [ASSET_KEY, ASSET_VALUE, note_ptr, note_ptr] # validate the asset - exec.asset::validate_asset + exec.asset::validate # => [ASSET_KEY, ASSET_VALUE, note_ptr, note_ptr] # emit event to signal that a new asset is going to be added to the note. @@ -530,7 +530,7 @@ proc add_asset_raw # if the asset exists, do not increment num assets # abort if the asset is non-fungible since it cannot be merged - exec.asset::is_fungible_asset + exec.asset::is_fungible_asset_key assert.err=ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS # => [ASSET_KEY, ASSET_VALUE, asset_ptr, note_ptr] diff --git a/crates/miden-protocol/asm/kernels/transaction/main.masm b/crates/miden-protocol/asm/kernels/transaction/main.masm index 6471f50fe2..37f057d51e 100644 --- a/crates/miden-protocol/asm/kernels/transaction/main.masm +++ b/crates/miden-protocol/asm/kernels/transaction/main.masm @@ -56,7 +56,8 @@ const EPILOGUE_END_EVENT=event("miden::protocol::tx::epilogue_end") #! ] #! Outputs: [ #! OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, -#! FEE_ASSET_VALUE, tx_expiration_block_num, pad(3) +#! native_asset_id_prefix, native_asset_id_suffix, fee_amount, tx_expiration_block_num, +#! pad(4) #! ] #! #! Where: @@ -68,7 +69,10 @@ const EPILOGUE_END_EVENT=event("miden::protocol::tx::epilogue_end") #! - OUTPUT_NOTES_COMMITMENT is the commitment to the notes created by the transaction. #! - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account #! delta commitment. -#! - FEE_ASSET_VALUE is the fungible asset used as the transaction fee. +#! - fee_amount is the computed fee amount of the transaction denominated in the native asset. +#! - native_asset_id_{prefix,suffix} are the prefix and suffix felts of the faucet that issues the +#! native asset. +#! - tx_expiration_block_num is the transaction expiration block number. @locals(1) proc main # Prologue @@ -170,11 +174,13 @@ proc main # execute the transaction epilogue exec.epilogue::finalize_transaction - # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET_VALUE, tx_expiration_block_num, pad(16)] + # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, + # native_asset_id_prefix, native_asset_id_suffix, fee_amount, tx_expiration_block_num, pad(16)] # truncate the stack to contain 16 elements in total - repeat.13 movup.13 drop end - # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, FEE_ASSET_VALUE, tx_expiration_block_num, pad(3)] + repeat.3 movupw.3 dropw end + # => [OUTPUT_NOTES_COMMITMENT, ACCOUNT_UPDATE_COMMITMENT, + # native_asset_id_prefix, native_asset_id_suffix, fee_amount, tx_expiration_block_num, pad(4)] emit.EPILOGUE_END_EVENT end diff --git a/crates/miden-protocol/asm/protocol/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm index 759363727b..56c858cdf4 100644 --- a/crates/miden-protocol/asm/protocol/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -555,29 +555,26 @@ pub proc get_initial_balance # => [balance] end -#! Returns a boolean indicating whether the non-fungible asset is present in the active account's -#! vault. +#! Returns a boolean indicating whether the active account stores an asset with the provided +#! non-fungible asset vault key in its vault. #! -#! Inputs: [ASSET_VALUE] +#! Inputs: [ASSET_KEY] #! Outputs: [has_asset] #! #! Where: -#! - ASSET_VALUE is the non-fungible asset of interest -#! - has_asset is a boolean indicating whether the account vault has the asset of interest +#! - ASSET_KEY is the key of the fungible asset to check. +#! - has_asset is a boolean indicating whether the account vault has the asset. #! #! Panics if: #! - the ASSET_VALUE is a fungible asset. #! #! Invocation: exec pub proc has_non_fungible_asset - # check if the asset is a non-fungible asset - # hack: assert second element is zero to ensure it is a non-fungible asset - # TODO(expand_assets): This procedure may go away. If not, check more reliably. - dup.2 neq.0 - assert.err=ERR_VAULT_HAS_NON_FUNGIBLE_ASSET_PROC_CAN_BE_CALLED_ONLY_WITH_NON_FUNGIBLE_ASSET - # => [ASSET_VALUE] + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - exec.asset::build_non_fungible_asset_vault_key + # assert that the faucet id is a non-fungible faucet + dup exec.account_id::is_non_fungible_faucet + assert.err=ERR_VAULT_HAS_NON_FUNGIBLE_ASSET_PROC_CAN_BE_CALLED_ONLY_WITH_NON_FUNGIBLE_ASSET # => [ASSET_KEY] exec.get_asset diff --git a/crates/miden-protocol/asm/protocol/asset.masm b/crates/miden-protocol/asm/protocol/asset.masm index 56d19aa9c5..18dac40bc2 100644 --- a/crates/miden-protocol/asm/protocol/asset.masm +++ b/crates/miden-protocol/asm/protocol/asset.masm @@ -1,4 +1,5 @@ use miden::protocol::account_id +use miden::protocol::util::asset # RE-EXPORTS # ================================================================================================= @@ -6,22 +7,27 @@ use miden::protocol::account_id pub use ::miden::protocol::util::asset::FUNGIBLE_ASSET_MAX_AMOUNT pub use ::miden::protocol::util::asset::ASSET_SIZE pub use ::miden::protocol::util::asset::ASSET_VALUE_MEMORY_OFFSET +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::mem_store pub use ::miden::protocol::util::asset::mem_load +pub use ::miden::protocol::util::asset::get_balance_from_fungible_asset # ERRORS # ================================================================================================= -const ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the fungible asset because the provided faucet id is not from a fungible faucet" - const ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT="fungible asset build operation called with amount that exceeds the maximum allowed asset amount" +const ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the fungible asset because the provided faucet id is not from a fungible faucet" + const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet" # PROCEDURES # ================================================================================================= -#! Builds a fungible asset for the specified fungible faucet and amount. +#! Creates a fungible asset for the specified fungible faucet and amount. #! #! Inputs: [faucet_id_prefix, faucet_id_suffix, amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] @@ -33,6 +39,10 @@ const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! - ASSET_KEY is the vault key of the created fungible asset. #! - ASSET_VALUE is the value of the created fungible asset. #! +#! Panics if: +#! - the provided faucet ID is not a fungible faucet. +#! - the provided amount exceeds FUNGIBLE_ASSET_MAX_AMOUNT. +#! #! Invocation: exec pub proc create_fungible_asset # assert the faucet is a fungible faucet @@ -44,38 +54,34 @@ pub proc create_fungible_asset assert.err=ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT # => [faucet_id_prefix, faucet_id_suffix, amount] - # create the asset - push.0 movdn.2 - # => [ASSET_VALUE] - - exec.::miden::protocol::util::asset::build_fungible_asset_vault_key + # SAFETY: faucet ID and amount were validated + exec.asset::create_fungible_asset_unchecked # => [ASSET_KEY, ASSET_VALUE] end -#! Builds a non fungible asset for the specified non-fungible faucet. +#! Creates a non fungible asset for the specified non-fungible faucet. #! -#! Inputs: [faucet_id_prefix, DATA_HASH] +#! Inputs: [faucet_id_prefix, faucet_id_suffix, DATA_HASH] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: #! - faucet_id_{prefix,suffix} are the prefix and suffix felts of the faucet to create the asset #! for. -#! - DATA_HASH is the data hash of the non-fungible asset to build. +#! - DATA_HASH is the data hash of the non-fungible asset to create. #! - ASSET_KEY is the vault key of the created non-fungible asset. -#! - ASSET_VALUE is the value of the created non-fungible asset. +#! - ASSET_VALUE is the value of the created non-fungible asset, which is identical to DATA_HASH. +#! +#! Panics if: +#! - the provided faucet ID is not a non-fungible faucet. #! #! Invocation: exec pub proc create_non_fungible_asset # assert the faucet is a non-fungible faucet dup exec.account_id::is_non_fungible_faucet assert.err=ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID - # => [faucet_id_prefix, hash3, hash2, hash1, hash0] - - # build the asset - swap drop - # => [faucet_id_prefix, hash2, hash1, hash0] - # => [ASSET_VALUE] + # => [faucet_id_prefix, faucet_id_suffix, DATA_HASH] - dupw exec.::miden::protocol::util::asset::build_non_fungible_asset_vault_key + # SAFETY: faucet ID was validated + exec.::miden::protocol::util::asset::create_non_fungible_asset_unchecked # => [ASSET_KEY, ASSET_VALUE] end diff --git a/crates/miden-protocol/asm/protocol/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm index 258b2ba189..b02e864853 100644 --- a/crates/miden-protocol/asm/protocol/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -43,8 +43,8 @@ end #! Invocation: exec pub proc create_non_fungible_asset # get the id of the faucet the transaction is being executed against - exec.active_account::get_id swap drop - # => [faucet_id_prefix, DATA_HASH] + exec.active_account::get_id + # => [faucet_id_prefix, faucet_id_suffix, DATA_HASH] # build the non-fungible asset exec.asset::create_non_fungible_asset diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index adbbaa4c68..a091d7f60b 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -12,9 +12,6 @@ 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 bitmask that when applied will set the fungible bit to zero. -const INVERSE_FUNGIBLE_BITMASK_U32=0xffffffdf # last byte: 0b1101_1111 - # PROCEDURES # ================================================================================================= @@ -63,7 +60,7 @@ end #! Returns the balance of the given fungible asset. #! -#! Note: Assumes that the given asset is fungible and does NOT validate it. +#! WARNING: Assumes that the given asset value is fungible and does NOT validate it. #! #! Inputs: [ASSET_VALUE] #! Outputs: [balance] @@ -76,69 +73,111 @@ pub proc get_balance_from_fungible_asset # => [balance] end -#! Builds the vault key of a fungible asset. The asset is NOT validated and therefore must -#! be a valid fungible asset. +#! Returns the faucet ID from an asset vault key. #! -#! Inputs: [ASSET_VALUE] -#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! WARNING: The faucet ID is not validated. +#! +#! Inputs: [ASSET_KEY] +#! Outputs: [faucet_id_prefix, faucet_id_suffix, ASSET_KEY] #! #! Where: -#! - ASSET_VALUE is the fungible asset for which the vault key is built. -#! - ASSET_KEY is the vault key of the fungible asset. -pub proc build_fungible_asset_vault_key - # => [faucet_id_prefix, faucet_id_suffix, 0, amount] +#! - 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 + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] - push.0.0 - # => [0, 0, faucet_id_prefix, faucet_id_suffix, 0, amount] + dup.1 dup.1 + # => [faucet_id_prefix, faucet_id_suffix, ASSET_KEY] +end - dup.3 dup.3 - # => [faucet_id_prefix, faucet_id_suffix, 0, 0, faucet_id_prefix, faucet_id_suffix, 0, amount] +#! Returns the faucet ID from an asset vault key and consumes it. +#! +#! WARNING: The faucet ID is not validated. +#! +#! Inputs: [ASSET_KEY] +#! Outputs: [faucet_id_prefix, faucet_id_suffix] +#! +#! Where: +#! - 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 + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] + + movup.2 drop movup.2 drop + # => [faucet_id_prefix, faucet_id_suffix] end -#! Builds the vault key of a non fungible asset. The asset is NOT validated and therefore must -#! be a valid non-fungible asset. +#! Returns the asset ID from an asset vault key. #! -#! Inputs: [ASSET_VALUE] -#! Outputs: [ASSET_KEY] +#! Inputs: [ASSET_KEY] +#! Outputs: [asset_id_prefix, asset_id_suffix, ASSET_KEY] #! #! Where: -#! - ASSET_VALUE is the non-fungible asset for which the vault key is built. -#! - ASSET_KEY is the vault key of the non-fungible asset. -pub proc build_non_fungible_asset_vault_key - # create the asset key from the non-fungible asset by swapping hash0 with the faucet id - # => [faucet_id_prefix, hash2, hash1, hash0] - swap.3 - # => [hash0, hash2, hash1, faucet_id_prefix] - - # disassemble hash0 into u32 limbs - u32split swap - # => [hash0_lo, hash0_hi, hash2, hash1, faucet_id_prefix] - - # set the fungible bit to 0 - u32and.INVERSE_FUNGIBLE_BITMASK_U32 - # => [hash0_lo', hash0_hi, hash2, hash1, faucet_id_prefix] - - # reassemble hash0 felt by multiplying the high part with 2^32 and adding the lo part - swap mul.0x0100000000 add - # => [ASSET_KEY] +#! - asset_id is the asset ID in the vault key. +#! - ASSET_KEY is the vault key from which to extract the asset ID. +pub proc key_to_asset_id + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] + + dup.3 dup.3 + # => [asset_id_prefix, asset_id_suffix, ASSET_KEY] end -#! Builds an asset vault key from an asset value. +#! Returns the asset ID from an asset vault key and consumes it. #! -#! Inputs: [ASSET_VALUE] +#! Inputs: [ASSET_KEY] +#! Outputs: [asset_id_prefix, asset_id_suffix] +#! +#! Where: +#! - asset_id is the asset ID in the vault key. +#! - ASSET_KEY is the vault key from which to extract the asset ID. +pub proc key_into_asset_id + # => [faucet_id_prefix, faucet_id_suffix, asset_id_prefix, asset_id_suffix] + + drop drop + # => [asset_id_prefix, asset_id_suffix] +end + +#! Creates a fungible asset for the specified fungible faucet and amount. +#! +#! WARNING: Does not validate its inputs. +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix, amount] +#! Outputs: [ASSET_KEY, ASSET_VALUE] +#! +#! Where: +#! - faucet_id_{prefix,suffix} are the prefix and suffix 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. +#! +#! Invocation: exec +pub proc create_fungible_asset_unchecked + # create the key and value + repeat.5 push.0 movdn.2 end + # => [[faucet_id_prefix, faucet_id_suffix, 0, 0], [0, 0, 0, amount]] + # => [ASSET_KEY, ASSET_VALUE] +end + +#! Creates a non fungible asset for the specified non-fungible faucet. +#! +#! WARNING: Does not validate its inputs. +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix, DATA_HASH] #! Outputs: [ASSET_KEY, ASSET_VALUE] -pub proc build_asset_vault_key - # check the first element, it will be: - # - zero for a fungible asset - # - non zero for a non-fungible asset - dup.2 eq.0 - # => [is_fungible_asset, ASSET_VALUE] - - if.true - exec.build_fungible_asset_vault_key - # => [ASSET_KEY, ASSET_VALUE] - else - dupw exec.build_non_fungible_asset_vault_key - # => [ASSET_KEY, ASSET_VALUE] - end +#! +#! Where: +#! - faucet_id_{prefix,suffix} are the prefix and suffix felts of the faucet to create the asset +#! for. +#! - DATA_HASH is the data hash of the non-fungible asset to create. +#! - ASSET_KEY is the vault key of the created non-fungible asset. +#! - ASSET_VALUE is the value of the created non-fungible asset, which is identical to DATA_HASH. +#! +#! Invocation: exec +pub proc create_non_fungible_asset_unchecked + # copy hashes at indices 0 and 1 in the data hash word to the corresponding index in the key + # word + dup.5 movdn.2 dup.5 movdn.2 + # => [[faucet_id_prefix, faucet_id_suffix, hash1, hash0], DATA_HASH] + # => [ASSET_KEY, ASSET_VALUE] end diff --git a/crates/miden-protocol/src/account/account_id/id_prefix.rs b/crates/miden-protocol/src/account/account_id/id_prefix.rs index 46207bcc85..cc3c1d8441 100644 --- a/crates/miden-protocol/src/account/account_id/id_prefix.rs +++ b/crates/miden-protocol/src/account/account_id/id_prefix.rs @@ -4,7 +4,7 @@ use core::fmt; use super::v0; use crate::Felt; use crate::account::account_id::AccountIdPrefixV0; -use crate::account::{AccountIdV0, AccountIdVersion, AccountStorageMode, AccountType}; +use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; use crate::errors::AccountIdError; use crate::utils::serde::{ ByteReader, @@ -153,20 +153,6 @@ impl AccountIdPrefix { AccountIdPrefix::V0(id_prefix) => id_prefix.to_hex(), } } - - /// Returns `felt` with the fungible bit set to zero. The version must be passed as the location - /// of the fungible bit may depend on the underlying account ID version. - pub(crate) fn clear_fungible_bit(version: AccountIdVersion, felt: Felt) -> Felt { - match version { - AccountIdVersion::Version0 => { - // Set the fungible bit to zero by taking the bitwise `and` of the felt with the - // inverted is_faucet mask. - let clear_fungible_bit_mask = !AccountIdV0::IS_FAUCET_MASK; - Felt::try_from(felt.as_int() & clear_fungible_bit_mask) - .expect("felt should still be valid as we cleared a bit and did not set any") - }, - } - } } // CONVERSIONS FROM ACCOUNT ID PREFIX diff --git a/crates/miden-protocol/src/account/account_id/v0/mod.rs b/crates/miden-protocol/src/account/account_id/v0/mod.rs index 34ad3ebbb9..b5ff428ac6 100644 --- a/crates/miden-protocol/src/account/account_id/v0/mod.rs +++ b/crates/miden-protocol/src/account/account_id/v0/mod.rs @@ -61,9 +61,6 @@ impl AccountIdV0 { pub(crate) const STORAGE_MODE_MASK: u8 = 0b11 << Self::STORAGE_MODE_SHIFT; pub(crate) const STORAGE_MODE_SHIFT: u64 = 6; - /// The bit at index 5 of the prefix encodes whether the account is a faucet. - pub(crate) const IS_FAUCET_MASK: u64 = 0b10 << Self::TYPE_SHIFT; - // CONSTRUCTORS // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-protocol/src/account/delta/mod.rs b/crates/miden-protocol/src/account/delta/mod.rs index b6fda996b2..dcfb5e4f36 100644 --- a/crates/miden-protocol/src/account/delta/mod.rs +++ b/crates/miden-protocol/src/account/delta/mod.rs @@ -190,22 +190,23 @@ impl AccountDelta { /// [`LexicographicWord`](crate::LexicographicWord). The WORD layout is in memory-order. /// /// - Append `[[nonce_delta, 0, account_id_suffix, account_id_prefix], EMPTY_WORD]`, where - /// account_id_{prefix,suffix} are the prefix and suffix felts of the native account id and - /// nonce_delta is the value by which the nonce was incremented. + /// `account_id_{prefix,suffix}` are the prefix and suffix felts of the native account id and + /// `nonce_delta` is the value by which the nonce was incremented. /// - Fungible Asset Delta /// - For each **updated** fungible asset, sorted by its vault key, whose amount delta is /// **non-zero**: - /// - Append `[domain = 1, was_added, 0, 0]`. - /// - Append `[amount, 0, faucet_id_suffix, faucet_id_prefix]` where amount 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). + /// - Append `[domain = 1, was_added, faucet_id_suffix, faucet_id_prefix]`. + /// - 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). /// - Non-Fungible Asset Delta /// - For each **updated** non-fungible asset, sorted by its vault key: - /// - Append `[domain = 1, was_added, 0, 0]` where was_added is a boolean flag indicating - /// whether the asset was added (1) or removed (0). Note that the domain is the same for - /// assets since `faucet_id_prefix` is at the same position in the layout for both assets, - /// and, by design, it is never the same for fungible and non-fungible assets. - /// - Append `[hash0, hash1, hash2, faucet_id_prefix]`, i.e. the non-fungible asset. + /// - Append `[domain = 1, was_added, faucet_id_suffix, faucet_id_prefix]` where `was_added` + /// is a boolean flag indicating whether the asset was added (1) or removed (0). Note that + /// the domain is the same for assets since `faucet_id_suffix` and `faucet_id_prefix` are + /// at the same position in the layout for both assets, and, by design, they are never the + /// same for fungible and non-fungible assets. + /// - Append `[hash0, hash1, hash2, hash3]`, i.e. the non-fungible asset. /// - Storage Slots are sorted by slot ID and are iterated in this order. For each slot **whose /// value has changed**, depending on the slot type: /// - Value Slot @@ -269,7 +270,7 @@ impl AccountDelta { /// [ /// ID_AND_NONCE, EMPTY_WORD, /// [/* no fungible asset delta */], - /// [[domain = 1, was_added = 0, 0, 0], NON_FUNGIBLE_ASSET], + /// [[domain = 1, was_added = 0, faucet_id_suffix, faucet_id_prefix], NON_FUNGIBLE_ASSET], /// [/* no storage delta */] /// ] /// ``` @@ -279,14 +280,15 @@ impl AccountDelta { /// ID_AND_NONCE, EMPTY_WORD, /// [/* no fungible asset delta */], /// [/* no non-fungible asset delta */], - /// [[domain = 2, 0, slot_id_suffix = 0, slot_id_prefix = 0], NEW_VALUE] + /// [[domain = 2, 0, slot_id_suffix = faucet_id_suffix, slot_id_prefix = faucet_id_prefix], NEW_VALUE] /// ] /// ``` /// - /// `NEW_VALUE` is user-controllable so it can be crafted to match `NON_FUNGIBLE_ASSET`. The - /// domain separator is then the only value that differentiates these two deltas. This shows the - /// importance of placing the domain separators in the same index within each word's layout - /// which makes it easy to see that this value cannot be crafted to be the same. + /// `NEW_VALUE` is user-controllable so it can be crafted to match `NON_FUNGIBLE_ASSET`. Users + /// would have to choose a slot ID (at account creation time) that is equal to the faucet ID. + /// The domain separator is then the only value that differentiates these two deltas. This shows + /// the importance of placing the domain separators in the same index within each word's layout + /// to ensure users cannot craft an ambiguous delta. /// /// ### Number of Changed Entries /// @@ -699,8 +701,7 @@ mod tests { AccountIdBuilder::new() .account_type(AccountType::NonFungibleFaucet) .storage_mode(AccountStorageMode::Public) - .build_with_rng(&mut rand::rng()) - .prefix(), + .build_with_rng(&mut rand::rng()), vec![6], ) .unwrap(), diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index 959b813764..2b79867dfe 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -12,8 +12,8 @@ use super::{ Serializable, }; use crate::account::{AccountId, AccountType}; -use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; -use crate::{Felt, LexicographicWord, ONE, Word, ZERO}; +use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset}; +use crate::{Felt, ONE, ZERO}; // ACCOUNT VAULT DELTA // ================================================================================================ @@ -100,8 +100,6 @@ impl AccountVaultDelta { added_assets: impl IntoIterator, removed_assets: impl IntoIterator, ) -> Self { - use crate::asset::Asset; - let mut fungible = FungibleAssetDelta::default(); let mut non_fungible = NonFungibleAssetDelta::default(); @@ -132,7 +130,6 @@ impl AccountVaultDelta { /// Returns an iterator over the added assets in this delta. pub fn added_assets(&self) -> impl Iterator + '_ { - use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; self.fungible .0 .iter() @@ -140,14 +137,15 @@ impl AccountVaultDelta { .map(|(&faucet_id, &diff)| { Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap()) }) - .chain(self.non_fungible.filter_by_action(NonFungibleDeltaAction::Add).map(|key| { - Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(key.into()) }) - })) + .chain( + self.non_fungible + .filter_by_action(NonFungibleDeltaAction::Add) + .map(Asset::NonFungible), + ) } /// Returns an iterator over the removed assets in this delta. pub fn removed_assets(&self) -> impl Iterator + '_ { - use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; self.fungible .0 .iter() @@ -155,9 +153,11 @@ impl AccountVaultDelta { .map(|(&faucet_id, &diff)| { Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap()) }) - .chain(self.non_fungible.filter_by_action(NonFungibleDeltaAction::Remove).map(|key| { - Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(key.into()) }) - })) + .chain( + self.non_fungible + .filter_by_action(NonFungibleDeltaAction::Remove) + .map(Asset::NonFungible), + ) } } @@ -327,12 +327,17 @@ impl FungibleAssetDelta { "fungible asset iterator should never yield amount deltas of 0" ); - let asset = FungibleAsset::new(*faucet_id, amount_delta.unsigned_abs()) - .expect("absolute amount delta should be less than i64::MAX"); let was_added = if *amount_delta > 0 { ONE } else { ZERO }; - - elements.extend_from_slice(&[DOMAIN_ASSET, was_added, ZERO, ZERO]); - elements.extend_from_slice(Word::from(asset).as_elements()); + let amount_delta = Felt::try_from(amount_delta.unsigned_abs()) + .expect("amount delta should be less than i64::MAX"); + + elements.extend_from_slice(&[ + DOMAIN_ASSET, + was_added, + faucet_id.suffix(), + faucet_id.prefix().as_felt(), + ]); + elements.extend_from_slice(&[amount_delta, ZERO, ZERO, ZERO]); } } } @@ -372,17 +377,17 @@ impl Deserializable for FungibleAssetDelta { /// A binary tree map of non-fungible asset changes (addition and removal) in the account vault. /// -/// The [`LexicographicWord`] wrapper is necessary to order the assets in the same way as the -/// in-kernel account delta which uses a link map. +/// 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 NonFungibleAssetDelta( - BTreeMap, NonFungibleDeltaAction>, + BTreeMap, ); impl NonFungibleAssetDelta { /// Creates a new non-fungible asset delta. pub const fn new( - map: BTreeMap, NonFungibleDeltaAction>, + map: BTreeMap, ) -> Self { Self(map) } @@ -415,7 +420,9 @@ impl NonFungibleAssetDelta { /// Returns an iterator over the (key, value) pairs of the map. pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|(key, value)| (key.inner(), value)) + self.0 + .iter() + .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action)) } /// Merges another delta into this one, overwriting any existing values. @@ -426,8 +433,8 @@ impl NonFungibleAssetDelta { /// Returns an error if duplicate non-fungible assets are added or removed. pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> { // Merge non-fungible assets. Each non-fungible asset can cancel others out. - for (&key, &action) in other.0.iter() { - self.apply_action(key.into_inner(), action)?; + for (&asset, &action) in other.iter() { + self.apply_action(asset, action)?; } Ok(()) @@ -446,13 +453,13 @@ impl NonFungibleAssetDelta { asset: NonFungibleAsset, action: NonFungibleDeltaAction, ) -> Result<(), AccountDeltaError> { - match self.0.entry(LexicographicWord::new(asset)) { + match self.0.entry(asset.vault_key()) { Entry::Vacant(entry) => { - entry.insert(action); + entry.insert((asset, action)); }, Entry::Occupied(entry) => { - let previous = *entry.get(); - if previous == action { + let (_prev_asset, previous_action) = *entry.get(); + if previous_action == action { // Asset cannot be added nor removed twice. return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset)); } @@ -471,8 +478,8 @@ impl NonFungibleAssetDelta { ) -> impl Iterator + '_ { self.0 .iter() - .filter(move |&(_, cur_action)| cur_action == &action) - .map(|(key, _)| key.into_inner()) + .filter(move |&(_, (_asset, cur_action))| cur_action == &action) + .map(|(_key, (asset, _action))| *asset) } /// Appends the non-fungible asset vault delta to the given `elements` from which the delta @@ -484,8 +491,13 @@ impl NonFungibleAssetDelta { NonFungibleDeltaAction::Add => ONE, }; - elements.extend_from_slice(&[DOMAIN_ASSET, was_added, ZERO, ZERO]); - elements.extend_from_slice(Word::from(*asset).as_elements()); + elements.extend_from_slice(&[ + DOMAIN_ASSET, + was_added, + asset.faucet_id().suffix(), + asset.faucet_id().prefix().as_felt(), + ]); + elements.extend_from_slice(asset.to_value_word().as_elements()); } } } @@ -519,14 +531,14 @@ impl Deserializable for NonFungibleAssetDelta { let num_added = source.read_usize()?; for _ in 0..num_added { - let added_asset = source.read()?; - map.insert(LexicographicWord::new(added_asset), NonFungibleDeltaAction::Add); + let added_asset: NonFungibleAsset = source.read()?; + map.insert(added_asset.vault_key(), (added_asset, NonFungibleDeltaAction::Add)); } let num_removed = source.read_usize()?; for _ in 0..num_removed { - let removed_asset = source.read()?; - map.insert(LexicographicWord::new(removed_asset), NonFungibleDeltaAction::Remove); + let removed_asset: NonFungibleAsset = source.read()?; + map.insert(removed_asset.vault_key(), (removed_asset, NonFungibleDeltaAction::Remove)); } Ok(Self::new(map)) @@ -545,7 +557,7 @@ pub enum NonFungibleDeltaAction { #[cfg(test)] mod tests { use super::{AccountVaultDelta, Deserializable, Serializable}; - use crate::account::{AccountId, AccountIdPrefix}; + use crate::account::AccountId; use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, @@ -627,11 +639,11 @@ mod tests { /// Creates an [AccountVaultDelta] with an optional [NonFungibleAsset] delta. This delta /// will be added if `Some(true)`, removed for `Some(false)` and missing for `None`. fn create_delta_with_non_fungible( - account_id_prefix: AccountIdPrefix, + account_id: AccountId, added: Option, ) -> AccountVaultDelta { let asset: Asset = NonFungibleAsset::new( - &NonFungibleAssetDetails::new(account_id_prefix, vec![1, 2, 3]).unwrap(), + &NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(), ) .unwrap() .into(); @@ -643,7 +655,7 @@ mod tests { } } - let account_id = NonFungibleAsset::mock_issuer().prefix(); + let account_id = NonFungibleAsset::mock_issuer(); let mut delta_x = create_delta_with_non_fungible(account_id, x); let delta_y = create_delta_with_non_fungible(account_id, y); diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index bc093e0114..ed43cdb9d6 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -1,10 +1,9 @@ -use alloc::boxed::Box; use alloc::string::ToString; use core::fmt; use super::vault::AssetVaultKey; -use super::{AccountType, Asset, AssetError, Felt, Word, ZERO, is_not_a_non_fungible_asset}; -use crate::account::{AccountId, AccountIdPrefix}; +use super::{AccountType, Asset, AssetError, Word}; +use crate::account::AccountId; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -12,6 +11,7 @@ use crate::utils::serde::{ DeserializationError, Serializable, }; +use crate::{Felt, FieldElement}; // FUNGIBLE ASSET // ================================================================================================ @@ -36,28 +36,65 @@ impl FungibleAsset { /// The serialized size of a [`FungibleAsset`] in bytes. /// - /// Currently an account ID (15 bytes) plus an amount (u64). + /// An account ID (15 bytes) plus an amount (u64). pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::(); // CONSTRUCTOR // -------------------------------------------------------------------------------------------- + /// Returns a fungible asset instantiated with the provided faucet ID and amount. /// /// # Errors + /// /// Returns an error if: - /// - The faucet_id is not a valid fungible faucet ID. - /// - The provided amount is greater than 2^63 - 1. - pub const fn new(faucet_id: AccountId, amount: u64) -> Result { - let asset = Self { faucet_id, amount }; - asset.validate() + /// - The faucet ID is not a valid fungible faucet ID. + /// - The provided amount is greater than [`FungibleAsset::MAX_AMOUNT`]. + pub fn new(faucet_id: AccountId, amount: u64) -> Result { + if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { + return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id)); + } + + if amount > Self::MAX_AMOUNT { + return Err(AssetError::FungibleAssetAmountTooBig(amount)); + } + + Ok(Self { faucet_id, amount }) } - /// Creates a new [FungibleAsset] without checking its validity. - pub(crate) fn new_unchecked(value: Word) -> FungibleAsset { - FungibleAsset { - faucet_id: AccountId::new_unchecked([value[3], value[2]]), - amount: value[0].as_int(), + /// Creates a fungible asset from the provided key and value. + /// + /// # Errors + /// + /// Returns an error if: + /// - The provided key does not contain a valid faucet ID. + /// - The provided key's asset ID limbs are not zero. + /// - The faucet ID is not a fungible faucet ID. + /// - The provided value's amount is greater than [`FungibleAsset::MAX_AMOUNT`] or its three + /// most significant elements are not zero. + pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result { + if !key.asset_id().is_empty() { + return Err(AssetError::FungibleAssetIdMustBeZero(key.asset_id())); } + + if value[1] != Felt::ZERO || value[2] != Felt::ZERO || value[3] != Felt::ZERO { + return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value)); + } + + Self::new(key.faucet_id(), value[0].as_int()) + } + + /// Creates a fungible asset from the provided key and value. + /// + /// Prefer [`Self::from_key_value`] for more type safety. + /// + /// # Errors + /// + /// Returns an error if: + /// - The provided key does not contain a valid faucet ID. + /// - [`Self::from_key_value`] fails. + pub fn from_key_value_words(key: Word, value: Word) -> Result { + let vault_key = AssetVaultKey::try_from(key)?; + Self::from_key_value(vault_key, value) } // PUBLIC ACCESSORS @@ -68,11 +105,6 @@ impl FungibleAsset { self.faucet_id } - /// Return ID prefix of the faucet which issued this asset. - pub fn faucet_id_prefix(&self) -> AccountIdPrefix { - self.faucet_id.prefix() - } - /// Returns the amount of this asset. pub fn amount(&self) -> u64 { self.amount @@ -85,18 +117,23 @@ impl FungibleAsset { /// Returns the key which is used to store this asset in the account vault. pub fn vault_key(&self) -> AssetVaultKey { - AssetVaultKey::from_account_id(self.faucet_id) - .expect("faucet ID should be of type fungible") + AssetVaultKey::new_fungible(self.faucet_id).expect("faucet ID should be of type fungible") } /// Returns the asset's key encoded to a [`Word`]. pub fn to_key_word(&self) -> Word { - *self.vault_key().as_word() + self.vault_key().to_word() } /// Returns the asset's value encoded to a [`Word`]. pub fn to_value_word(&self) -> Word { - Word::from(*self) + Word::new([ + Felt::try_from(self.amount) + .expect("fungible asset should only allow amounts that fit into a felt"), + Felt::ZERO, + Felt::ZERO, + Felt::ZERO, + ]) } // OPERATIONS @@ -152,38 +189,6 @@ impl FungibleAsset { Ok(FungibleAsset { faucet_id: self.faucet_id, amount }) } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Validates this fungible asset. - /// # Errors - /// Returns an error if: - /// - The faucet_id is not a valid fungible faucet ID. - /// - The provided amount is greater than 2^63 - 1. - const fn validate(self) -> Result { - let account_type = self.faucet_id.account_type(); - if !matches!(account_type, AccountType::FungibleFaucet) { - return Err(AssetError::FungibleFaucetIdTypeMismatch(self.faucet_id)); - } - - if self.amount > Self::MAX_AMOUNT { - return Err(AssetError::FungibleAssetAmountTooBig(self.amount)); - } - - Ok(self) - } -} - -impl From for Word { - fn from(asset: FungibleAsset) -> Self { - let mut result = Word::empty(); - result[0] = Felt::new(asset.amount); - result[2] = asset.faucet_id.suffix(); - result[3] = asset.faucet_id.prefix().as_felt(); - debug_assert!(is_not_a_non_fungible_asset(result)); - result - } } impl From for Asset { @@ -192,22 +197,9 @@ impl From for Asset { } } -impl TryFrom for FungibleAsset { - type Error = AssetError; - - fn try_from(value: Word) -> Result { - if value[1] != ZERO { - return Err(AssetError::FungibleAssetExpectedZero(value)); - } - let faucet_id = AccountId::try_from([value[3], value[2]]) - .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; - let amount = value[0].as_int(); - Self::new(faucet_id, amount) - } -} - impl fmt::Display for FungibleAsset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: Replace with hex representation? write!(f, "{self:?}") } } @@ -230,30 +222,18 @@ impl Serializable for FungibleAsset { impl Deserializable for FungibleAsset { fn read_from(source: &mut R) -> Result { - let faucet_id_prefix: AccountIdPrefix = source.read()?; - FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source) + let faucet_id: AccountId = source.read()?; + FungibleAsset::deserialize_with_faucet_id(faucet_id, source) } } impl FungibleAsset { - /// Deserializes a [`FungibleAsset`] from an [`AccountIdPrefix`] and the remaining data from the - /// given `source`. - pub(super) fn deserialize_with_faucet_id_prefix( - faucet_id_prefix: AccountIdPrefix, + /// Deserializes a [`FungibleAsset`] from an [`AccountId`] and the remaining data from the given + /// `source`. + pub(super) fn deserialize_with_faucet_id( + faucet_id: AccountId, source: &mut R, ) -> Result { - // The 8 bytes of the prefix have already been read, so we only need to read the remaining 7 - // bytes of the account ID's 15 total bytes. - let suffix_bytes: [u8; 7] = source.read()?; - // Convert prefix back to bytes so we can call the TryFrom<[u8; 15]> impl. - let prefix_bytes: [u8; 8] = faucet_id_prefix.into(); - let mut id_bytes: [u8; 15] = [0; 15]; - id_bytes[..8].copy_from_slice(&prefix_bytes); - id_bytes[8..].copy_from_slice(&suffix_bytes); - - let faucet_id = AccountId::try_from(id_bytes) - .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; - let amount: u64 = source.read()?; FungibleAsset::new(faucet_id, amount) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) @@ -265,8 +245,11 @@ impl FungibleAsset { #[cfg(test)] mod tests { + use assert_matches::assert_matches; + use super::*; use crate::account::AccountId; + use crate::asset::AssetId; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, @@ -277,7 +260,34 @@ mod tests { }; #[test] - fn test_fungible_asset_serde() { + fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> { + let invalid_key = AssetVaultKey::new( + AssetId::new(1u32.into(), 2u32.into()), + ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, + )?; + + let err = + FungibleAsset::from_key_value(invalid_key, FungibleAsset::mock(5).to_value_word()) + .unwrap_err(); + assert_matches!(err, AssetError::FungibleAssetIdMustBeZero(_)); + + Ok(()) + } + + #[test] + fn fungible_asset_from_key_value_fails_on_invalid_value() -> anyhow::Result<()> { + let asset = FungibleAsset::mock(42); + let mut invalid_value = asset.to_value_word(); + invalid_value[2] = Felt::from(5u32); + + let err = FungibleAsset::from_key_value(asset.vault_key(), invalid_value).unwrap_err(); + assert_matches!(err, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_)); + + Ok(()) + } + + #[test] + fn test_fungible_asset_serde() -> anyhow::Result<()> { for fungible_account_id in [ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, @@ -291,6 +301,15 @@ mod tests { fungible_asset, FungibleAsset::read_from_bytes(&fungible_asset.to_bytes()).unwrap() ); + assert_eq!(fungible_asset.to_bytes().len(), fungible_asset.get_size_hint()); + + assert_eq!( + fungible_asset, + FungibleAsset::from_key_value_words( + fungible_asset.to_key_word(), + fungible_asset.to_value_word() + )? + ) } let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap(); @@ -306,5 +325,16 @@ mod tests { asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes()); let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err(); assert!(matches!(err, DeserializationError::InvalidValue(_))); + + Ok(()) + } + + #[test] + fn test_vault_key_for_fungible_asset() { + let asset = FungibleAsset::mock(34); + + assert_eq!(asset.vault_key().faucet_id(), FungibleAsset::mock_issuer()); + assert_eq!(asset.vault_key().asset_id().prefix().as_int(), 0); + assert_eq!(asset.vault_key().asset_id().suffix().as_int(), 0); } } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index ad5ea84667..e32f2d195a 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -7,11 +7,10 @@ use super::utils::serde::{ DeserializationError, Serializable, }; -use super::{Felt, Hasher, Word, ZERO}; -use crate::account::AccountIdPrefix; +use super::{Felt, Word}; +use crate::account::AccountId; mod fungible; -use alloc::boxed::Box; pub use fungible::FungibleAsset; @@ -19,73 +18,71 @@ mod nonfungible; pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails}; +use crate::FieldElement; + mod token_symbol; pub use token_symbol::TokenSymbol; mod vault; -pub use vault::{AssetVault, AssetVaultKey, AssetWitness, PartialVault}; +pub use vault::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault}; // ASSET // ================================================================================================ /// A fungible or a non-fungible asset. /// -/// All assets are encoded using a single word (4 elements) such that it is easy to determine the -/// type of an asset both inside and outside Miden VM. Specifically: -/// -/// Element 1 of the asset will be: -/// - ZERO for a fungible asset. -/// - non-ZERO for a non-fungible asset. +/// All assets are encoded as the vault key of the asset and its value, each represented as one word +/// (4 elements). This makes it is easy to determine the type of an asset both inside and outside +/// Miden VM. Specifically: /// -/// Element 3 of both asset types is an [`AccountIdPrefix`] or equivalently, the prefix of an -/// [`AccountId`](crate::account::AccountId), which can be used to distinguish assets -/// based on [`AccountIdPrefix::account_type`]. +/// The vault key of an asset contains the [`AccountId`] of the faucet that issues the asset. It can +/// be used to distinguish assets based on the encoded [`AccountId::account_type`]. In the vault +/// keys of assets, the account type bits at index 4 and 5 determine whether the asset is fungible +/// or non-fungible. /// -/// For element 3 of the vault keys of assets, the bit at index 5 (referred to as the -/// "fungible bit" will be): -/// - `1` for a fungible asset. -/// - `0` for a non-fungible asset. -/// -/// The above properties guarantee that there can never be a collision between a fungible and a +/// This property guarantees that there can never be a collision between a fungible and a /// non-fungible asset. /// /// The methodology for constructing fungible and non-fungible assets is described below. /// /// # Fungible assets /// -/// - A fungible asset's data layout is: `[amount, 0, faucet_id_suffix, faucet_id_prefix]`. +/// - A fungible asset's value layout is: `[amount, 0, 0, 0]`. /// - A fungible asset's vault key layout is: `[0, 0, faucet_id_suffix, faucet_id_prefix]`. /// -/// The most significant elements of a fungible asset are set to the prefix (`faucet_id_prefix`) and -/// suffix (`faucet_id_suffix`) of the ID of the faucet which issues the asset. This guarantees the -/// properties described above (the fungible bit is `1`). -/// -/// The least significant element is set to the amount of the asset. This amount cannot be greater -/// than [`FungibleAsset::MAX_AMOUNT`] and thus fits into a felt. +/// The most significant elements of a fungible asset's key are set to the prefix +/// (`faucet_id_prefix`) and suffix (`faucet_id_suffix`) of the ID of the faucet which issues the +/// asset. The asset ID limbs are set to zero, which means two instances of the same fungible asset +/// have the same asset key and will be merged together when stored in the same account's vault. /// -/// Elements 1 and 2 are set to ZERO. +/// The least significant element of the value is set to the amount of the asset and the remaining +/// felts are zero. This amount cannot be greater than [`FungibleAsset::MAX_AMOUNT`] and thus fits +/// into a felt. /// /// It is impossible to find a collision between two fungible assets issued by different faucets as -/// the faucet_id is included in the description of the asset and this is guaranteed to be different +/// the faucet ID is included in the description of the asset and this is guaranteed to be different /// for each faucet as per the faucet creation logic. /// /// # Non-fungible assets /// -/// - A non-fungible asset's data layout is: `[hash0, hash1, hash2, faucet_id_prefix]`. -/// - A non-fungible asset's vault key layout is: `[faucet_id_prefix, hash1, hash2, hash0']`, where -/// `hash0'` is equivalent to `hash0` with the fungible bit set to `0`. See -/// [`NonFungibleAsset::vault_key`] for more details. +/// - A non-fungible asset's data layout is: `[hash0, hash1, hash2, hash3]`. +/// - A non-fungible asset's vault key layout is: `[hash0, hash1, faucet_id_suffix, +/// faucet_id_prefix]`. /// -/// The 4 elements of non-fungible assets are computed as follows: -/// - First the asset data is hashed. This compresses an asset of an arbitrary length to 4 field -/// elements: `[hash0, hash1, hash2, hash3]`. -/// - `hash3` is then replaced with the prefix of the faucet ID (`faucet_id_prefix`) which issues -/// the asset: `[hash0, hash1, hash2, faucet_id_prefix]`. +/// The 4 elements of non-fungible assets are computed by hashing the asset data. This compresses an +/// asset of an arbitrary length to 4 field elements: `[hash0, hash1, hash2, hash3]`. /// /// It is impossible to find a collision between two non-fungible assets issued by different faucets -/// as the faucet_id is included in the description of the non-fungible asset and this is guaranteed -/// to be different as per the faucet creation logic. Collision resistance for non-fungible assets -/// issued by the same faucet is ~2^95. +/// as the faucet ID is included in the description of the non-fungible asset and this is guaranteed +/// to be different as per the faucet creation logic. +/// +/// The most significant elements of a non-fungible asset's key are set to the prefix +/// (`faucet_id_prefix`) and suffix (`faucet_id_suffix`) of the ID of the faucet which issues the +/// asset. The asset ID limbs are set to hashes from the asset's value. This means the collision +/// resistance of non-fungible assets issued by the same faucet is ~2^64, due to the 128-bit asset +/// ID that is unique per non-fungible asset. In other words, two non-fungible assets issued by the +/// same faucet are very unlikely to have the same asset key and thus should not collide when stored +/// in the same account's vault. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Asset { Fungible(FungibleAsset), @@ -93,15 +90,34 @@ pub enum Asset { } impl Asset { - /// Creates a new [Asset] without checking its validity. - pub(crate) fn new_unchecked(value: Word) -> Asset { - if is_not_a_non_fungible_asset(value) { - Asset::Fungible(FungibleAsset::new_unchecked(value)) + /// Creates an asset from the provided key and value. + /// + /// # Errors + /// + /// Returns an error if: + /// - [`FungibleAsset::from_key_value`] or [`NonFungibleAsset::from_key_value`] fails. + pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result { + if matches!(key.faucet_id().account_type(), AccountType::FungibleFaucet) { + FungibleAsset::from_key_value(key, value).map(Asset::Fungible) } else { - Asset::NonFungible(unsafe { NonFungibleAsset::new_unchecked(value) }) + NonFungibleAsset::from_key_value(key, value).map(Asset::NonFungible) } } + /// Creates an asset from the provided key and value. + /// + /// Prefer [`Self::from_key_value`] for more type safety. + /// + /// # Errors + /// + /// Returns an error if: + /// - The provided key does not contain a valid faucet ID. + /// - [`Self::from_key_value`] fails. + pub fn from_key_value_words(key: Word, value: Word) -> Result { + let vault_key = AssetVaultKey::try_from(key)?; + Self::from_key_value(vault_key, value) + } + /// Returns true if this asset is the same as the specified asset. /// /// Two assets are defined to be the same if: @@ -117,23 +133,20 @@ impl Asset { } /// Returns true if this asset is a fungible asset. - pub const fn is_fungible(&self) -> bool { + pub fn is_fungible(&self) -> bool { matches!(self, Self::Fungible(_)) } /// Returns true if this asset is a non fungible asset. - pub const fn is_non_fungible(&self) -> bool { + pub fn is_non_fungible(&self) -> bool { matches!(self, Self::NonFungible(_)) } - /// Returns the prefix of the faucet ID which issued this asset. - /// - /// To get the full [`AccountId`](crate::account::AccountId) of a fungible asset the asset - /// must be matched on. - pub fn faucet_id_prefix(&self) -> AccountIdPrefix { + /// Returns the ID of the faucet that issued this asset. + pub fn faucet_id(&self) -> AccountId { match self { - Self::Fungible(asset) => asset.faucet_id_prefix(), - Self::NonFungible(asset) => asset.faucet_id_prefix(), + Self::Fungible(asset) => asset.faucet_id(), + Self::NonFungible(asset) => asset.faucet_id(), } } @@ -147,12 +160,15 @@ impl Asset { /// Returns the asset's key encoded to a [`Word`]. pub fn to_key_word(&self) -> Word { - *self.vault_key().as_word() + self.vault_key().to_word() } /// Returns the asset's value encoded to a [`Word`]. pub fn to_value_word(&self) -> Word { - Word::from(*self) + match self { + Asset::Fungible(fungible_asset) => fungible_asset.to_value_word(), + Asset::NonFungible(non_fungible_asset) => non_fungible_asset.to_value_word(), + } } /// Returns the asset encoded as elements. @@ -160,7 +176,7 @@ impl Asset { /// The first four elements contain the asset key and the last four elements contain the asset /// value. pub fn as_elements(&self) -> [Felt; 8] { - let mut elements = [ZERO; 8]; + let mut elements = [Felt::ZERO; 8]; elements[0..4].copy_from_slice(self.to_key_word().as_elements()); elements[4..8].copy_from_slice(self.to_value_word().as_elements()); elements @@ -191,47 +207,6 @@ impl Asset { } } -impl From for Word { - fn from(asset: Asset) -> Self { - match asset { - Asset::Fungible(asset) => asset.into(), - Asset::NonFungible(asset) => asset.into(), - } - } -} - -impl From<&Asset> for Word { - fn from(value: &Asset) -> Self { - (*value).into() - } -} - -impl TryFrom<&Word> for Asset { - type Error = AssetError; - - fn try_from(value: &Word) -> Result { - (*value).try_into() - } -} - -impl TryFrom for Asset { - type Error = AssetError; - - fn try_from(value: Word) -> Result { - // Return an error if element 3 is not a valid account ID prefix, which cannot be checked by - // is_not_a_non_fungible_asset. - // Keep in mind serialized assets do _not_ carry the suffix required to reconstruct the full - // account identifier. - let prefix = AccountIdPrefix::try_from(value[3]) - .map_err(|err| AssetError::InvalidFaucetAccountId(Box::from(err)))?; - match prefix.account_type() { - AccountType::FungibleFaucet => FungibleAsset::try_from(value).map(Asset::from), - AccountType::NonFungibleFaucet => NonFungibleAsset::try_from(value).map(Asset::from), - _ => Err(AssetError::InvalidFaucetAccountIdPrefix(prefix)), - } - } -} - // SERIALIZATION // ================================================================================================ @@ -253,58 +228,34 @@ impl Serializable for Asset { impl Deserializable for Asset { fn read_from(source: &mut R) -> Result { - // Both asset types have their faucet ID prefix as the first element, so we can use it to - // inspect what type of asset it is. - let faucet_id_prefix: AccountIdPrefix = source.read()?; + // Both asset types have their faucet ID as the first element, so we can use it to inspect + // what type of asset it is. + let faucet_id: AccountId = source.read()?; - match faucet_id_prefix.account_type() { + match faucet_id.account_type() { AccountType::FungibleFaucet => { - FungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source) - .map(Asset::from) + FungibleAsset::deserialize_with_faucet_id(faucet_id, source).map(Asset::from) }, AccountType::NonFungibleFaucet => { - NonFungibleAsset::deserialize_with_faucet_id_prefix(faucet_id_prefix, source) - .map(Asset::from) + NonFungibleAsset::deserialize_with_faucet_id(faucet_id, source).map(Asset::from) }, other_type => Err(DeserializationError::InvalidValue(format!( - "failed to deserialize asset: expected an account ID prefix of type faucet, found {other_type:?}" + "failed to deserialize asset: expected an account ID prefix of type faucet, found {other_type}" ))), } } } -// HELPER FUNCTIONS -// ================================================================================================ - -/// Returns `true` if asset in [Word] is not a non-fungible asset. -/// -/// Note: this does not mean that the word is a fungible asset as the word may contain a value -/// which is not a valid asset. -fn is_not_a_non_fungible_asset(asset: Word) -> bool { - match AccountIdPrefix::try_from(asset[3]) { - Ok(prefix) => { - matches!(prefix.account_type(), AccountType::FungibleFaucet) - }, - Err(_err) => { - #[cfg(debug_assertions)] - panic!("invalid account ID prefix passed to is_not_a_non_fungible_asset: {_err}"); - #[cfg(not(debug_assertions))] - false - }, - } -} - // TESTS // ================================================================================================ #[cfg(test)] mod tests { - use miden_crypto::Word; use miden_crypto::utils::{Deserializable, Serializable}; use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; - use crate::account::{AccountId, AccountIdPrefix}; + use crate::account::AccountId; use crate::testing::account_id::{ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, @@ -316,8 +267,9 @@ mod tests { ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, }; + /// Tests the serialization roundtrip for assets for assets <-> bytes and assets <-> words. #[test] - fn test_asset_serde() { + fn test_asset_serde() -> anyhow::Result<()> { for fungible_account_id in [ ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, @@ -328,6 +280,13 @@ mod tests { let account_id = AccountId::try_from(fungible_account_id).unwrap(); let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into(); assert_eq!(fungible_asset, Asset::read_from_bytes(&fungible_asset.to_bytes()).unwrap()); + assert_eq!( + fungible_asset, + Asset::from_key_value_words( + fungible_asset.to_key_word(), + fungible_asset.to_value_word() + )?, + ); } for non_fungible_account_id in [ @@ -336,50 +295,33 @@ mod tests { ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, ] { let account_id = AccountId::try_from(non_fungible_account_id).unwrap(); - let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); + let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(); let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into(); assert_eq!( non_fungible_asset, Asset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap() ); - } - } - - #[test] - fn test_new_unchecked() { - for fungible_account_id in [ - ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_2, - ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3, - ] { - let account_id = AccountId::try_from(fungible_account_id).unwrap(); - let fungible_asset: Asset = FungibleAsset::new(account_id, 10).unwrap().into(); - assert_eq!(fungible_asset, Asset::new_unchecked(Word::from(&fungible_asset))); + assert_eq!( + non_fungible_asset, + Asset::from_key_value_words( + non_fungible_asset.to_key_word(), + non_fungible_asset.to_value_word() + )? + ); } - for non_fungible_account_id in [ - ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, - ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, - ] { - let account_id = AccountId::try_from(non_fungible_account_id).unwrap(); - let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); - let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into(); - assert_eq!(non_fungible_asset, Asset::new_unchecked(Word::from(non_fungible_asset))); - } + Ok(()) } - /// This test asserts that account ID's prefix is serialized in the first felt of assets. + /// This test asserts that account ID's is serialized in the first felt of assets. /// Asset deserialization relies on that fact and if this changes the serialization must /// be updated. #[test] - fn test_account_id_prefix_is_in_first_serialized_felt() { + fn test_account_id_is_serialized_first() { for asset in [FungibleAsset::mock(300), NonFungibleAsset::mock(&[0xaa, 0xbb])] { let serialized_asset = asset.to_bytes(); - let prefix = AccountIdPrefix::read_from_bytes(&serialized_asset).unwrap(); - assert_eq!(prefix, asset.faucet_id_prefix()); + let prefix = AccountId::read_from_bytes(&serialized_asset).unwrap(); + assert_eq!(prefix, asset.faucet_id()); } } } diff --git a/crates/miden-protocol/src/asset/nonfungible.rs b/crates/miden-protocol/src/asset/nonfungible.rs index 394c43c276..47405d078d 100644 --- a/crates/miden-protocol/src/asset/nonfungible.rs +++ b/crates/miden-protocol/src/asset/nonfungible.rs @@ -1,44 +1,27 @@ -use alloc::boxed::Box; use alloc::string::ToString; use alloc::vec::Vec; use core::fmt; use super::vault::AssetVaultKey; -use super::{AccountIdPrefix, AccountType, Asset, AssetError, Felt, Hasher, Word}; +use super::{AccountType, Asset, AssetError, Word}; +use crate::Hasher; +use crate::account::AccountId; +use crate::asset::vault::AssetId; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use crate::{FieldElement, WORD_SIZE}; - -/// Position of the faucet_id inside the [`NonFungibleAsset`] word having fields in BigEndian. -const FAUCET_ID_POS_BE: usize = 3; // NON-FUNGIBLE ASSET // ================================================================================================ /// A commitment to a non-fungible asset. /// -/// The commitment is constructed as follows: -/// -/// - Hash the asset data producing `[hash0, hash1, hash2, hash3]`. -/// - Replace the value of `hash3` with the prefix of the faucet id (`faucet_id_prefix`) producing -/// `[hash0, hash1, hash2, faucet_id_prefix]`. -/// - This layout ensures that fungible and non-fungible assets are distinguishable by interpreting -/// the 3rd element of an asset as an [`AccountIdPrefix`] and checking its type. +/// See [`Asset`] for details on how it is constructed. /// /// [`NonFungibleAsset`] itself does not contain the actual asset data. The container for this data /// is [`NonFungibleAssetDetails`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct NonFungibleAsset(Word); - -impl PartialOrd for NonFungibleAsset { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for NonFungibleAsset { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.cmp(&other.0) - } +pub struct NonFungibleAsset { + faucet_id: AccountId, + value: Word, } impl NonFungibleAsset { @@ -47,8 +30,8 @@ impl NonFungibleAsset { /// The serialized size of a [`NonFungibleAsset`] in bytes. /// - /// Currently represented as a word. - pub const SERIALIZED_SIZE: usize = Felt::ELEMENT_BYTES * WORD_SIZE; + /// An account ID (15 bytes) plus a word (32 bytes). + pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + Word::SERIALIZED_SIZE; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -70,23 +53,46 @@ impl NonFungibleAsset { /// /// # Errors /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet. - pub fn from_parts(faucet_id: AccountIdPrefix, mut data_hash: Word) -> Result { + pub fn from_parts(faucet_id: AccountId, value: Word) -> Result { if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); } - data_hash[FAUCET_ID_POS_BE] = Felt::from(faucet_id); + Ok(Self { faucet_id, value }) + } - Ok(Self(data_hash)) + /// Creates a non-fungible asset from the provided key and value. + /// + /// # Errors + /// + /// Returns an error if: + /// - The provided key does not contain a valid faucet ID. + /// - The provided key's asset ID limbs are not equal to the provided value's first and second + /// element. + /// - The faucet ID is not a non-fungible faucet ID. + pub fn from_key_value(key: AssetVaultKey, value: Word) -> Result { + if key.asset_id().suffix() != value[0] || key.asset_id().prefix() != value[1] { + return Err(AssetError::NonFungibleAssetIdMustMatchValue { + asset_id: key.asset_id(), + value, + }); + } + + Self::from_parts(key.faucet_id(), value) } - /// Creates a new [NonFungibleAsset] without checking its validity. + /// Creates a non-fungible asset from the provided key and value. + /// + /// Prefer [`Self::from_key_value`] for more type safety. + /// + /// # Errors /// - /// # Safety - /// This function requires that the provided value is a valid word encoding of a - /// [NonFungibleAsset]. - pub unsafe fn new_unchecked(value: Word) -> NonFungibleAsset { - NonFungibleAsset(value) + /// Returns an error if: + /// - The provided key does not contain a valid faucet ID. + /// - [`Self::from_key_value`] fails. + pub fn from_key_value_words(key: Word, value: Word) -> Result { + let vault_key = AssetVaultKey::try_from(key)?; + Self::from_key_value(vault_key, value) } // ACCESSORS @@ -94,71 +100,36 @@ impl NonFungibleAsset { /// Returns the vault key of the [`NonFungibleAsset`]. /// - /// This is the same as the asset with the following modifications, in this order: - /// - Swaps the faucet ID at index 0 and `hash0` at index 3. - /// - Sets the fungible bit for `hash0` to `0`. - /// - /// # Rationale - /// - /// This means `hash0` will be used as the leaf index in the asset SMT which ensures that a - /// non-fungible faucet's assets generally end up in different leaves as the key is not based on - /// the faucet ID. - /// - /// It also ensures that there is never any collision in the leaf index between a non-fungible - /// asset and a fungible asset, as the former's vault key always has the fungible bit set to `0` - /// and the latter's vault key always has the bit set to `1`. + /// See [`Asset`] docs for details on the key. pub fn vault_key(&self) -> AssetVaultKey { - let mut vault_key = self.0; + let asset_id_suffix = self.value[0]; + let asset_id_prefix = self.value[1]; + let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix); - // Swap prefix of faucet ID with hash0. - vault_key.swap(0, FAUCET_ID_POS_BE); - - // Set the fungible bit to zero. - vault_key[3] = - AccountIdPrefix::clear_fungible_bit(self.faucet_id_prefix().version(), vault_key[3]); - - AssetVaultKey::new_unchecked(vault_key) + AssetVaultKey::new(asset_id, self.faucet_id) + .expect("constructors should ensure account ID is of type non-fungible faucet") } - /// Return ID prefix of the faucet which issued this asset. - pub fn faucet_id_prefix(&self) -> AccountIdPrefix { - AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS_BE]) + /// Returns the ID of the faucet which issued this asset. + pub fn faucet_id(&self) -> AccountId { + self.faucet_id } /// Returns the asset's key encoded to a [`Word`]. pub fn to_key_word(&self) -> Word { - *self.vault_key().as_word() + self.vault_key().to_word() } /// Returns the asset's value encoded to a [`Word`]. pub fn to_value_word(&self) -> Word { - Word::from(*self) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Validates this non-fungible asset. - /// # Errors - /// Returns an error if: - /// - The faucet_id is not a valid non-fungible faucet ID. - /// - The most significant bit of the asset is not ZERO. - fn validate(&self) -> Result<(), AssetError> { - let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS_BE]) - .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; - - let account_type = faucet_id.account_type(); - if !matches!(account_type, AccountType::NonFungibleFaucet) { - return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); - } - - Ok(()) + self.value } } -impl From for Word { - fn from(asset: NonFungibleAsset) -> Self { - asset.0 +impl fmt::Display for NonFungibleAsset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: Replace with hex representation? + write!(f, "{self:?}") } } @@ -168,22 +139,6 @@ impl From for Asset { } } -impl TryFrom for NonFungibleAsset { - type Error = AssetError; - - fn try_from(value: Word) -> Result { - let asset = Self(value); - asset.validate()?; - Ok(asset) - } -} - -impl fmt::Display for NonFungibleAsset { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - // SERIALIZATION // ================================================================================================ @@ -191,10 +146,8 @@ impl Serializable for NonFungibleAsset { fn write_into(&self, target: &mut W) { // All assets should serialize their faucet ID at the first position to allow them to be // easily distinguishable during deserialization. - target.write(self.faucet_id_prefix()); - target.write(self.0[2]); - target.write(self.0[1]); - target.write(self.0[0]); + target.write(self.faucet_id()); + target.write(self.value); } fn get_size_hint(&self) -> usize { @@ -204,31 +157,24 @@ impl Serializable for NonFungibleAsset { impl Deserializable for NonFungibleAsset { fn read_from(source: &mut R) -> Result { - let faucet_id_prefix: AccountIdPrefix = source.read()?; + let faucet_id: AccountId = source.read()?; - Self::deserialize_with_faucet_id_prefix(faucet_id_prefix, source) + Self::deserialize_with_faucet_id(faucet_id, source) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } impl NonFungibleAsset { - /// Deserializes a [`NonFungibleAsset`] from an [`AccountIdPrefix`] and the remaining data from - /// the given `source`. - pub(super) fn deserialize_with_faucet_id_prefix( - faucet_id_prefix: AccountIdPrefix, + /// Deserializes a [`NonFungibleAsset`] from an [`AccountId`] and the remaining data from the + /// given `source`. + pub(super) fn deserialize_with_faucet_id( + faucet_id: AccountId, source: &mut R, ) -> Result { - let hash_2: Felt = source.read()?; - let hash_1: Felt = source.read()?; - let hash_0: Felt = source.read()?; - - // The last felt in the data_hash will be replaced by the faucet id, so we can set it to - // zero here. - NonFungibleAsset::from_parts( - faucet_id_prefix, - Word::from([hash_0, hash_1, hash_2, Felt::ZERO]), - ) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let value: Word = source.read()?; + + NonFungibleAsset::from_parts(faucet_id, value) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -240,7 +186,7 @@ impl NonFungibleAsset { /// Unlike [NonFungibleAsset] struct, this struct contains full details of a non-fungible asset. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NonFungibleAssetDetails { - faucet_id: AccountIdPrefix, + faucet_id: AccountId, asset_data: Vec, } @@ -249,7 +195,7 @@ impl NonFungibleAssetDetails { /// /// # Errors /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet. - pub fn new(faucet_id: AccountIdPrefix, asset_data: Vec) -> Result { + pub fn new(faucet_id: AccountId, asset_data: Vec) -> Result { if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); } @@ -258,7 +204,7 @@ impl NonFungibleAssetDetails { } /// Returns ID of the faucet which issued this asset. - pub fn faucet_id(&self) -> AccountIdPrefix { + pub fn faucet_id(&self) -> AccountId { self.faucet_id } @@ -285,32 +231,66 @@ mod tests { }; #[test] - fn test_non_fungible_asset_serde() { + fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> { + let invalid_key = AssetVaultKey::new( + AssetId::new(1u32.into(), 2u32.into()), + ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?, + )?; + let err = + NonFungibleAsset::from_key_value(invalid_key, Word::from([4, 5, 6, 7u32])).unwrap_err(); + + assert_matches!(err, AssetError::NonFungibleAssetIdMustMatchValue { .. }); + + Ok(()) + } + + #[test] + fn test_non_fungible_asset_serde() -> anyhow::Result<()> { for non_fungible_account_id in [ ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET_1, ] { let account_id = AccountId::try_from(non_fungible_account_id).unwrap(); - let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); + let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(); let non_fungible_asset = NonFungibleAsset::new(&details).unwrap(); assert_eq!( non_fungible_asset, NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap() ); + assert_eq!(non_fungible_asset.to_bytes().len(), non_fungible_asset.get_size_hint()); + + assert_eq!( + non_fungible_asset, + NonFungibleAsset::from_key_value_words( + non_fungible_asset.to_key_word(), + non_fungible_asset.to_value_word() + )? + ) } let account = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(); - let details = NonFungibleAssetDetails::new(account.prefix(), vec![4, 5, 6, 7]).unwrap(); + let details = NonFungibleAssetDetails::new(account, vec![4, 5, 6, 7]).unwrap(); let asset = NonFungibleAsset::new(&details).unwrap(); let mut asset_bytes = asset.to_bytes(); let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(); - // Set invalid Faucet ID Prefix. - asset_bytes[0..8].copy_from_slice(&fungible_faucet_id.prefix().to_bytes()); + // Set invalid faucet ID. + asset_bytes[0..AccountId::SERIALIZED_SIZE].copy_from_slice(&fungible_faucet_id.to_bytes()); let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err(); assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet")); + + Ok(()) + } + + #[test] + fn test_vault_key_for_non_fungible_asset() { + let asset = NonFungibleAsset::mock(&[42]); + + assert_eq!(asset.vault_key().faucet_id(), NonFungibleAsset::mock_issuer()); + assert_eq!(asset.vault_key().asset_id().suffix(), asset.to_value_word()[0]); + assert_eq!(asset.vault_key().asset_id().prefix(), asset.to_value_word()[1]); } } diff --git a/crates/miden-protocol/src/asset/vault/asset_id.rs b/crates/miden-protocol/src/asset/vault/asset_id.rs new file mode 100644 index 0000000000..14c83f1f8f --- /dev/null +++ b/crates/miden-protocol/src/asset/vault/asset_id.rs @@ -0,0 +1,43 @@ +use core::fmt::Display; + +use crate::{Felt, FieldElement}; + +/// The [`AssetId`] in an [`AssetVaultKey`](crate::asset::AssetVaultKey) distinguishes different +/// assets issued by the same faucet. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct AssetId { + suffix: Felt, + prefix: Felt, +} + +impl AssetId { + /// Constructs an asset ID from its parts. + pub fn new(suffix: Felt, prefix: Felt) -> Self { + Self { suffix, prefix } + } + + /// Returns the suffix of the asset ID. + pub fn suffix(&self) -> Felt { + self.suffix + } + + /// Returns the prefix of the asset ID. + pub fn prefix(&self) -> Felt { + self.prefix + } + + /// Returns `true` if both prefix and suffix are zero, `false` otherwise. + pub fn is_empty(&self) -> bool { + self.prefix == Felt::ZERO && self.suffix == Felt::ZERO + } +} + +impl Display for AssetId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_fmt(format_args!( + "0x{:016x}{:016x}", + self.prefix().as_int(), + self.suffix().as_int() + )) + } +} diff --git a/crates/miden-protocol/src/asset/vault/asset_witness.rs b/crates/miden-protocol/src/asset/vault/asset_witness.rs index 503b468d41..b11bcb2ed3 100644 --- a/crates/miden-protocol/src/asset/vault/asset_witness.rs +++ b/crates/miden-protocol/src/asset/vault/asset_witness.rs @@ -1,3 +1,4 @@ +use alloc::boxed::Box; use alloc::string::ToString; use miden_crypto::merkle::InnerNodeInfo; @@ -23,17 +24,12 @@ impl AssetWitness { /// # Errors /// /// Returns an error if: - /// - any of the entries in the SMT leaf is not a valid asset. - /// - any of the entries' vault keys does not match the expected vault key of the asset. + /// - any of the key value pairs in the SMT leaf do not form a valid asset. pub fn new(smt_proof: SmtProof) -> Result { - for (vault_key, asset) in smt_proof.leaf().entries() { - let asset = Asset::try_from(asset)?; - if *vault_key != asset.vault_key().into() { - return Err(AssetError::AssetVaultKeyMismatch { - actual: *vault_key, - expected: asset.vault_key().into(), - }); - } + for (vault_key, asset_value) in smt_proof.leaf().entries() { + // This ensures that vault key and value are consistent. + Asset::from_key_value_words(*vault_key, *asset_value) + .map_err(|err| AssetError::AssetWitnessInvalid(Box::new(err)))?; } Ok(Self(smt_proof)) @@ -72,8 +68,9 @@ impl AssetWitness { SmtLeaf::Multiple(kv_pairs) => kv_pairs, }; - entries.iter().map(|(_key, value)| { - Asset::try_from(value).expect("asset witness should track valid assets") + entries.iter().map(|(key, value)| { + Asset::from_key_value_words(*key, *value) + .expect("asset witness should track valid assets") }) } @@ -132,7 +129,9 @@ mod tests { let err = AssetWitness::new(proof).unwrap_err(); - assert_matches!(err, AssetError::InvalidFaucetAccountId(_)); + assert_matches!(err, AssetError::AssetWitnessInvalid(source) => { + assert_matches!(*source, AssetError::InvalidFaucetAccountId(_)); + }); Ok(()) } @@ -144,15 +143,16 @@ mod tests { let fungible_asset = FungibleAsset::mock(500); let non_fungible_asset = NonFungibleAsset::mock(&[1]); - let smt = - Smt::with_entries([(fungible_asset.vault_key().into(), non_fungible_asset.into())])?; + let smt = Smt::with_entries([( + fungible_asset.vault_key().into(), + non_fungible_asset.to_value_word(), + )])?; let proof = smt.open(&fungible_asset.vault_key().into()); let err = AssetWitness::new(proof).unwrap_err(); - assert_matches!(err, AssetError::AssetVaultKeyMismatch { actual, expected } => { - assert_eq!(actual, fungible_asset.vault_key().into()); - assert_eq!(expected, non_fungible_asset.vault_key().into()); + assert_matches!(err, AssetError::AssetWitnessInvalid(source) => { + assert_matches!(*source, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_)); }); Ok(()) diff --git a/crates/miden-protocol/src/asset/vault/mod.rs b/crates/miden-protocol/src/asset/vault/mod.rs index b74c9abfc4..1612b33c91 100644 --- a/crates/miden-protocol/src/asset/vault/mod.rs +++ b/crates/miden-protocol/src/asset/vault/mod.rs @@ -28,6 +28,9 @@ pub use asset_witness::AssetWitness; mod vault_key; pub use vault_key::AssetVaultKey; +mod asset_id; +pub use asset_id::AssetId; + // ASSET VAULT // ================================================================================================ @@ -61,7 +64,7 @@ impl AssetVault { pub fn new(assets: &[Asset]) -> Result { Ok(Self { asset_tree: Smt::with_entries( - assets.iter().map(|asset| (asset.vault_key().into(), (*asset).into())), + assets.iter().map(|asset| (asset.vault_key().to_word(), asset.to_value_word())), ) .map_err(AssetVaultError::DuplicateAsset)?, }) @@ -78,19 +81,22 @@ impl AssetVault { /// Returns the asset corresponding to the provided asset vault key, or `None` if the asset /// doesn't exist. pub fn get(&self, asset_vault_key: AssetVaultKey) -> Option { - let word = self.asset_tree.get_value(asset_vault_key.as_word()); + let asset_value = self.asset_tree.get_value(&asset_vault_key.to_word()); - if word.is_empty() { + if asset_value.is_empty() { None } else { - Some(Asset::try_from(word).expect("asset vault should only store valid assets")) + Some( + Asset::from_key_value(asset_vault_key, asset_value) + .expect("asset vault should only store valid assets"), + ) } } /// Returns true if the specified non-fungible asset is stored in this vault. pub fn has_non_fungible_asset(&self, asset: NonFungibleAsset) -> Result { // check if the asset is stored in the vault - match self.asset_tree.get_value(&asset.vault_key().into()) { + match self.asset_tree.get_value(&asset.vault_key().to_word()) { asset if asset == Smt::EMPTY_VALUE => Ok(false), _ => Ok(true), } @@ -106,21 +112,22 @@ impl AssetVault { return Err(AssetVaultError::NotAFungibleFaucetId(faucet_id)); } - // if the tree value is [0, 0, 0, 0], the asset is not stored in the vault - match self.asset_tree.get_value( - &AssetVaultKey::from_account_id(faucet_id) - .expect("faucet ID should be of type fungible") - .into(), - ) { - asset if asset == Smt::EMPTY_VALUE => Ok(0), - asset => Ok(FungibleAsset::new_unchecked(asset).amount()), - } + let vault_key = + AssetVaultKey::new_fungible(faucet_id).expect("faucet ID should be of type fungible"); + let asset_value = self.asset_tree.get_value(&vault_key.to_word()); + let asset = FungibleAsset::from_key_value(vault_key, asset_value) + .expect("asset vault should only store valid assets"); + + Ok(asset.amount()) } /// Returns an iterator over the assets stored in the vault. pub fn assets(&self) -> impl Iterator + '_ { // SAFETY: The asset tree tracks only valid assets. - self.asset_tree.entries().map(|(_key, value)| Asset::new_unchecked(*value)) + self.asset_tree.entries().map(|(key, value)| { + Asset::from_key_value_words(*key, *value) + .expect("asset vault should only store valid assets") + }) } /// Returns an iterator over the inner nodes of the underlying [`Smt`]. @@ -132,7 +139,7 @@ impl AssetVault { /// /// The `vault_key` can be obtained with [`Asset::vault_key`]. pub fn open(&self, vault_key: AssetVaultKey) -> AssetWitness { - let smt_proof = self.asset_tree.open(&vault_key.into()); + let smt_proof = self.asset_tree.open(&vault_key.to_word()); // SAFETY: The asset vault should only contain valid assets. AssetWitness::new_unchecked(smt_proof) } @@ -165,7 +172,7 @@ impl AssetVault { /// /// # Errors /// Returns an error: - /// - If the total value of assets is greater than or equal to 2^63. + /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`]. /// - If the delta contains an addition/subtraction for a fungible asset that is not stored in /// the vault. /// - If the delta contains a non-fungible asset removal that is not stored in the vault. @@ -196,7 +203,7 @@ impl AssetVault { /// Add the specified asset to the vault. /// /// # Errors - /// - If the total value of two fungible assets is greater than or equal to 2^63. + /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`]. /// - If the vault already contains the same non-fungible asset. /// - The maximum number of leaves per asset is exceeded. pub fn add_asset(&mut self, asset: Asset) -> Result { @@ -210,26 +217,26 @@ impl AssetVault { /// issued by the same faucet, the amounts are added together. /// /// # Errors - /// - If the total value of assets is greater than or equal to 2^63. + /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`]. /// - The maximum number of leaves per asset is exceeded. fn add_fungible_asset( &mut self, - asset: FungibleAsset, + other_asset: FungibleAsset, ) -> Result { - // fetch current asset value from the tree and add the new asset to it. - let new: FungibleAsset = match self.asset_tree.get_value(&asset.vault_key().into()) { - current if current == Smt::EMPTY_VALUE => asset, - current => { - let current = FungibleAsset::new_unchecked(current); - current.add(asset).map_err(AssetVaultError::AddFungibleAssetBalanceError)? - }, - }; + let current_asset_value = self.asset_tree.get_value(&other_asset.vault_key().to_word()); + let current_asset = + FungibleAsset::from_key_value(other_asset.vault_key(), current_asset_value) + .expect("asset vault should store valid assets"); + + let new_asset = current_asset + .add(other_asset) + .map_err(AssetVaultError::AddFungibleAssetBalanceError)?; + self.asset_tree - .insert(new.vault_key().into(), new.into()) + .insert(new_asset.vault_key().to_word(), new_asset.to_value_word()) .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; - // return the new asset - Ok(new) + Ok(new_asset) } /// Add the specified non-fungible asset to the vault. @@ -244,7 +251,7 @@ impl AssetVault { // add non-fungible asset to the vault let old = self .asset_tree - .insert(asset.vault_key().into(), asset.into()) + .insert(asset.vault_key().to_word(), asset.to_value_word()) .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; // if the asset already exists, return an error @@ -285,30 +292,37 @@ impl AssetVault { /// - The maximum number of leaves per asset is exceeded. fn remove_fungible_asset( &mut self, - asset: FungibleAsset, + other_asset: FungibleAsset, ) -> Result { - // fetch the asset from the vault. - let new: FungibleAsset = match self.asset_tree.get_value(&asset.vault_key().into()) { - current if current == Smt::EMPTY_VALUE => { - return Err(AssetVaultError::FungibleAssetNotFound(asset)); - }, - current => { - let current = FungibleAsset::new_unchecked(current); - current.sub(asset).map_err(AssetVaultError::SubtractFungibleAssetBalanceError)? - }, - }; + let current_asset_value = self.asset_tree.get_value(&other_asset.vault_key().to_word()); + let current_asset = + FungibleAsset::from_key_value(other_asset.vault_key(), current_asset_value) + .expect("asset vault should store valid assets"); + + // If the asset's amount is 0, we consider it absent from the vault. + if current_asset.amount() == 0 { + return Err(AssetVaultError::FungibleAssetNotFound(other_asset)); + } + + let new_asset = current_asset + .sub(other_asset) + .map_err(AssetVaultError::SubtractFungibleAssetBalanceError)?; + + // Note that if new_asset's amount is 0, its value's word representation is equal to + // the empty word, which results in the removal of the entire entry from the corresponding + // leaf. + #[cfg(debug_assertions)] + { + if new_asset.amount() == 0 { + assert!(new_asset.to_value_word().is_empty()) + } + } - // if the amount of the asset is zero, remove the asset from the vault. - let value = match new.amount() { - 0 => Smt::EMPTY_VALUE, - _ => new.into(), - }; self.asset_tree - .insert(new.vault_key().into(), value) + .insert(new_asset.vault_key().to_word(), new_asset.to_value_word()) .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; - // return the asset that was removed. - Ok(asset) + Ok(other_asset) } /// Remove the specified non-fungible asset from the vault and returns the asset that was just @@ -324,7 +338,7 @@ impl AssetVault { // remove the asset from the vault. let old = self .asset_tree - .insert(asset.vault_key().into(), Smt::EMPTY_VALUE) + .insert(asset.vault_key().to_word(), Smt::EMPTY_VALUE) .map_err(AssetVaultError::MaxLeafEntriesExceeded)?; // return an error if the asset did not exist in the vault. @@ -369,3 +383,20 @@ impl Deserializable for AssetVault { Self::new(&assets).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + + #[test] + fn vault_fails_on_absent_fungible_asset() { + let mut vault = AssetVault::default(); + let err = vault.remove_asset(FungibleAsset::mock(50)).unwrap_err(); + assert_matches!(err, AssetVaultError::FungibleAssetNotFound(_)); + } +} diff --git a/crates/miden-protocol/src/asset/vault/partial.rs b/crates/miden-protocol/src/asset/vault/partial.rs index 1427a8902c..3dee3bbce5 100644 --- a/crates/miden-protocol/src/asset/vault/partial.rs +++ b/crates/miden-protocol/src/asset/vault/partial.rs @@ -98,13 +98,16 @@ impl PartialVault { /// Returns an error if: /// - the key is not tracked by this partial SMT. pub fn get(&self, vault_key: AssetVaultKey) -> Result, MerkleError> { - self.partial_smt.get_value(&vault_key.into()).map(|word| { - if word.is_empty() { + self.partial_smt.get_value(&vault_key.into()).map(|asset_value| { + if asset_value.is_empty() { None } else { // SAFETY: If this returned a non-empty word, then it should be a valid asset, // because the vault should only track valid ones. - Some(Asset::try_from(word).expect("partial vault should only track valid assets")) + Some( + Asset::from_key_value(vault_key, asset_value) + .expect("partial vault should only track valid assets"), + ) } }) } @@ -136,17 +139,11 @@ impl PartialVault { fn validate_entries<'a>( entries: impl IntoIterator, ) -> Result<(), PartialAssetVaultError> { - for (vault_key, asset) in entries { - let asset = Asset::try_from(asset).map_err(|source| { - PartialAssetVaultError::InvalidAssetInSmt { entry: *asset, source } + for (vault_key, asset_value) in entries { + // This ensures that vault key and value are consistent. + Asset::from_key_value_words(*vault_key, *asset_value).map_err(|source| { + PartialAssetVaultError::InvalidAssetInSmt { entry: *asset_value, source } })?; - - if *vault_key != asset.vault_key().into() { - return Err(PartialAssetVaultError::AssetVaultKeyMismatch { - expected: asset.vault_key(), - actual: *vault_key, - }); - } } Ok(()) @@ -216,15 +213,12 @@ mod tests { fn partial_vault_ensures_asset_vault_key_matches() -> anyhow::Result<()> { let asset = FungibleAsset::mock(500); let invalid_vault_key = Word::from([0, 1, 2, 3u32]); - let smt = Smt::with_entries([(invalid_vault_key, asset.into())])?; + let smt = Smt::with_entries([(invalid_vault_key, asset.to_value_word())])?; let proof = smt.open(&invalid_vault_key); let partial_smt = PartialSmt::from_proofs([proof.clone()])?; let err = PartialVault::try_from(partial_smt).unwrap_err(); - assert_matches!(err, PartialAssetVaultError::AssetVaultKeyMismatch { expected, actual } => { - assert_eq!(actual, invalid_vault_key); - assert_eq!(expected, asset.vault_key()); - }); + assert_matches!(err, PartialAssetVaultError::InvalidAssetInSmt { .. }); Ok(()) } diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 4e5bfd0811..45e763b82a 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -1,107 +1,120 @@ +use alloc::boxed::Box; use core::fmt; +use miden_core::LexicographicWord; use miden_crypto::merkle::smt::LeafIndex; use miden_processor::SMT_DEPTH; -use crate::Word; -use crate::account::AccountType::FungibleFaucet; -use crate::account::{AccountId, AccountIdPrefix}; +use crate::account::AccountId; +use crate::account::AccountType::{self}; +use crate::asset::vault::AssetId; use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; use crate::errors::AssetError; +use crate::{Felt, FieldElement, Word}; -/// The key of an [`Asset`] in the asset vault. +/// The unique identifier of an [`Asset`] in the [`AssetVault`](crate::asset::AssetVault). /// -/// The layout of an asset key is: -/// - Fungible asset key: `[0, 0, faucet_id_suffix, faucet_id_prefix]`. -/// - Non-fungible asset key: `[faucet_id_prefix, hash1, hash2, hash0']`, where `hash0'` is -/// equivalent to `hash0` with the fungible bit set to `0`. See [`NonFungibleAsset::vault_key`] -/// for more details. +/// Its [`Word`] layout is: +/// ```text +/// [ +/// asset_id_suffix (64 bits), +/// asset_id_prefix (64 bits), +/// faucet_id_suffix (56 bits), +/// faucet_id_prefix (64 bits) +/// ] +/// ``` /// -/// For details on the layout of an asset, see the documentation of [`Asset`]. -/// -/// ## Guarantees -/// -/// This type guarantees that it contains a valid fungible or non-fungible asset key: -/// - For fungible assets -/// - The felt at index 3 has the fungible bit set to 1 and it is a valid account ID prefix. -/// - The felt at index 2 is a valid account ID suffix. -/// - For non-fungible assets -/// - The felt at index 3 has the fungible bit set to 0. -/// - The felt at index 0 is a valid account ID prefix. -/// -/// The fungible bit is the bit in the [`AccountId`] that encodes whether the ID is a faucet. -#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] -pub struct AssetVaultKey(Word); +/// 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. + asset_id: AssetId, + + /// The ID of the faucet that issued the asset. + faucet_id: AccountId, +} impl AssetVaultKey { - /// Creates a new [`AssetVaultKey`] from the given [`Word`] **without performing validation**. + /// Creates an [`AssetVaultKey`] from its parts. /// - /// ## Warning + /// # Errors /// - /// This function **does not check** whether the provided `Word` represents a valid - /// fungible or non-fungible asset key. - pub fn new_unchecked(value: Word) -> Self { - Self(value) + /// Returns an error if: + /// - the provided ID is not of type + /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) or + /// [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet) + pub fn new(asset_id: AssetId, faucet_id: AccountId) -> Result { + if !faucet_id.is_faucet() { + return Err(AssetError::InvalidFaucetAccountId(Box::from(format!( + "expected account ID of type faucet, found account type {}", + faucet_id.account_type() + )))); + } + + Ok(Self { asset_id, faucet_id }) } - /// Returns an [`AccountIdPrefix`] from the asset key. - pub fn faucet_id_prefix(&self) -> AccountIdPrefix { - if self.is_fungible() { - AccountIdPrefix::new_unchecked(self.0[3]) - } else { - AccountIdPrefix::new_unchecked(self.0[0]) - } + /// 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) } - /// Returns the [`AccountId`] from the asset key if it is a fungible asset, `None` otherwise. - pub fn faucet_id(&self) -> Option { - if self.is_fungible() { - Some(AccountId::new_unchecked([self.0[3], self.0[2]])) - } else { - None - } + /// Returns the [`AssetId`] of the vault key that distinguishes different assets issued by the + /// same faucet. + pub fn asset_id(&self) -> AssetId { + self.asset_id } - /// Returns the leaf index of a vault key. - pub fn to_leaf_index(&self) -> LeafIndex { - LeafIndex::::from(self.0) + /// Returns the [`AccountId`] of the faucet that issued the asset. + pub fn faucet_id(&self) -> AccountId { + self.faucet_id } /// Constructs a fungible asset's key from a faucet ID. /// /// Returns `None` if the provided ID is not of type /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) - pub fn from_account_id(faucet_id: AccountId) -> Option { - match faucet_id.account_type() { - FungibleFaucet => { - let mut key = Word::empty(); - key[2] = faucet_id.suffix(); - key[3] = faucet_id.prefix().as_felt(); - Some(AssetVaultKey::new_unchecked(key)) - }, - _ => None, + pub fn new_fungible(faucet_id: AccountId) -> Option { + if matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { + let asset_id = AssetId::new(Felt::ZERO, Felt::ZERO); + Some( + Self::new(asset_id, faucet_id) + .expect("we should have account type fungible faucet"), + ) + } else { + None } } - /// Returns a reference to the inner [Word] of this key. - pub fn as_word(&self) -> &Word { - &self.0 + /// Returns the leaf index of a vault key. + pub fn to_leaf_index(&self) -> LeafIndex { + LeafIndex::::from(self.to_word()) } +} + +// CONVERSIONS +// ================================================================================================ - /// Returns `true` if the asset key is for a fungible asset, `false` otherwise. - fn is_fungible(&self) -> bool { - self.0[0].as_int() == 0 && self.0[1].as_int() == 0 +impl From for Word { + fn from(vault_key: AssetVaultKey) -> Self { + vault_key.to_word() } } -impl fmt::Display for AssetVaultKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) +impl Ord for AssetVaultKey { + /// Implements comparison based on [`LexicographicWord`]. + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + LexicographicWord::new(self.to_word()).cmp(&LexicographicWord::new(other.to_word())) } } -// CONVERSIONS -// ================================================================================================ +impl PartialOrd for AssetVaultKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} impl TryFrom for AssetVaultKey { type Error = AssetError; @@ -111,18 +124,24 @@ impl TryFrom for AssetVaultKey { /// # Errors /// /// Returns an error if: - /// - TODO(expand_assets) + /// - the faucet ID in the key is invalid. fn try_from(key: Word) -> Result { - // TODO(expand_assets): Implement validation once the new structure of the asset vault - // key is defined. + let asset_id_suffix = key[0]; + let asset_id_prefix = key[1]; + let faucet_id_suffix = key[2]; + let faucet_id_prefix = key[3]; + + let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix); + let faucet_id = AccountId::try_from([faucet_id_prefix, faucet_id_suffix]) + .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; - Ok(Self::new_unchecked(key)) + Self::new(asset_id, faucet_id) } } -impl From for Word { - fn from(vault_key: AssetVaultKey) -> Self { - vault_key.0 +impl fmt::Display for AssetVaultKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_word().to_hex()) } } @@ -144,56 +163,11 @@ impl From for AssetVaultKey { } } -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use miden_core::Felt; - - use super::*; - use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; - - fn make_non_fungible_key(prefix: u64) -> AssetVaultKey { - let word = [Felt::new(prefix), Felt::new(11), Felt::new(22), Felt::new(33)].into(); - AssetVaultKey::new_unchecked(word) - } - - #[test] - fn test_faucet_id_for_fungible_asset() { - let id = AccountId::dummy( - [0xff; 15], - AccountIdVersion::Version0, - AccountType::FungibleFaucet, - AccountStorageMode::Public, - ); - - let key = - AssetVaultKey::from_account_id(id).expect("Expected AssetVaultKey for FungibleFaucet"); - - // faucet_id_prefix() should match AccountId prefix - assert_eq!(key.faucet_id_prefix(), id.prefix()); - - // faucet_id() should return the same account id - assert_eq!(key.faucet_id().unwrap(), id); - } - - #[test] - fn test_faucet_id_for_non_fungible_asset() { - let id = AccountId::dummy( - [0xff; 15], - AccountIdVersion::Version0, - AccountType::NonFungibleFaucet, - AccountStorageMode::Public, - ); - - let prefix_value = id.prefix().as_u64(); - let key = make_non_fungible_key(prefix_value); - - // faucet_id_prefix() should match AccountId prefix - assert_eq!(key.faucet_id_prefix(), id.prefix()); - - // faucet_id() should return the None - assert_eq!(key.faucet_id(), None); - } +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(), + ]) } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 27af4a9912..136f0338bc 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -29,7 +29,7 @@ use crate::account::{ StorageSlotName, }; use crate::address::AddressType; -use crate::asset::AssetVaultKey; +use crate::asset::AssetId; use crate::batch::BatchId; use crate::block::BlockNumber; use crate::note::{NoteAssets, NoteAttachmentArray, NoteTag, NoteType, Nullifier}; @@ -443,8 +443,6 @@ pub enum AssetError { FungibleAssetAmountTooBig(u64), #[error("subtracting {subtrahend} from fungible asset amount {minuend} would underflow")] FungibleAssetAmountNotSufficient { minuend: u64, subtrahend: u64 }, - #[error("fungible asset word {0} does not contain expected ZERO at word index 1")] - FungibleAssetExpectedZero(Word), #[error( "cannot add fungible asset with issuer {other_issuer} to fungible asset with issuer {original_issuer}" )] @@ -454,22 +452,30 @@ pub enum AssetError { }, #[error("faucet account ID in asset is invalid")] InvalidFaucetAccountId(#[source] Box), - #[error("faucet account ID in asset has a non-faucet prefix: {}", .0)] - InvalidFaucetAccountIdPrefix(AccountIdPrefix), #[error( "faucet id {0} of type {id_type} must be of type {expected_ty} for fungible assets", id_type = .0.account_type(), expected_ty = AccountType::FungibleFaucet )] FungibleFaucetIdTypeMismatch(AccountId), + #[error( + "asset ID prefix and suffix in a non-fungible asset's vault key must match indices 0 and 1 in the value, but asset ID was {asset_id} and value was {value}" + )] + NonFungibleAssetIdMustMatchValue { asset_id: AssetId, value: Word }, + #[error("asset ID prefix and suffix in a fungible asset's vault key must be zero but was {0}")] + FungibleAssetIdMustBeZero(AssetId), + #[error( + "the three most significant elements in a fungible asset's value must be zero but provided value was {0}" + )] + FungibleAssetValueMostSignificantElementsMustBeZero(Word), #[error( "faucet id {0} of type {id_type} must be of type {expected_ty} for non fungible assets", id_type = .0.account_type(), expected_ty = AccountType::NonFungibleFaucet )] - NonFungibleFaucetIdTypeMismatch(AccountIdPrefix), - #[error("asset vault key {actual} does not match expected asset vault key {expected}")] - AssetVaultKeyMismatch { actual: Word, expected: Word }, + NonFungibleFaucetIdTypeMismatch(AccountId), + #[error("smt proof in asset witness contains invalid key or value")] + AssetWitnessInvalid(#[source] Box), } // TOKEN SYMBOL ERROR @@ -517,8 +523,6 @@ pub enum AssetVaultError { pub enum PartialAssetVaultError { #[error("provided SMT entry {entry} is not a valid asset")] InvalidAssetInSmt { entry: Word, source: AssetError }, - #[error("expected asset vault key to be {expected} but it was {actual}")] - AssetVaultKeyMismatch { expected: AssetVaultKey, actual: Word }, #[error("failed to add asset proof")] FailedToAddProof(#[source] MerkleError), #[error("asset is not tracked in the partial vault")] diff --git a/crates/miden-protocol/src/note/assets.rs b/crates/miden-protocol/src/note/assets.rs index d0fa47dcd5..20effe4d73 100644 --- a/crates/miden-protocol/src/note/assets.rs +++ b/crates/miden-protocol/src/note/assets.rs @@ -250,7 +250,7 @@ mod tests { let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(); let faucet_id_2 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap(); - let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); + let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(); let asset1 = Asset::Fungible(FungibleAsset::new(faucet_id_1, 100).unwrap()); let asset2 = Asset::Fungible(FungibleAsset::new(faucet_id_2, 50).unwrap()); diff --git a/crates/miden-protocol/src/testing/asset.rs b/crates/miden-protocol/src/testing/asset.rs index b1b12223bd..a89be85545 100644 --- a/crates/miden-protocol/src/testing/asset.rs +++ b/crates/miden-protocol/src/testing/asset.rs @@ -1,97 +1,15 @@ -use rand::Rng; -use rand::distr::StandardUniform; - -use crate::account::{AccountId, AccountIdPrefix, AccountType}; +use crate::account::AccountId; use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; -use crate::errors::AssetError; use crate::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, }; -/// Builder for an `NonFungibleAssetDetails`, the builder can be configured and used multiplied -/// times. -#[derive(Debug, Clone)] -pub struct NonFungibleAssetDetailsBuilder { - faucet_id: AccountIdPrefix, - rng: T, -} - -/// Builder for an `FungibleAsset`, the builder can be configured and used multiplied times. -#[derive(Debug, Clone)] -pub struct FungibleAssetBuilder { - faucet_id: AccountId, - amount: u64, -} - -impl NonFungibleAssetDetailsBuilder { - pub fn new(faucet_id: AccountIdPrefix, rng: T) -> Result { - if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { - return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); - } - - Ok(Self { faucet_id, rng }) - } - - pub fn build(&mut self) -> Result { - let data = (&mut self.rng).sample_iter(StandardUniform).take(5).collect(); - NonFungibleAssetDetails::new(self.faucet_id, data) - } -} - -/// Builder for an `NonFungibleAsset`, the builder can be configured and used multiplied times. -#[derive(Debug, Clone)] -pub struct NonFungibleAssetBuilder { - details_builder: NonFungibleAssetDetailsBuilder, -} - -impl NonFungibleAssetBuilder { - pub fn new(faucet_id: AccountIdPrefix, rng: T) -> Result { - let details_builder = NonFungibleAssetDetailsBuilder::new(faucet_id, rng)?; - Ok(Self { details_builder }) - } - - pub fn build(&mut self) -> Result { - let details = self.details_builder.build()?; - NonFungibleAsset::new(&details) - } -} - -impl FungibleAssetBuilder { - pub const DEFAULT_AMOUNT: u64 = 10; - - pub fn new(faucet_id: AccountId) -> Result { - let account_type = faucet_id.account_type(); - if !matches!(account_type, AccountType::FungibleFaucet) { - return Err(AssetError::FungibleFaucetIdTypeMismatch(faucet_id)); - } - - Ok(Self { faucet_id, amount: Self::DEFAULT_AMOUNT }) - } - - pub fn amount(&mut self, amount: u64) -> Result<&mut Self, AssetError> { - if amount > FungibleAsset::MAX_AMOUNT { - return Err(AssetError::FungibleAssetAmountTooBig(amount)); - } - - self.amount = amount; - Ok(self) - } - - pub fn with_amount(&self, amount: u64) -> Result { - FungibleAsset::new(self.faucet_id, amount) - } - - pub fn build(&self) -> Result { - FungibleAsset::new(self.faucet_id, self.amount) - } -} - impl NonFungibleAsset { /// Returns a mocked non-fungible asset, issued by [ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET]. pub fn mock(asset_data: &[u8]) -> Asset { let non_fungible_asset_details = NonFungibleAssetDetails::new( - AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap().prefix(), + AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(), asset_data.to_vec(), ) .unwrap(); diff --git a/crates/miden-protocol/src/transaction/inputs/mod.rs b/crates/miden-protocol/src/transaction/inputs/mod.rs index b575a7cefd..d3309316c8 100644 --- a/crates/miden-protocol/src/transaction/inputs/mod.rs +++ b/crates/miden-protocol/src/transaction/inputs/mod.rs @@ -348,8 +348,8 @@ impl TransactionInputs { let asset = smt_leaf .entries() .iter() - .find(|(key, _value)| key == asset_key.as_word()) - .map(|(_key, value)| Asset::try_from(value)) + .find(|(key, _value)| key == &asset_key.to_word()) + .map(|(_key, value)| Asset::from_key_value(asset_key, *value)) .transpose()?; Ok(asset) diff --git a/crates/miden-protocol/src/transaction/kernel/mod.rs b/crates/miden-protocol/src/transaction/kernel/mod.rs index 86011b5963..8f9a882414 100644 --- a/crates/miden-protocol/src/transaction/kernel/mod.rs +++ b/crates/miden-protocol/src/transaction/kernel/mod.rs @@ -221,12 +221,16 @@ impl TransactionKernel { ) -> StackOutputs { let account_update_commitment = Hasher::merge(&[final_account_commitment, account_delta_commitment]); - let mut outputs: Vec = Vec::with_capacity(9); + + let mut outputs: Vec = Vec::with_capacity(12); outputs.push(Felt::from(expiration_block_num)); - outputs.extend(Word::from(fee)); + outputs.push(Felt::try_from(fee.amount()).expect("amount should fit into felt")); + outputs.push(fee.faucet_id().suffix()); + outputs.push(fee.faucet_id().prefix().as_felt()); outputs.extend(account_update_commitment); outputs.extend(output_notes_commitment); outputs.reverse(); + StackOutputs::new(outputs) .map_err(|e| e.to_string()) .expect("Invalid stack output") @@ -270,13 +274,19 @@ impl TransactionKernel { .get_stack_word_be(TransactionOutputs::ACCOUNT_UPDATE_COMMITMENT_WORD_IDX * 4) .expect("account_update_commitment (second word) missing"); - let fee = stack - .get_stack_word_be(TransactionOutputs::FEE_ASSET_WORD_IDX * 4) - .expect("fee_asset (third word) missing"); + let native_asset_id_prefix = stack + .get_stack_item(TransactionOutputs::NATIVE_ASSET_ID_PREFIX_ELEMENT_IDX) + .expect("native_asset_id_prefix missing"); + let native_asset_id_suffix = stack + .get_stack_item(TransactionOutputs::NATIVE_ASSET_ID_SUFFIX_ELEMENT_IDX) + .expect("native_asset_id_suffix missing"); + let fee_amount = stack + .get_stack_item(TransactionOutputs::FEE_AMOUNT_ELEMENT_IDX) + .expect("fee_amount missing"); let expiration_block_num = stack .get_stack_item(TransactionOutputs::EXPIRATION_BLOCK_ELEMENT_IDX) - .expect("tx_expiration_block_num (element on index 12) missing"); + .expect("tx_expiration_block_num missing"); let expiration_block_num = u32::try_from(expiration_block_num.as_int()) .map_err(|_| { @@ -296,7 +306,9 @@ impl TransactionKernel { )); } - let fee = FungibleAsset::try_from(fee) + let native_asset_id = AccountId::try_from([native_asset_id_prefix, native_asset_id_suffix]) + .expect("native asset ID should be validated by the tx kernel"); + let fee = FungibleAsset::new(native_asset_id, fee_amount.as_int()) .map_err(TransactionOutputError::FeeAssetNotFungibleAsset)?; Ok((output_notes_commitment, account_update_commitment, fee, expiration_block_num)) diff --git a/crates/miden-protocol/src/transaction/outputs.rs b/crates/miden-protocol/src/transaction/outputs.rs index 179da30367..161e580ccf 100644 --- a/crates/miden-protocol/src/transaction/outputs.rs +++ b/crates/miden-protocol/src/transaction/outputs.rs @@ -54,11 +54,19 @@ impl TransactionOutputs { /// The index of the word at which the account update commitment is stored on the output stack. pub const ACCOUNT_UPDATE_COMMITMENT_WORD_IDX: usize = 1; - /// The index of the word at which the fee asset is stored on the output stack. - pub const FEE_ASSET_WORD_IDX: usize = 2; + /// The index of the element at which the ID prefix of the faucet that issues the native asset + /// is stored on the output stack. + pub const NATIVE_ASSET_ID_PREFIX_ELEMENT_IDX: usize = 8; + + /// The index of the element at which the ID suffix of the faucet that issues the native asset + /// is stored on the output stack. + pub const NATIVE_ASSET_ID_SUFFIX_ELEMENT_IDX: usize = 9; + + /// The index of the element at which the fee amount is stored on the output stack. + pub const FEE_AMOUNT_ELEMENT_IDX: usize = 10; /// The index of the item at which the expiration block height is stored on the output stack. - pub const EXPIRATION_BLOCK_ELEMENT_IDX: usize = 12; + pub const EXPIRATION_BLOCK_ELEMENT_IDX: usize = 11; } impl Serializable for TransactionOutputs { diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index a2aafce62e..bc6a75bc2e 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -186,9 +186,9 @@ impl AccountComponentInterface { let asset = partial_note.assets().iter().next().expect("note should contain an asset"); - if asset.faucet_id_prefix() != sender_account_id.prefix() { + if asset.faucet_id() != sender_account_id { return Err(AccountInterfaceError::IssuanceFaucetMismatch( - asset.faucet_id_prefix(), + asset.faucet_id(), )); } diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index 3b9746ae2d..41181e037e 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -1,7 +1,7 @@ use alloc::string::String; use alloc::vec::Vec; -use miden_protocol::account::{AccountId, AccountIdPrefix, AccountType}; +use miden_protocol::account::{AccountId, AccountType}; use miden_protocol::note::{NoteAttachmentContent, PartialNote}; use miden_protocol::transaction::TransactionScript; use thiserror::Error; @@ -249,8 +249,8 @@ pub enum NoteAccountCompatibility { /// Account interface related errors. #[derive(Debug, Error)] pub enum AccountInterfaceError { - #[error("note asset is not issued by this faucet: {0}")] - IssuanceFaucetMismatch(AccountIdPrefix), + #[error("note asset is not issued by faucet {0}")] + IssuanceFaucetMismatch(AccountId), #[error("note created by the basic fungible faucet doesn't contain exactly one asset")] FaucetNoteWithoutAsset, #[error("invalid transaction script")] diff --git a/crates/miden-standards/src/note/swap.rs b/crates/miden-standards/src/note/swap.rs index 506c44bd85..b06bb7fa64 100644 --- a/crates/miden-standards/src/note/swap.rs +++ b/crates/miden-standards/src/note/swap.rs @@ -155,10 +155,10 @@ impl SwapNote { swap_use_case_id |= (swap_root_bytes[1] >> 2) as u16; // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload. - let offered_asset_id: u64 = offered_asset.faucet_id_prefix().into(); + let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into(); let offered_asset_tag = (offered_asset_id >> 56) as u8; - let requested_asset_id: u64 = requested_asset.faucet_id_prefix().into(); + let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into(); let requested_asset_tag = (requested_asset_id >> 56) as u8; let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); @@ -215,8 +215,7 @@ mod tests { AccountIdVersion::Version0, AccountType::NonFungibleFaucet, AccountStorageMode::Public, - ) - .prefix(), + ), vec![0xaa, 0xbb, 0xcc, 0xdd], ) .unwrap(), 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 69752382cc..30cb6c8350 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -1326,7 +1326,7 @@ async fn test_get_init_asset() -> anyhow::Result<()> { # => [note_idx] push.{REMOVED_ASSET_VALUE} - push.{REMOVED_ASSET_KEY} + push.{ASSET_KEY} exec.util::move_asset_to_note # => [] @@ -1342,15 +1342,14 @@ async fn test_get_init_asset() -> anyhow::Result<()> { push.{ASSET_KEY} exec.active_account::get_initial_asset # => [INITIAL_ASSET] - push.{INITIAL_ASSET} + push.{INITIAL_ASSET_VALUE} assert_eqw.err="initial asset is incorrect" end "#, - ASSET_KEY = fungible_asset_for_note_existing.vault_key(), + ASSET_KEY = fungible_asset_for_note_existing.to_key_word(), REMOVED_ASSET_VALUE = fungible_asset_for_note_existing.to_value_word(), - REMOVED_ASSET_KEY = fungible_asset_for_note_existing.to_key_word(), - INITIAL_ASSET = Word::from(fungible_asset_for_account), - FINAL_ASSET = Word::from(final_asset), + INITIAL_ASSET_VALUE = fungible_asset_for_account.to_value_word(), + FINAL_ASSET = final_asset.to_value_word(), ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(remove_existing_source)?; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs b/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs index 088db3da34..cdbd3fb857 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs @@ -18,7 +18,13 @@ use miden_protocol::account::{ StorageSlotDelta, StorageSlotName, }; -use miden_protocol::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}; +use miden_protocol::asset::{ + Asset, + AssetVault, + FungibleAsset, + NonFungibleAsset, + NonFungibleAssetDetails, +}; use miden_protocol::note::{Note, NoteTag, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, @@ -28,7 +34,6 @@ use miden_protocol::testing::account_id::{ ACCOUNT_ID_SENDER, AccountIdBuilder, }; -use miden_protocol::testing::asset::NonFungibleAssetBuilder; use miden_protocol::testing::constants::{ CONSUMED_ASSET_1_AMOUNT, CONSUMED_ASSET_3_AMOUNT, @@ -520,10 +525,22 @@ async fn non_fungible_asset_delta() -> anyhow::Result<()> { .account_type(AccountType::NonFungibleFaucet) .build_with_seed(rng.random()); - let asset0 = NonFungibleAssetBuilder::new(faucet0.prefix(), &mut rng)?.build()?; - let asset1 = NonFungibleAssetBuilder::new(faucet1.prefix(), &mut rng)?.build()?; - let asset2 = NonFungibleAssetBuilder::new(faucet2.prefix(), &mut rng)?.build()?; - let asset3 = NonFungibleAssetBuilder::new(faucet3.prefix(), &mut rng)?.build()?; + let asset0 = NonFungibleAsset::new(&NonFungibleAssetDetails::new( + faucet0, + rng.random::<[u8; 32]>().to_vec(), + )?)?; + let asset1 = NonFungibleAsset::new(&NonFungibleAssetDetails::new( + faucet1, + rng.random::<[u8; 32]>().to_vec(), + )?)?; + let asset2 = NonFungibleAsset::new(&NonFungibleAssetDetails::new( + faucet2, + rng.random::<[u8; 32]>().to_vec(), + )?)?; + let asset3 = NonFungibleAsset::new(&NonFungibleAssetDetails::new( + faucet3, + rng.random::<[u8; 32]>().to_vec(), + )?)?; let TestSetup { mock_chain, account_id, notes } = setup_test([], [asset1, asset3].map(Asset::from), [asset0, asset2].map(Asset::from))?; @@ -571,20 +588,20 @@ async fn non_fungible_asset_delta() -> anyhow::Result<()> { .account_delta() .vault() .added_assets() - .map(|asset| (asset.faucet_id_prefix(), asset.unwrap_non_fungible())) + .map(|asset| (asset.faucet_id(), asset.unwrap_non_fungible())) .collect::>(); let mut removed_assets = executed_tx .account_delta() .vault() .removed_assets() - .map(|asset| (asset.faucet_id_prefix(), asset.unwrap_non_fungible())) + .map(|asset| (asset.faucet_id(), asset.unwrap_non_fungible())) .collect::>(); assert_eq!(added_assets.len(), 1); assert_eq!(removed_assets.len(), 1); - assert_eq!(added_assets.remove(&asset0.faucet_id_prefix()).unwrap(), asset0); - assert_eq!(removed_assets.remove(&asset1.faucet_id_prefix()).unwrap(), asset1); + assert_eq!(added_assets.remove(&asset0.faucet_id()).unwrap(), asset0); + assert_eq!(removed_assets.remove(&asset1.faucet_id()).unwrap(), asset1); Ok(()) } 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 dfb2d64b27..8264fa06d7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -1,10 +1,26 @@ -use miden_protocol::Hasher; -use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; -use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; +use miden_protocol::Word; +use miden_protocol::account::AccountId; +use miden_protocol::asset::{AssetId, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; +use miden_protocol::errors::MasmError; +use miden_protocol::errors::tx_kernel::{ + ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, + ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE, + ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO, + ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO, + ERR_NON_FUNGIBLE_ASSET_ID_PREFIX_MUST_MATCH_HASH1, + ERR_NON_FUNGIBLE_ASSET_ID_SUFFIX_MUST_MATCH_HASH0, + ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE, +}; +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, + ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE, +}; use miden_protocol::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; -use crate::TransactionContextBuilder; +use crate::executor::CodeExecutor; use crate::kernel_tests::tx::ExecutionOutputExt; +use crate::{TransactionContextBuilder, assert_execution_error}; #[tokio::test] async fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { @@ -46,7 +62,11 @@ async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) .build()?; - let non_fungible_asset = NonFungibleAsset::mock(&NON_FUNGIBLE_ASSET_DATA); + let non_fungible_asset_details = NonFungibleAssetDetails::new( + NonFungibleAsset::mock_issuer(), + NON_FUNGIBLE_ASSET_DATA.to_vec(), + )?; + let non_fungible_asset = NonFungibleAsset::new(&non_fungible_asset_details)?; let code = format!( " @@ -57,14 +77,14 @@ async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { exec.prologue::prepare_transaction # push non-fungible asset data hash onto the stack - push.{non_fungible_asset_data_hash} + push.{NON_FUNGIBLE_ASSET_DATA_HASH} exec.faucet::create_non_fungible_asset # truncate the stack exec.::miden::core::sys::truncate_stack end ", - non_fungible_asset_data_hash = Hasher::hash(&NON_FUNGIBLE_ASSET_DATA), + NON_FUNGIBLE_ASSET_DATA_HASH = non_fungible_asset.to_value_word(), ); let exec_output = &tx_context.execute_code(&code).await?; @@ -75,34 +95,128 @@ async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { Ok(()) } +#[rstest::rstest] +#[case::account_is_not_non_fungible_faucet( + ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?, + AssetId::default(), + ERR_NON_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_NON_FUNGIBLE +)] +#[case::asset_id_suffix_mismatch( + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?, + AssetId::new(0u32.into(), 4u32.into()), + ERR_NON_FUNGIBLE_ASSET_ID_SUFFIX_MUST_MATCH_HASH0 +)] +#[case::asset_id_prefix_mismatch( + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?, + AssetId::new(5u32.into(), 0u32.into()), + ERR_NON_FUNGIBLE_ASSET_ID_PREFIX_MUST_MATCH_HASH1 +)] #[tokio::test] -async fn test_validate_non_fungible_asset() -> anyhow::Result<()> { - let tx_context = - TransactionContextBuilder::with_non_fungible_faucet(NonFungibleAsset::mock_issuer().into()) - .build()?; - let non_fungible_asset = NonFungibleAsset::mock(&[1, 2, 3]); +async fn test_validate_non_fungible_asset( + #[case] account_id: AccountId, + #[case] asset_id: AssetId, + #[case] expected_err: MasmError, +) -> anyhow::Result<()> { + let code = format!( + " + use $kernel::non_fungible_asset + + begin + # a random asset value + push.5 push.4 push.3 push.2 + # => [2, 3, hash1 = 4, hash0 = 5] + + push.{asset_id_suffix} + push.{asset_id_prefix} + push.{account_id_suffix} + push.{account_id_prefix} + # => [ASSET_KEY, ASSET_VALUE] + + exec.non_fungible_asset::validate + + # truncate the stack + swapdw dropw dropw + end + ", + asset_id_suffix = asset_id.suffix(), + asset_id_prefix = asset_id.prefix(), + account_id_suffix = account_id.suffix(), + account_id_prefix = account_id.prefix().as_felt(), + ); + + let exec_result = CodeExecutor::with_default_host().run(&code).await; + + assert_execution_error!(exec_result, expected_err); + + Ok(()) +} +#[rstest::rstest] +#[case::account_is_not_fungible_faucet( + ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into()?, + AssetId::default(), + Word::empty(), + ERR_FUNGIBLE_ASSET_KEY_ACCOUNT_ID_MUST_BE_FUNGIBLE +)] +#[case::asset_id_suffix_is_non_zero( + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?, + AssetId::new(1u32.into(), 0u32.into()), + Word::empty(), + ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO +)] +#[case::asset_id_prefix_is_non_zero( + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?, + AssetId::new(0u32.into(), 1u32.into()), + Word::empty(), + ERR_FUNGIBLE_ASSET_KEY_ASSET_ID_MUST_BE_ZERO +)] +#[case::non_amount_value_is_non_zero( + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?, + AssetId::default(), + Word::from([0, 1, 0, 0u32]), + ERR_FUNGIBLE_ASSET_VALUE_MOST_SIGNIFICANT_ELEMENTS_MUST_BE_ZERO +)] +#[case::amount_exceeds_max( + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?, + AssetId::default(), + Word::try_from([FungibleAsset::MAX_AMOUNT + 1, 0, 0, 0])?, + ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT +)] +#[tokio::test] +async fn test_validate_fungible_asset( + #[case] account_id: AccountId, + #[case] asset_id: AssetId, + #[case] asset_value: Word, + #[case] expected_err: MasmError, +) -> anyhow::Result<()> { let code = format!( " - use $kernel::asset + use $kernel::fungible_asset begin - push.{NON_FUNGIBLE_ASSET_VALUE} - push.{NON_FUNGIBLE_ASSET_KEY} - exec.asset::validate_non_fungible_asset + push.{ASSET_VALUE} + push.{asset_id_suffix} + push.{asset_id_prefix} + push.{account_id_suffix} + push.{account_id_prefix} + # => [ASSET_KEY, ASSET_VALUE] + + exec.fungible_asset::validate # truncate the stack swapdw dropw dropw end ", - NON_FUNGIBLE_ASSET_KEY = non_fungible_asset.to_key_word(), - NON_FUNGIBLE_ASSET_VALUE = non_fungible_asset.to_value_word(), + asset_id_suffix = asset_id.suffix(), + asset_id_prefix = asset_id.prefix(), + account_id_suffix = account_id.suffix(), + account_id_prefix = account_id.prefix().as_felt(), + ASSET_VALUE = asset_value, ); - let exec_output = &tx_context.execute_code(&code).await?; + let exec_result = CodeExecutor::with_default_host().run(&code).await; - assert_eq!(exec_output.get_stack_word_be(0), non_fungible_asset.to_key_word()); - assert_eq!(exec_output.get_stack_word_be(4), non_fungible_asset.to_value_word()); + assert_execution_error!(exec_result, expected_err); 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 14a0c50b70..d67a4e927f 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 @@ -1,4 +1,5 @@ use assert_matches::assert_matches; +use miden_protocol::ONE; use miden_protocol::account::AccountId; use miden_protocol::asset::{ Asset, @@ -23,7 +24,6 @@ use miden_protocol::testing::account_id::{ }; use miden_protocol::testing::constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; use miden_protocol::transaction::memory; -use miden_protocol::{Felt, ONE, Word, ZERO}; use crate::executor::CodeExecutor; use crate::kernel_tests::tx::ExecutionOutputExt; @@ -70,7 +70,7 @@ async fn get_balance_returns_correct_amount() -> anyhow::Result<()> { async fn peek_asset_returns_correct_asset() -> anyhow::Result<()> { let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); - let asset_key = AssetVaultKey::from_account_id(faucet_id).unwrap(); + let asset_key = AssetVaultKey::new_fungible(faucet_id).unwrap(); let code = format!( r#" @@ -91,20 +91,20 @@ async fn peek_asset_returns_correct_asset() -> anyhow::Result<()> { # => [ASSET_KEY, account_vault_root_ptr] exec.asset_vault::peek_asset - # => [PEEKED_ASSET] + # => [PEEKED_ASSET_VALUE] # truncate the stack swapw dropw end "#, - ASSET_KEY = asset_key + ASSET_KEY = asset_key.to_word() ); let exec_output = tx_context.execute_code(&code).await?; assert_eq!( exec_output.get_stack_word_be(0), - Word::from(tx_context.account().vault().get(asset_key).unwrap()) + tx_context.account().vault().get(asset_key).unwrap().to_value_word() ); Ok(()) @@ -157,14 +157,14 @@ async fn test_has_non_fungible_asset() -> anyhow::Result<()> { begin exec.prologue::prepare_transaction - push.{non_fungible_asset_key} + push.{NON_FUNGIBLE_ASSET_KEY} exec.active_account::has_non_fungible_asset # truncate the stack swap drop end ", - non_fungible_asset_key = Word::from(non_fungible_asset) + NON_FUNGIBLE_ASSET_KEY = non_fungible_asset.to_key_word(), ); let exec_output = tx_context.execute_code(&code).await?; @@ -180,13 +180,7 @@ async fn test_add_fungible_asset_success() -> anyhow::Result<()> { let mut account_vault = tx_context.account().vault().clone(); let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let amount = FungibleAsset::MAX_AMOUNT - FUNGIBLE_ASSET_AMOUNT; - let add_fungible_asset = Asset::try_from(Word::new([ - Felt::new(amount), - ZERO, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ])) - .unwrap(); + let add_fungible_asset = FungibleAsset::new(faucet_id, amount)?; let code = format!( " @@ -211,7 +205,10 @@ async fn test_add_fungible_asset_success() -> anyhow::Result<()> { assert_eq!( exec_output.get_stack_word_be(0), - Word::from(account_vault.add_asset(add_fungible_asset).unwrap()) + account_vault + .add_asset(Asset::Fungible(add_fungible_asset)) + .unwrap() + .to_value_word() ); assert_eq!( @@ -229,13 +226,7 @@ async fn test_add_non_fungible_asset_fail_overflow() -> anyhow::Result<()> { let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let amount = FungibleAsset::MAX_AMOUNT - FUNGIBLE_ASSET_AMOUNT + 1; - let add_fungible_asset = Asset::try_from(Word::new([ - Felt::new(amount), - ZERO, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ])) - .unwrap(); + let add_fungible_asset = FungibleAsset::new(faucet_id, amount)?; let code = format!( " @@ -257,7 +248,7 @@ async fn test_add_non_fungible_asset_fail_overflow() -> anyhow::Result<()> { let exec_result = tx_context.execute_code(&code).await; assert_execution_error!(exec_result, ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED); - assert!(account_vault.add_asset(add_fungible_asset).is_err()); + assert!(account_vault.add_asset(Asset::Fungible(add_fungible_asset)).is_err()); Ok(()) } @@ -268,7 +259,7 @@ async fn test_add_non_fungible_asset_success() -> anyhow::Result<()> { let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into()?; let mut account_vault = tx_context.account().vault().clone(); let add_non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new( - &NonFungibleAssetDetails::new(faucet_id.prefix(), vec![1, 2, 3, 4, 5, 6, 7, 8]).unwrap(), + &NonFungibleAssetDetails::new(faucet_id, vec![1, 2, 3, 4, 5, 6, 7, 8]).unwrap(), )?); let code = format!( @@ -294,7 +285,7 @@ async fn test_add_non_fungible_asset_success() -> anyhow::Result<()> { assert_eq!( exec_output.get_stack_word_be(0), - Word::from(account_vault.add_asset(add_non_fungible_asset)?) + account_vault.add_asset(add_non_fungible_asset)?.to_value_word() ); assert_eq!( @@ -311,7 +302,7 @@ async fn test_add_non_fungible_asset_fail_duplicate() -> anyhow::Result<()> { let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); let non_fungible_asset_details = - NonFungibleAssetDetails::new(faucet_id.prefix(), NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); + NonFungibleAssetDetails::new(faucet_id, NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new(&non_fungible_asset_details).unwrap()); @@ -347,13 +338,7 @@ async fn test_remove_fungible_asset_success_no_balance_remaining() -> anyhow::Re let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT; - let remove_fungible_asset = Asset::try_from(Word::new([ - Felt::new(amount), - ZERO, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ])) - .unwrap(); + let remove_fungible_asset = FungibleAsset::new(faucet_id, amount)?; let code = format!( " @@ -378,7 +363,10 @@ async fn test_remove_fungible_asset_success_no_balance_remaining() -> anyhow::Re assert_eq!( exec_output.get_stack_word_be(0), - Word::from(account_vault.remove_asset(remove_fungible_asset).unwrap()) + account_vault + .remove_asset(Asset::Fungible(remove_fungible_asset)) + .unwrap() + .to_value_word() ); assert_eq!( @@ -394,13 +382,7 @@ async fn test_remove_fungible_asset_fail_remove_too_much() -> anyhow::Result<()> let tx_context = TransactionContextBuilder::with_existing_mock_account().build()?; let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT + 1; - let remove_fungible_asset = Asset::try_from(Word::new([ - Felt::new(amount), - ZERO, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ])) - .unwrap(); + let remove_fungible_asset = FungibleAsset::new(faucet_id, amount)?; let code = format!( " @@ -435,13 +417,7 @@ async fn test_remove_fungible_asset_success_balance_remaining() -> anyhow::Resul let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT - 1; - let remove_fungible_asset = Asset::try_from(Word::new([ - Felt::new(amount), - ZERO, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ])) - .unwrap(); + let remove_fungible_asset = FungibleAsset::new(faucet_id, amount)?; let code = format!( " @@ -466,7 +442,10 @@ async fn test_remove_fungible_asset_success_balance_remaining() -> anyhow::Resul assert_eq!( exec_output.get_stack_word_be(0), - Word::from(account_vault.remove_asset(remove_fungible_asset).unwrap()) + account_vault + .remove_asset(Asset::Fungible(remove_fungible_asset)) + .unwrap() + .to_value_word() ); assert_eq!( @@ -484,7 +463,7 @@ async fn test_remove_inexisting_non_fungible_asset_fails() -> anyhow::Result<()> let mut account_vault = tx_context.account().vault().clone(); let non_fungible_asset_details = - NonFungibleAssetDetails::new(faucet_id.prefix(), NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); + NonFungibleAssetDetails::new(faucet_id, NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); let nonfungible = NonFungibleAsset::new(&non_fungible_asset_details).unwrap(); let non_existent_non_fungible_asset = Asset::NonFungible(nonfungible); @@ -528,7 +507,7 @@ async fn test_remove_non_fungible_asset_success() -> anyhow::Result<()> { let faucet_id: AccountId = ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); let non_fungible_asset_details = - NonFungibleAssetDetails::new(faucet_id.prefix(), NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); + NonFungibleAssetDetails::new(faucet_id, NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new(&non_fungible_asset_details).unwrap()); @@ -555,7 +534,7 @@ async fn test_remove_non_fungible_asset_success() -> anyhow::Result<()> { assert_eq!( exec_output.get_stack_word_be(0), - Word::from(account_vault.remove_asset(non_fungible_asset).unwrap()) + account_vault.remove_asset(non_fungible_asset).unwrap().to_value_word() ); assert_eq!( @@ -642,6 +621,85 @@ async fn test_merge_fungible_asset_fails_when_max_amount_exceeded() -> anyhow::R Ok(()) } +/// Tests that splitting a fungible asset returns the correct remaining amount. +#[rstest::rstest] +#[case::different_amounts(FungibleAsset::mock(FUNGIBLE_ASSET_AMOUNT), FungibleAsset::mock(FUNGIBLE_ASSET_AMOUNT - 1))] +#[case::same_amounts( + FungibleAsset::mock(FUNGIBLE_ASSET_AMOUNT), + FungibleAsset::mock(FUNGIBLE_ASSET_AMOUNT) +)] +#[tokio::test] +async fn test_split_fungible_asset_success( + #[case] asset0: Asset, + #[case] asset1: Asset, +) -> anyhow::Result<()> { + let split_asset = asset0.unwrap_fungible().sub(asset1.unwrap_fungible())?; + + let code = format!( + " + use $kernel::fungible_asset + + begin + push.{ASSET0} + push.{ASSET1} + exec.fungible_asset::split + # => [NEW_ASSET_VALUE_0] + + # truncate the stack + swapw dropw + end + ", + ASSET0 = asset0.to_value_word(), + ASSET1 = asset1.to_value_word(), + ); + + let exec_output = CodeExecutor::with_default_host().run(&code).await?; + + assert_eq!(exec_output.get_stack_word_be(0), split_asset.to_value_word()); + + Ok(()) +} + +/// Tests that splitting a fungible asset fails when the amount to withdraw exceeds the balance. +#[tokio::test] +async fn test_split_fungible_asset_fails_when_amount_exceeds_balance() -> anyhow::Result<()> { + let asset0 = FungibleAsset::mock(FUNGIBLE_ASSET_AMOUNT); + let asset1 = FungibleAsset::mock(FUNGIBLE_ASSET_AMOUNT + 1); + + // Sanity check that the Rust implementation errors. + assert_matches!( + asset0.unwrap_fungible().sub(asset1.unwrap_fungible()).unwrap_err(), + AssetError::FungibleAssetAmountNotSufficient { .. } + ); + + let code = format!( + " + use $kernel::fungible_asset + + begin + push.{ASSET0} + push.{ASSET1} + exec.fungible_asset::split + # => [SPLIT_ASSET] + + # truncate the stack + swapw dropw + end + ", + ASSET0 = asset0.to_value_word(), + ASSET1 = asset1.to_value_word(), + ); + + let exec_output = CodeExecutor::with_default_host().run(&code).await; + + assert_execution_error!( + exec_output, + ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW + ); + + Ok(()) +} + /// Tests that merging two different fungible assets fails. #[tokio::test] async fn test_merge_different_fungible_assets_fails() -> anyhow::Result<()> { diff --git a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs index 5d4ea6526e..f72f44e446 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_epilogue.rs @@ -4,7 +4,6 @@ use std::borrow::ToOwned; use miden_processor::crypto::RpoRandomCoin; use miden_processor::{Felt, ONE}; -use miden_protocol::Word; use miden_protocol::account::{Account, AccountDelta, AccountStorageDelta, AccountVaultDelta}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::errors::tx_kernel::{ @@ -27,6 +26,7 @@ use miden_protocol::transaction::memory::{ OUTPUT_NOTE_SECTION_OFFSET, }; use miden_protocol::transaction::{OutputNote, OutputNotes, TransactionOutputs}; +use miden_protocol::{Hasher, Word}; use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::mock_account::MockAccountExt; use miden_standards::testing::note::NoteBuilder; @@ -116,24 +116,20 @@ async fn test_transaction_epilogue() -> anyhow::Result<()> { .to_commitment(); let account_update_commitment = - miden_protocol::Hasher::merge(&[final_account.to_commitment(), account_delta_commitment]); + Hasher::merge(&[final_account.to_commitment(), account_delta_commitment]); + let fee_asset = FungibleAsset::new( + tx_context.tx_inputs().block_header().fee_parameters().native_asset_id(), + 0, + )?; let mut expected_stack = Vec::with_capacity(16); expected_stack.extend(output_notes.commitment().as_elements().iter().rev()); expected_stack.extend(account_update_commitment.as_elements().iter().rev()); - expected_stack.extend( - Word::from( - FungibleAsset::new( - tx_context.tx_inputs().block_header().fee_parameters().native_asset_id(), - 0, - ) - .unwrap(), - ) - .iter() - .rev(), - ); + expected_stack.push(fee_asset.faucet_id().prefix().as_felt()); + expected_stack.push(fee_asset.faucet_id().suffix()); + expected_stack.push(Felt::try_from(fee_asset.amount()).unwrap()); expected_stack.push(Felt::from(u32::MAX)); // Value for tx expiration block number - expected_stack.extend((13..16).map(|_| ZERO)); + expected_stack.resize(16, ZERO); assert_eq!( exec_output.stack.as_slice(), 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 805627f364..6dc95431ea 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -1,12 +1,11 @@ use alloc::sync::Arc; -use miden_protocol::Word; use miden_protocol::account::{Account, AccountBuilder, AccountComponent, AccountId, AccountType}; use miden_protocol::assembly::DefaultSourceManager; use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; use miden_protocol::errors::tx_kernel::{ + ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, - ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS, ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, ERR_VAULT_FUNGIBLE_ASSET_AMOUNT_LESS_THAN_AMOUNT_TO_WITHDRAW, ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND, @@ -97,13 +96,13 @@ async fn mint_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result<()> use mock::faucet begin - push.{asset_key} - push.{asset_value} + push.{ASSET_VALUE} + push.{ASSET_KEY} call.faucet::mint end ", - asset_key = asset.vault_key(), - asset_value = Word::from(asset), + ASSET_KEY = asset.to_key_word(), + ASSET_VALUE = asset.to_value_word(), ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; @@ -131,13 +130,13 @@ async fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> begin exec.prologue::prepare_transaction - push.{asset_key} - push.{asset_value} + push.{ASSET_VALUE} + push.{ASSET_KEY} call.faucet::mint end ", - asset_key = asset.vault_key(), - asset_value = Word::from(asset), + ASSET_KEY = asset.to_key_word(), + ASSET_VALUE = asset.to_value_word(), ); let exec_output = tx_context.execute_code(&code).await; @@ -157,21 +156,18 @@ async fn test_mint_fungible_asset_fails_when_amount_exceeds_max_representable_am begin push.{max_amount_plus_1} push.0 - push.{faucet_id_suffix} - push.{faucet_id_prefix} + push.0 + push.0 # => [ASSET_VALUE] - push.0.0 - push.{faucet_id_suffix} - push.{faucet_id_prefix} + push.{ASSET_KEY} # => [ASSET_KEY, ASSET_VALUE] call.faucet::mint dropw dropw end ", - faucet_id_prefix = FungibleAsset::mock_issuer().prefix().as_felt(), - faucet_id_suffix = FungibleAsset::mock_issuer().suffix(), + ASSET_KEY = FungibleAsset::mock(0).to_key_word(), max_amount_plus_1 = FungibleAsset::MAX_AMOUNT + 1, ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; @@ -183,10 +179,7 @@ async fn test_mint_fungible_asset_fails_when_amount_exceeds_max_representable_am .execute() .await; - assert_transaction_executor_error!( - result, - ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS - ); + assert_transaction_executor_error!(result, ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT); Ok(()) } @@ -261,7 +254,7 @@ async fn test_mint_non_fungible_asset_fails_inconsistent_faucet_id() -> anyhow:: end ", asset_key = non_fungible_asset.to_key_word(), - asset_value = Word::from(non_fungible_asset), + asset_value = non_fungible_asset.to_value_word(), ); let exec_output = tx_context.execute_code(&code).await; @@ -281,13 +274,13 @@ async fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result use mock::faucet begin - push.{asset_key} - push.{asset_value} + push.{ASSET_VALUE} + push.{ASSET_KEY} call.faucet::mint end ", - asset_key = asset.vault_key(), - asset_value = Word::from(asset), + ASSET_KEY = asset.to_key_word(), + ASSET_VALUE = asset.to_value_word(), ); let tx_script = CodeBuilder::with_mock_libraries().compile_tx_script(code)?; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs b/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs index b6ee630f85..f37411dfec 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fpi.rs @@ -740,7 +740,7 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu // Create two different assets. let fungible_asset = Asset::Fungible(FungibleAsset::new(fungible_faucet_id, 1)?); let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new( - &NonFungibleAssetDetails::new(non_fungible_faucet_id.prefix(), vec![1, 2, 3])?, + &NonFungibleAssetDetails::new(non_fungible_faucet_id, vec![1, 2, 3])?, )?); let foreign_account_code_source = format!( @@ -754,7 +754,7 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu # => [balance] # check presence of non fungible asset - push.{non_fungible_asset_word} + push.{NON_FUNGIBLE_ASSET_KEY} exec.active_account::has_non_fungible_asset # => [has_asset, balance] @@ -769,7 +769,7 @@ async fn foreign_account_can_get_balance_and_presence_of_asset() -> anyhow::Resu ", fungible_faucet_id_prefix = fungible_faucet_id.prefix().as_felt(), fungible_faucet_id_suffix = fungible_faucet_id.suffix(), - non_fungible_asset_word = Word::from(non_fungible_asset), + NON_FUNGIBLE_ASSET_KEY = non_fungible_asset.to_key_word(), ); let source_manager = Arc::new(DefaultSourceManager::default()); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs b/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs index 216dc54c17..30e564e4fb 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_input_note.rs @@ -259,8 +259,8 @@ async fn test_get_assets() -> anyhow::Result<()> { add.{ASSET_SIZE} # => [dest_ptr+ASSET_SIZE, note_index] "#, - NOTE_ASSET_KEY = asset.vault_key().as_word(), - NOTE_ASSET_VALUE = Word::from(*asset), + NOTE_ASSET_KEY = asset.to_key_word(), + NOTE_ASSET_VALUE = asset.to_value_word(), asset_index = asset_index, note_index = note_index, )); diff --git a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs index 5bae88dca0..1c03572f6a 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -521,8 +521,8 @@ fn input_notes_memory_assertions( ); for (asset, asset_idx) in note.assets().iter().cloned().zip(0_u32..) { - let asset_key: Word = *asset.vault_key().as_word(); - let asset_value: Word = asset.into(); + let asset_key = asset.to_key_word(); + let asset_value = asset.to_value_word(); let asset_key_addr = INPUT_NOTE_ASSETS_OFFSET + asset_idx * ASSET_SIZE; let asset_value_addr = asset_key_addr + ASSET_VALUE_OFFSET; diff --git a/crates/miden-testing/src/tx_context/context.rs b/crates/miden-testing/src/tx_context/context.rs index 7e5922b5a8..c51321ac86 100644 --- a/crates/miden-testing/src/tx_context/context.rs +++ b/crates/miden-testing/src/tx_context/context.rs @@ -90,7 +90,7 @@ impl TransactionContext { .iter() .flat_map(|note| note.note().assets().iter().map(Asset::vault_key)) .collect::>(); - let fee_asset_vault_key = AssetVaultKey::from_account_id( + let fee_asset_vault_key = AssetVaultKey::new_fungible( self.tx_inputs().block_header().fee_parameters().native_asset_id(), ) .expect("fee asset should be a fungible asset"); @@ -107,7 +107,7 @@ impl TransactionContext { // Add the vault key for the fee asset to the list of asset vault keys which may need to be // accessed at the end of the transaction. let fee_asset_vault_key = - AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id()) + AssetVaultKey::new_fungible(block_header.fee_parameters().native_asset_id()) .expect("fee asset should be a fungible asset"); asset_vault_keys.insert(fee_asset_vault_key); diff --git a/crates/miden-tx/src/executor/mod.rs b/crates/miden-tx/src/executor/mod.rs index 1dc2378404..92d40bafbf 100644 --- a/crates/miden-tx/src/executor/mod.rs +++ b/crates/miden-tx/src/executor/mod.rs @@ -274,7 +274,7 @@ where let native_account_vault_root = account.vault().root(); let fee_asset_vault_key = - AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id()) + AssetVaultKey::new_fungible(block_header.fee_parameters().native_asset_id()) .expect("fee asset should be a fungible asset"); let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes) @@ -339,7 +339,7 @@ where let initial_fee_asset_balance = { let vault_root = tx_inputs.account().vault().root(); let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id(); - let fee_asset_vault_key = AssetVaultKey::from_account_id(native_asset_id) + let fee_asset_vault_key = AssetVaultKey::new_fungible(native_asset_id) .expect("fee asset should be a fungible asset"); let fee_asset = tx_inputs diff --git a/crates/miden-tx/src/host/tx_event.rs b/crates/miden-tx/src/host/tx_event.rs index fc1708205f..3c134d1dc5 100644 --- a/crates/miden-tx/src/host/tx_event.rs +++ b/crates/miden-tx/src/host/tx_event.rs @@ -204,7 +204,12 @@ impl TransactionEvent { let vault_root_ptr = process.get_stack_item(9); let asset_vault_key = - AssetVaultKey::try_from(asset_vault_key).expect("TODO(expand_assets)"); + AssetVaultKey::try_from(asset_vault_key).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "AccountVaultBefore{Add,Remove}Asset", + source, + } + })?; let current_vault_root = process.get_vault_root(vault_root_ptr)?; on_account_vault_asset_accessed( @@ -216,24 +221,31 @@ impl TransactionEvent { }, TransactionEventId::AccountVaultAfterRemoveAsset => { // Expected stack state: [event, ASSET_KEY, ASSET_VALUE] - let asset: Asset = process.get_stack_word_be(5).try_into().map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_account_vault_after_remove_asset", - source, - } - })?; + let asset_key = process.get_stack_word_be(1); + let asset_value = process.get_stack_word_be(5); + + let asset = + Asset::from_key_value_words(asset_key, asset_value).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "AccountVaultAfterRemoveAsset", + source, + } + })?; Some(TransactionEvent::AccountVaultAfterRemoveAsset { asset }) }, TransactionEventId::AccountVaultAfterAddAsset => { // Expected stack state: [event, ASSET_KEY, ASSET_VALUE] + let asset_key = process.get_stack_word_be(1); + let asset_value = process.get_stack_word_be(5); - let asset: Asset = process.get_stack_word_be(5).try_into().map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_account_vault_after_add_asset", - source, - } - })?; + let asset = + Asset::from_key_value_words(asset_key, asset_value).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "AccountVaultAfterAddAsset", + source, + } + })?; Some(TransactionEvent::AccountVaultAfterAddAsset { asset }) }, @@ -243,7 +255,12 @@ impl TransactionEvent { let asset_key = process.get_stack_word_be(1); let vault_root_ptr = process.get_stack_item(5); - let asset_key = AssetVaultKey::try_from(asset_key).expect("TODO(expand_assets)"); + let asset_key = AssetVaultKey::try_from(asset_key).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "AccountVaultBeforeGetAsset", + source, + } + })?; let vault_root = process.get_vault_root(vault_root_ptr)?; on_account_vault_asset_accessed(base_host, process, asset_key, vault_root)? @@ -387,15 +404,17 @@ impl TransactionEvent { TransactionEventId::NoteBeforeAddAsset => { // Expected stack state: [event, ASSET_KEY, ASSET_VALUE, note_ptr] + let asset_key = process.get_stack_word_be(1); let asset_value = process.get_stack_word_be(5); let note_ptr = process.get_stack_item(9); - let asset = Asset::try_from(asset_value).map_err(|source| { - TransactionKernelError::MalformedAssetInEventHandler { - handler: "on_note_before_add_asset", - source, - } - })?; + let asset = + Asset::from_key_value_words(asset_key, asset_value).map_err(|source| { + TransactionKernelError::MalformedAssetInEventHandler { + handler: "NoteBeforeAddAsset", + source, + } + })?; let note_idx = note_ptr_to_idx(note_ptr)? as usize; Some(TransactionEvent::NoteBeforeAddAsset { note_idx, asset }) @@ -451,10 +470,10 @@ impl TransactionEvent { TransactionEventId::EpilogueBeforeTxFeeRemovedFromAccount => { // Expected stack state: [event, FEE_ASSET_KEY, FEE_ASSET_VALUE] - + let fee_asset_key = process.get_stack_word_be(1); let fee_asset_value = process.get_stack_word_be(5); - let fee_asset = FungibleAsset::try_from(fee_asset_value) + let fee_asset = FungibleAsset::from_key_value_words(fee_asset_key, fee_asset_value) .map_err(TransactionKernelError::FailedToConvertFeeAsset)?; Some(TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount { fee_asset }) diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index 6d4d0c46c6..f60b5a51d3 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -162,4 +162,4 @@ 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_prefix, faucet_id_suffix, 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_prefix, DATA_HASH]`
**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_prefix, faucet_id_suffix, DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any |