Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,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

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