Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Changes

- [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)).
- 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 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)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =================================================================================================

Expand Down
2 changes: 1 addition & 1 deletion crates/miden-protocol/asm/protocol/src/account_id.masm
Original file line number Diff line number Diff line change
Expand Up @@ -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
47 changes: 25 additions & 22 deletions crates/miden-protocol/asm/protocol_utils/src/asset.masm
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
# =================================================================================================
Expand Down Expand Up @@ -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
Expand All @@ -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]
Expand All @@ -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)
Expand All @@ -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

Expand Down
6 changes: 4 additions & 2 deletions crates/miden-protocol/src/asset/fungible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion crates/miden-protocol/src/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
98 changes: 68 additions & 30 deletions crates/miden-protocol/src/asset/vault/asset_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,13 +67,21 @@ 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.
///
/// If we make this public, we may want to instead consider introducing an `AssetIdVersion`
/// struct, similar to [`AccountIdVersion`](crate::account::AccountIdVersion).
pub(in crate::asset) const VERSION_1: u8 = 1;
Comment thread
PhilippGackstatter marked this conversation as resolved.
Outdated

/// 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;
pub(in crate::asset) const COMPOSITION_MASK: u8 = 0b11 << Self::COMPOSITION_SHIFT;

/// 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 6-7 of the metadata byte are reserved and must be zero.
pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1100_0000;

// CONSTRUCTORS
// --------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -124,7 +132,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");
Expand Down Expand Up @@ -163,6 +171,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
Expand Down Expand Up @@ -224,10 +240,11 @@ impl TryFrom<Word> 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<Self, Self::Error> {
let asset_class_suffix = id[0];
let asset_class_prefix = id[1];
Expand All @@ -237,12 +254,20 @@ impl TryFrom<Word> 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_MASK) >> Self::COMPOSITION_SHIFT,
)?;
Comment thread
PhilippGackstatter marked this conversation as resolved.
Outdated

let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64))
.expect("clearing lower bits should not produce an invalid felt");
Expand Down Expand Up @@ -325,8 +350,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};
Expand Down Expand Up @@ -365,28 +388,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(
AssetId::COMPOSITION_MASK | 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(())
}
Expand Down
2 changes: 2 additions & 0 deletions crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,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
Expand Down
15 changes: 8 additions & 7 deletions crates/miden-standards/asm/standards/assets/fungible_asset.masm
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ================================================================================================

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ================================================================================================

Expand All @@ -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
Expand Down
Loading
Loading