diff --git a/CHANGELOG.md b/CHANGELOG.md index d021bb34c4..d71160e337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,26 +10,27 @@ ### Changes -- Documented that standard note scripts claim only the assets remaining in a note at consumption time ([#3650](https://github.com/0xMiden/protocol/pull/3650)). -- Moved the transaction kernel API procedures into the kernel's `api` submodule, leaving `exec_kernel_proc` as the only `syscall`-invocable kernel procedure ([#3646](https://github.com/0xMiden/protocol/pull/3646)). -- [BREAKING] Added the `miden::standards::expiration` MASM module with `apply_default` and used it to apply a default 20-block transaction expiration limit to the standard allowlist and blocklist transfer policies and the fee manager's `estimate_note_fee` procedure ([#3512](https://github.com/0xMiden/protocol/pull/3512)). -- [BREAKING] Moved the kernel data section of the transaction kernel memory to address `0`, so that `exec_kernel_proc` becomes reusable across multiple kernels ([#3655](https://github.com/0xMiden/protocol/pull/3655)). +- [BREAKING] Refactored `AccountVaultDelta` to track generic assets. `FungibleAssetDelta`, `NonFungibleAssetDelta` and `NonFungibleDeltaAction` were removed ([3485](https://github.com/0xMiden/protocol/pull/3485)). - [BREAKING] Moved the internal shared helpers of `miden::protocol::input_note`, `miden::protocol::active_note`, and the note memory-write helpers into private `input_note_internal` and `note_internal` modules ([#3501](https://github.com/0xMiden/protocol/pull/3501)). -- [BREAKING] Sorted the procedures of `AccountCode` after the authentication procedure at index 0, making the account code commitment independent of the order in which components are provided ([#2961](https://github.com/0xMiden/protocol/issues/2961)). +- [BREAKING] Added the `miden::standards::expiration` MASM module with `apply_default` and used it to apply a default 20-block transaction expiration limit to the standard allowlist and blocklist transfer policies and the fee manager's `estimate_note_fee` procedure ([#3512](https://github.com/0xMiden/protocol/pull/3512)). - The transaction kernel now validates that a new account's procedures are sorted and unique ([#3567](https://github.com/0xMiden/protocol/pull/3567)). - [BREAKING] Changed asset callbacks into validation-only interfaces that return no asset value; the transaction kernel retains and uses the original value, preventing callbacks from modifying it. The kernel commitment changes ([#3505](https://github.com/0xMiden/protocol/issues/3505), [#3513](https://github.com/0xMiden/protocol/pull/3513)). - [BREAKING] Extracted the shared `MastForestScript` type and `MastForestScriptError` backing `NoteScript` / `TransactionScript`, moving `TransactionScript` into `transaction::script` ([#3516](https://github.com/0xMiden/protocol/pull/3516)). - Documented the RBAC freeze-only actor pattern on `Authority` and added test coverage pinning that a `FREEZER` can trip the emergency switch but can never unfreeze the account ([#3520](https://github.com/0xMiden/protocol/pull/3520)). - [BREAKING] `NoteScript::from_parts` and `TransactionScript::from_parts` now return a `Result` instead of panicking when the specified entrypoint is not in the provided MAST forest ([#3548](https://github.com/0xMiden/protocol/pull/3548)). +- [BREAKING] Sorted the procedures of `AccountCode` after the authentication procedure at index 0, making the account code commitment independent of the order in which components are provided ([#2961](https://github.com/0xMiden/protocol/pull/3565)). - [BREAKING] Renamed the fungible asset amount extraction procedures so the unsuffixed name is the validating one ([#3576](https://github.com/0xMiden/protocol/pull/3576)): - `miden::protocol::asset::fungible_value_into_amount` -> `fungible_value_into_amount_unchecked`. - `miden::standards::assets::fungible_asset::value_into_amount` to `value_into_amount_unchecked`. - `to_amount` to `to_amount_unchecked`. - `try_value_to_amount` to `value_into_amount`. - [BREAKING] AggLayer bridge and faucet account builders now take a concrete `BasicConstantFeePolicy` and fee faucet ID, constructing their `FeePolicyManager` internally ([#3583](https://github.com/0xMiden/protocol/pull/3583)). -- [BREAKING] Refactored `AccountVaultDelta` to track generic assets. `FungibleAssetDelta`, `NonFungibleAssetDelta` and `NonFungibleDeltaAction` were removed ([3485](https://github.com/0xMiden/protocol/pull/3485)). - [BREAKING] The transaction kernel no longer requires assets with `AssetComposition::None` to have the non-fungible asset layout ([#3624](https://github.com/0xMiden/protocol/pull/3624)). - [BREAKING] Refactored `Asset` into a struct holding `AssetId` and `AssetValue` ([#3625](https://github.com/0xMiden/protocol/pull/3625)). +- Moved the transaction kernel API procedures into the kernel's `api` submodule, leaving `exec_kernel_proc` as the only `syscall`-invocable kernel procedure ([#3646](https://github.com/0xMiden/protocol/pull/3646)). +- Documented that standard note scripts claim only the assets remaining in a note at consumption time ([#3650](https://github.com/0xMiden/protocol/pull/3650)). +- [BREAKING] Moved the kernel data section of the transaction kernel memory to address `0`, so that `exec_kernel_proc` becomes reusable across multiple kernels ([#3655](https://github.com/0xMiden/protocol/pull/3655)). +- [BREAKING] Added a 4-bit version to the lowest bits of the asset ID's metadata byte, moving the asset composition to bits 4-5 ([#3670](https://github.com/0xMiden/protocol/pull/3670)). ### Fixes diff --git a/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm b/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm index 00c475193d..0565d2b7b4 100644 --- a/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction-core/src/account.masm @@ -13,6 +13,9 @@ use {ACCOUNT_PROCEDURE_DATA_LENGTH, EMPTY_SMT_ROOT, STORAGE_SLOT_TYPE_MAP, STORA use {ACCOUNT_DATA_LENGTH, ACCT_ID_PREFIX_OFFSET, ACCT_ID_SUFFIX_OFFSET, MAX_FOREIGN_ACCOUNT_PTR, NATIVE_ACCOUNT_DATA_PTR} from miden::tx_kernel_core::memory +# Re-exported for testing purposes. +pub use {validate as validate_id} from miden::protocol_utils::account_id + # ERRORS # ================================================================================================= diff --git a/crates/miden-protocol/asm/protocol/src/account_id.masm b/crates/miden-protocol/asm/protocol/src/account_id.masm index b9dce57b0c..2b844dc24b 100644 --- a/crates/miden-protocol/asm/protocol/src/account_id.masm +++ b/crates/miden-protocol/asm/protocol/src/account_id.masm @@ -4,5 +4,5 @@ # Re-exported from the `miden::protocol_utils` library, where the implementation lives. This keeps # the procedures available under the public `miden::protocol::account_id` path. -pub use {eq, eqz, shape_suffix, testz, validate, validate_structure} +pub use {eq, eqz, shape_suffix, testz, validate_structure} from miden::protocol_utils::account_id diff --git a/crates/miden-protocol/asm/protocol_utils/src/asset.masm b/crates/miden-protocol/asm/protocol_utils/src/asset.masm index 1c8af793d7..6f0c5b3054 100644 --- a/crates/miden-protocol/asm/protocol_utils/src/asset.masm +++ b/crates/miden-protocol/asm/protocol_utils/src/asset.masm @@ -3,6 +3,8 @@ const ERR_VAULT_ASSET_METADATA_NOT_U32 = "asset metadata is not a u32" +const ERR_VAULT_ASSET_METADATA_UNKNOWN_VERSION = "unknown asset ID version" + const ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS = "reserved asset metadata bits are non-zero" const ERR_VAULT_ASSET_METADATA_UNKNOWN_COMPOSITION = "unknown asset metadata composition value" @@ -21,8 +23,17 @@ pub const ASSET_SIZE = 8 # The offset of the asset value in an asset stored in memory. pub const ASSET_VALUE_MEMORY_OFFSET = 4 +# Version 1 of the asset ID encoding. +const ASSET_VERSION_1 = 1 + +# The mask for the version bits in the asset metadata. +const VERSION_MASK = 0x0f # 0b1111 + +# The number of bits by which the composition is shifted in the asset metadata. +const COMPOSITION_SHIFT = 4 + #! The mask for the composition bits in the asset metadata. -const COMPOSITION_MASK = 3 # 0b11 +const COMPOSITION_MASK = 0x30 # 0b0011_0000 # The flag representing the AssetComposition::None composition. pub const COMPOSITION_NONE = 0 @@ -37,7 +48,7 @@ pub const COMPOSITION_CUSTOM = 2 const COMPOSITION_INVALID = 3 #! The u32 mask for the reserved bits in the asset metadata. -const METADATA_RESERVED_MASK = 0xfffffffc # lower 8 bits: 0b1111_1100 +const METADATA_RESERVED_MASK = 0xffffffc0 # lower 8 bits: 0b1100_0000 # PROCEDURES # ================================================================================================= @@ -182,12 +193,16 @@ end #! Validates that asset metadata is well formed and consumes it. #! +#! WARNING: This procedure should not be exposed to or called from user code (e.g. miden::protocol +#! or miden::standards) as this would make the calling code only accept asset version 1. +#! #! Inputs: [asset_metadata] #! Outputs: [] #! #! Panics if: #! - asset_metadata is not a valid u32 -#! - has reserved bits 3-7 set. +#! - encodes an unknown asset ID version. +#! - has reserved bits 6 or 7 set. #! - encodes an unknown asset composition. pub proc validate_metadata # assert that the metadata fits in a u8 @@ -196,7 +211,12 @@ pub proc validate_metadata eq.0 assert.err=ERR_VAULT_ASSET_METADATA_NOT_U32 # => [asset_metadata] - # assert the reserved bits are all zero (bits 3..32) + # the version defines how the rest of the metadata is decoded, so assert it first + dup u32and.VERSION_MASK + eq.ASSET_VERSION_1 assert.err=ERR_VAULT_ASSET_METADATA_UNKNOWN_VERSION + # => [asset_metadata] + + # assert the reserved bits are all zero (bits 6..32) dup u32and.METADATA_RESERVED_MASK eq.0 assert.err=ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS # => [asset_metadata] @@ -210,23 +230,6 @@ pub proc validate_metadata # => [] end -#! Creates asset metadata from the provided composition. -#! -#! Inputs: [asset_composition] -#! Outputs: [asset_metadata] -#! -#! Where: -#! - asset_composition is the composition value (see COMPOSITION_* constants). -#! - asset_metadata is the asset metadata. -#! -#! Panics if: -#! - the resulting metadata byte has invalid reserved bits set. -proc create_metadata - # the asset metadata is currently just the composition - dup exec.validate_metadata - # => [asset_metadata] -end - #! Extracts the asset composition from asset metadata. #! #! WARNING: asset_metadata is assumed to be a byte (in particular a valid u32) @@ -239,7 +242,7 @@ end #! - asset_composition is the composition value (see COMPOSITION_* constants). proc metadata_into_composition # extract composition bits from the metadata - u32and.COMPOSITION_MASK + u32and.COMPOSITION_MASK u32shr.COMPOSITION_SHIFT # => [asset_composition] end diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index b934873f2d..1c4a8888cb 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -257,8 +257,10 @@ mod tests { #[test] fn fungible_asset_from_id_and_value_words_fails_on_invalid_composition() -> anyhow::Result<()> { - let asset_id = - set_asset_metadata(FungibleAsset::mock(25).id(), AssetComposition::None.as_u8()); + let asset_id = set_asset_metadata( + FungibleAsset::mock(25).id(), + AssetId::encode_metadata(AssetComposition::None), + ); let err = FungibleAsset::from_id_and_value_words( asset_id, diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 499361546b..f396db6761 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -74,7 +74,8 @@ pub use vault::{AssetClass, AssetId, AssetIdHash, AssetVault, AssetWitness, Part /// - the remaining elements in the value word must be zero. /// - `faucet_id_prefix` is the prefix of the faucet ID which issues the asset. /// - `faucet_id_suffix_and_metadata` is the suffix of the faucet ID which issues the asset and the -/// asset metadata ([`AssetComposition`]). See [`AssetId`] for more details on the ID's layout. +/// asset metadata, which is the encoding version together with the [`AssetComposition`]. See +/// [`AssetId`] for more details on the ID's layout. /// - the asset class limbs must be zero, which means two instances of the same fungible asset have /// the same asset ID and will be merged together when stored in the same account's vault. #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/crates/miden-protocol/src/asset/vault/asset_id.rs b/crates/miden-protocol/src/asset/vault/asset_id.rs index 3bcf5000ee..df5605e61c 100644 --- a/crates/miden-protocol/src/asset/vault/asset_id.rs +++ b/crates/miden-protocol/src/asset/vault/asset_id.rs @@ -26,14 +26,14 @@ use crate::{Felt, Hasher, Word}; /// [ /// asset_class_suffix (64 bits), /// asset_class_prefix (64 bits), -/// [faucet_id_suffix (56 bits) | reserved (6 bits) | composition (2 bits)], +/// [faucet_id_suffix (56 bits) | reserved (2 bits) | composition (2 bits) | version (4 bits)], /// faucet_id_prefix (64 bits) /// ] /// ``` /// -/// The composition is the discriminator between assets and so it is placed at a static offset much -/// like the version in an account ID. This makes it slightly easier to change the asset metadata in -/// the future without affecting identification of previous assets. +/// The version determines how the remainder of the asset is decoded and so it is placed at a +/// static offset so it can be read first independent of the version. Version 0 is invalid, which +/// guarantees that an empty word is not a valid asset ID. /// /// Use [`AssetId::hash`] to produce the corresponding [`AssetIdHash`] that is used as /// the key in the asset vault's underlying SMT. Hashing ensures a uniform distribution across @@ -67,13 +67,17 @@ impl AssetId { /// The metadata byte occupies the lower 8 bits of the third element of the asset ID word. pub(in crate::asset) const METADATA_BYTE_MASK: u8 = 0xff; - /// Bits 0-1 of the metadata byte encode the [`AssetComposition`]. The composition occupies - /// the lowest bits so its position remains stable as new metadata bits are added, since it - /// identifies the asset's type. - pub(in crate::asset) const COMPOSITION_MASK: u8 = 0b11; + /// Version 1 of the asset ID encoding. + pub(in crate::asset) const VERSION_1: u8 = 1; - /// Bits 2-7 of the metadata byte are reserved and must be zero. - pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1111_1100; + /// Bits 0-3 of the metadata byte encode the version. + pub(in crate::asset) const VERSION_MASK: u8 = 0b1111; + + /// Bits 4-5 of the metadata byte encode the [`AssetComposition`]. + pub(in crate::asset) const COMPOSITION_SHIFT: u8 = 4; + + /// Bits 6-7 of the metadata byte are reserved and must be zero. + pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1100_0000; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -124,7 +128,7 @@ impl AssetId { faucet_suffix & Self::METADATA_BYTE_MASK as u64 == 0, "lower 8 bits of faucet suffix must be zero", ); - let metadata_byte = self.composition.as_u8(); + let metadata_byte = Self::encode_metadata(self.composition); let faucet_id_suffix_and_metadata = faucet_suffix | metadata_byte as u64; let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata) .expect("highest bit should still be zero resulting in a valid felt"); @@ -163,6 +167,14 @@ impl AssetId { pub fn hash(&self) -> AssetIdHash { AssetIdHash::from_raw(Hasher::hash_elements(self.to_word().as_elements())) } + + // HELPERS + // -------------------------------------------------------------------------------------------- + + /// Encodes the given composition into a metadata byte of the current version. + pub(in crate::asset) fn encode_metadata(composition: AssetComposition) -> u8 { + (composition.as_u8() << Self::COMPOSITION_SHIFT) | Self::VERSION_1 + } } // ASSET ID HASH @@ -224,10 +236,11 @@ impl TryFrom for AssetId { /// # Errors /// /// Returns an error if: - /// - the asset class limbs are not zero when asset composition is - /// [`AssetComposition::Fungible`]. + /// - the version encoded in the metadata byte is unknown. /// - the metadata byte has reserved bits set. /// - the composition encoded in the metadata byte is invalid. + /// - the asset class limbs are not zero when asset composition is + /// [`AssetComposition::Fungible`]. fn try_from(id: Word) -> Result { let asset_class_suffix = id[0]; let asset_class_prefix = id[1]; @@ -237,12 +250,18 @@ impl TryFrom for AssetId { let raw = faucet_id_suffix_and_metadata.as_canonical_u64(); let metadata_byte = (raw & Self::METADATA_BYTE_MASK as u64) as u8; + // The version defines how the rest of the metadata is decoded, so check it first. + let version = metadata_byte & Self::VERSION_MASK; + if version != Self::VERSION_1 { + return Err(AssetError::UnknownAssetIdVersion(version)); + } + // Make sure the reserved bits of the metadata are zero. if metadata_byte & Self::METADATA_RESERVED_MASK != 0 { return Err(AssetError::ReservedAssetMetadata(metadata_byte)); } - let composition = AssetComposition::try_from(metadata_byte & Self::COMPOSITION_MASK)?; + let composition = AssetComposition::try_from(metadata_byte >> Self::COMPOSITION_SHIFT)?; let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64)) .expect("clearing lower bits should not produce an invalid felt"); @@ -325,8 +344,6 @@ impl Deserializable for AssetId { #[cfg(test)] mod tests { - use assert_matches::assert_matches; - use super::*; use crate::asset::AssetComposition; use crate::asset::tests::{asset_metadata, set_asset_metadata}; @@ -365,28 +382,43 @@ mod tests { Ok(()) } - #[test] - fn decoding_word_with_reserved_bits_set_fails() -> anyhow::Result<()> { - let id = FungibleAsset::mock(42).id(); - let valid_metadata = asset_metadata(id); - // Set the reserved bits so the reserved-bits check fires. - let word = set_asset_metadata(id, valid_metadata | AssetId::METADATA_RESERVED_MASK); + /// Version 0 is never valid, so the all-zero word cannot decode into an asset ID. + #[rstest::rstest] + #[case::version_zero(0, AssetError::UnknownAssetIdVersion(0))] + #[case::unknown_version(AssetId::VERSION_1 + 1, AssetError::UnknownAssetIdVersion(2))] + #[case::reserved_bits_set( + AssetId::encode_metadata(AssetComposition::Fungible) | AssetId::METADATA_RESERVED_MASK, + AssetError::ReservedAssetMetadata(0b1101_0001) + )] + // Composition value 3 is the unused bit pattern within the 2-bit field. + #[case::unknown_composition( + 0b0011_0000 | AssetId::VERSION_1, + AssetError::UnknownAssetComposition(0b11) + )] + fn decoding_word_with_invalid_metadata_fails( + #[case] metadata: u8, + #[case] expected_err: AssetError, + ) -> anyhow::Result<()> { + let word = set_asset_metadata(FungibleAsset::mock(42).id(), metadata); let err = AssetId::try_from(word).unwrap_err(); - assert_matches!(err, AssetError::ReservedAssetMetadata(_)); + assert_eq!(err.to_string(), expected_err.to_string()); Ok(()) } #[test] - fn decoding_word_with_invalid_composition_value_fails() -> anyhow::Result<()> { - let id = FungibleAsset::mock(42).id(); - // Set all composition bits — value 3 is the invalid bit pattern within the 2-bit field. - let invalid_metadata = AssetId::COMPOSITION_MASK; - let word = set_asset_metadata(id, invalid_metadata); + fn metadata_encodes_version_and_composition() -> anyhow::Result<()> { + let fungible = + AssetId::new_fungible(AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?); + assert_eq!(asset_metadata(fungible), 0b0001_0001); - let err = AssetId::try_from(word).unwrap_err(); - assert_matches!(err, AssetError::UnknownAssetComposition(_)); + let non_fungible = AssetId::new( + AssetClass::new(Felt::from(42u32), Felt::from(99u32)), + AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?, + AssetComposition::None, + )?; + assert_eq!(asset_metadata(non_fungible), 0b0000_0001); Ok(()) } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 750431ec4a..b7fe519d25 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -605,6 +605,8 @@ pub enum AssetError { }, #[error("asset metadata byte 0x{0:02x} has reserved bits set to non-zero values")] ReservedAssetMetadata(u8), + #[error("unknown asset ID version: {0}")] + UnknownAssetIdVersion(u8), } // TOKEN SYMBOL ERROR diff --git a/crates/miden-standards/asm/standards/assets/fungible_asset.masm b/crates/miden-standards/asm/standards/assets/fungible_asset.masm index b0b556a5d4..cbdeee48fb 100644 --- a/crates/miden-standards/asm/standards/assets/fungible_asset.masm +++ b/crates/miden-standards/asm/standards/assets/fungible_asset.masm @@ -22,6 +22,12 @@ const ERR_FUNGIBLE_ASSET_ID_COMPOSITION_MUST_BE_FUNGIBLE = "fungible asset ID's const ERR_FUNGIBLE_ASSET_ID_ASSET_CLASS_MUST_BE_ZERO = "fungible asset ID asset class prefix and suffix must be zero" +# CONSTANTS +# ================================================================================================ + +#! Encodes the asset metadata with composition fungible and asset version 1. +const ASSET_METADATA_FUNGIBLE = 0x11 + # PROCEDURES # ================================================================================================ @@ -36,13 +42,8 @@ const ERR_FUNGIBLE_ASSET_ID_ASSET_CLASS_MUST_BE_ZERO = "fungible asset ID asset #! #! Invocation: exec pub proc create_id - # push the fungible composition for create_metadata - # this is equivalent to the asset metadata - push.COMPOSITION_FUNGIBLE - # => [asset_metadata, faucet_id_suffix, faucet_id_prefix] - - # merge the asset metadata into the lower 8 bits of the suffix - add + # merge the fungible asset metadata into the lower 8 bits of the suffix + add.ASSET_METADATA_FUNGIBLE # => [faucet_id_suffix_and_metadata, faucet_id_prefix] push.0.0 diff --git a/crates/miden-standards/asm/standards/assets/non_fungible_asset.masm b/crates/miden-standards/asm/standards/assets/non_fungible_asset.masm index 56c9a446ab..077e169eda 100644 --- a/crates/miden-standards/asm/standards/assets/non_fungible_asset.masm +++ b/crates/miden-standards/asm/standards/assets/non_fungible_asset.masm @@ -14,6 +14,12 @@ const ERR_NON_FUNGIBLE_ASSET_CLASS_SUFFIX_MUST_MATCH_HASH0 = "the asset class su const ERR_NON_FUNGIBLE_ASSET_CLASS_PREFIX_MUST_MATCH_HASH1 = "the asset class prefix in a non-fungible asset ID must match hash1 of the asset value" +# CONSTANTS +# ================================================================================================ + +#! Encodes the asset metadata with composition none and asset version 1. +const ASSET_METADATA_NONE = 0x01 + # PROCEDURES # ================================================================================================ @@ -33,13 +39,8 @@ const ERR_NON_FUNGIBLE_ASSET_CLASS_PREFIX_MUST_MATCH_HASH1 = "the asset class pr #! #! Invocation: exec pub proc create - # push the non-fungible (None) composition - # this is equivalent to the asset metadata - push.COMPOSITION_NONE - # => [asset_metadata, faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] - - # merge the asset metadata into the lower 8 bits of the suffix - add + # merge the non-fungible asset metadata into the lower 8 bits of the suffix + add.ASSET_METADATA_NONE # => [faucet_id_suffix_and_metadata, faucet_id_prefix, ASSET_VALUE] # copy hashes at indices 0 and 1 in the data hash word to the corresponding index in the key 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 53ecae8a05..90f2c50c5f 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -219,10 +219,10 @@ async fn test_account_validate_id() -> anyhow::Result<()> { let (prefix, suffix) = account_id_felts(account_id)?; let code = " - use miden::protocol::account_id + use miden::tx_kernel_core::account begin - exec.account_id::validate + exec.account::validate_id end "; 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 60716f7815..3856200aff 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -8,7 +8,10 @@ use miden_protocol::asset::{ NonFungibleAssetDetails, }; use miden_protocol::errors::MasmError; -use miden_protocol::errors::protocol::ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS; +use miden_protocol::errors::protocol::{ + ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS, + ERR_VAULT_ASSET_METADATA_UNKNOWN_VERSION, +}; use miden_protocol::errors::tx_kernel::{ ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, ERR_FUNGIBLE_ASSET_ID_ASSET_CLASS_MUST_BE_ZERO, @@ -108,8 +111,16 @@ async fn test_create_non_fungible_asset_succeeds() -> anyhow::Result<()> { Ok(()) } -const METADATA_BYTE_NONE: u64 = AssetComposition::None as u64; -const METADATA_BYTE_FUNGIBLE: u64 = AssetComposition::Fungible as u64; +/// The only asset ID version the kernel accepts. +const ASSET_VERSION_1: u64 = 1; + +/// Encodes the given composition into a metadata byte of version 1. +const fn metadata_byte(composition: u64) -> u64 { + (composition << 4) | ASSET_VERSION_1 +} + +const METADATA_BYTE_NONE: u64 = metadata_byte(AssetComposition::None as u64); +const METADATA_BYTE_FUNGIBLE: u64 = metadata_byte(AssetComposition::Fungible as u64); /// Returns the third element of a synthesised asset ID, packing the faucet ID suffix with the /// given metadata byte (lower 8 bits). @@ -223,7 +234,7 @@ async fn test_validate_non_fungible_asset_standards_succeeds() -> anyhow::Result #[rstest::rstest] #[case::asset_class_is_not_derived_from_value(METADATA_BYTE_NONE, None)] #[case::metadata_reserved_bits_are_set( - METADATA_BYTE_NONE | 0b100, + METADATA_BYTE_NONE | 0b0100_0000, Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS) )] #[tokio::test] @@ -351,22 +362,38 @@ async fn test_validate_fungible_asset( } #[rstest::rstest] -// Valid: composition=None, callbacks=disabled. -#[case::valid_none(0, None)] -// Valid: composition=Fungible, callbacks=disabled. +// Valid: composition=None. +#[case::valid_none(METADATA_BYTE_NONE, None)] +// Valid: composition=Fungible. #[case::valid_fungible(METADATA_BYTE_FUNGIBLE, None)] // Valid: composition=Custom. -#[case::valid_custom(AssetComposition::Custom as u64, None)] +#[case::valid_custom(metadata_byte(AssetComposition::Custom as u64), None)] // Metadata is not a valid u32 (does not fit in 32 bits). #[case::not_u32(u32::MAX as u64 + 1, Some(ERR_VAULT_ASSET_METADATA_NOT_U32))] +// Version 0 is never valid. +#[case::version_zero(0, Some(ERR_VAULT_ASSET_METADATA_UNKNOWN_VERSION))] +// Version 2 is not known. +#[case::unknown_version(ASSET_VERSION_1 + 1, Some(ERR_VAULT_ASSET_METADATA_UNKNOWN_VERSION))] // Metadata is not a valid byte. -#[case::not_u8(u16::MAX as u64, Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS))] -// Reserved bit 2 is set. -#[case::reserved_bit_2_set(0b100, Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS))] -// Reserved bit 3 is set. -#[case::reserved_bits_set(0b1000, Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS))] +#[case::not_u8( + METADATA_BYTE_NONE | 0x0100, + Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS) +)] +// Reserved bit 6 is set. +#[case::reserved_bit_6_set( + METADATA_BYTE_NONE | 0b0100_0000, + Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS) +)] +// Reserved bit 7 is set. +#[case::reserved_bit_7_set( + METADATA_BYTE_NONE | 0b1000_0000, + Some(ERR_VAULT_ASSET_METADATA_NON_ZERO_RESERVED_BITS) +)] // Composition value 3 is the unused bit pattern within the 2-bit field. -#[case::unknown_composition(0b011, Some(ERR_VAULT_ASSET_METADATA_UNKNOWN_COMPOSITION))] +#[case::unknown_composition( + metadata_byte(0b11), + Some(ERR_VAULT_ASSET_METADATA_UNKNOWN_COMPOSITION) +)] #[tokio::test] async fn test_validate_asset_metadata( #[case] asset_metadata: u64, 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 ab8c91b441..fe25cbb5ee 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -222,8 +222,7 @@ async fn test_mint_fungible_asset_inconsistent_faucet_id() -> anyhow::Result<()> Ok(()) } -/// Tests that minting a fungible asset on a non-faucet account fails when the key has its asset -/// metadata (lower 8 bits) set to u8::MAX. +/// Tests that minting a fungible asset fails when a reserved bit of the asset metadata is set. #[tokio::test] async fn mint_fungible_asset_fails_on_invalid_asset_metadata() -> anyhow::Result<()> { let asset = FungibleAsset::mock(50); diff --git a/docs/src/asset.md b/docs/src/asset.md index e2720c254f..a7bb1135e5 100644 --- a/docs/src/asset.md +++ b/docs/src/asset.md @@ -52,7 +52,7 @@ While the asset value is unique to each type of asset, the asset ID has a common [ asset_class_suffix (64 bits), asset_class_prefix (64 bits), - [faucet_id_suffix (56 bits) | reserved (6 bits) | composition (2 bits)], + [faucet_id_suffix (56 bits) | reserved (2 bits) | composition (2 bits) | version (4 bits)], faucet_id_prefix (64 bits) ] ``` @@ -60,6 +60,7 @@ While the asset value is unique to each type of asset, the asset ID has a common - `faucet_id_suffix` and `faucet_id_prefix` is the ID of the faucet which issues the asset. The transaction kernel ensures that a given account can only issue assets when the faucet ID matches its own ID. - `asset_class_suffix` and `asset_class_prefix` is a class that determines if two assets issued by the same faucet are considered to be the same asset. It is set by the asset creator arbitrarily - see [identity](#identity) for more. - `composition` describes how assets compose. Read on for more details. +- `version` determines how the remainder of the asset is decoded. The only valid version is currently `1`. Version `0` is unassigned and invalid, which means an empty word is guaranteed to _not_ be a valid asset ID. - `reserved` bits are reserved for future use and should be assumed to be undefined and therefore not relied upon. Whether the asset triggers [callbacks](#callbacks) is not part of the asset ID: it is an immutable property of the issuing faucet's account ID. @@ -115,7 +116,7 @@ On the other hand, `Custom` would involve invoking `merge` and `split` implement The native fungible asset has the following asset ID and value layout: -- Asset ID: `[0, 0, faucet_id_suffix | composition, faucet_id_prefix]`. +- Asset ID: `[0, 0, faucet_id_suffix | composition | version, faucet_id_prefix]`. - Its `composition` must be set to `Fungible`. - Value: `[amount, 0, 0, 0]`. - The amount is always $2^{63}-2^{31}$ or smaller, representing the maximum supply for any fungible `Asset`. @@ -128,7 +129,7 @@ Examples of such assets include ETH and various stablecoins (e.g. DAI, USDT, USD The native non-fungible asset is encoded by hashing arbitrary data into 32 bytes, which results in the asset value. -- Asset ID: `[hash0, hash1, faucet_id_suffix | composition, faucet_id_prefix]`. +- Asset ID: `[hash0, hash1, faucet_id_suffix | composition | version, faucet_id_prefix]`. - Its `composition` must be set to `None`. - Value: `[hash0, hash1, hash2, hash3]`.