From 40e538c1a0ec20fc20f0a9bf8176fa194a118f34 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 26 Feb 2026 11:30:33 -0300 Subject: [PATCH 01/56] Refactor code structure for improved readability and maintainability --- CHANGELOG.md | 3 + .../faucets/basic_fungible_faucet.masm | 16 + .../faucets/network_fungible_faucet.masm | 21 + .../asm/standards/faucets/mod.masm | 24 +- .../asm/standards/metadata/fungible.masm | 706 +++++++ .../src/account/components/mod.rs | 14 + .../src/account/faucets/basic_fungible.rs | 92 +- .../src/account/faucets/mod.rs | 2 +- .../src/account/faucets/network_fungible.rs | 68 +- .../src/account/faucets/token_metadata.rs | 286 ++- .../src/account/interface/test.rs | 8 + .../src/account/metadata/mod.rs | 692 ++++++- .../src/mock_chain/chain_builder.rs | 80 +- crates/miden-testing/tests/lib.rs | 1 + crates/miden-testing/tests/metadata.rs | 1663 +++++++++++++++++ 15 files changed, 3615 insertions(+), 61 deletions(-) create mode 100644 crates/miden-standards/asm/standards/metadata/fungible.masm create mode 100644 crates/miden-testing/tests/metadata.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 862398f533..386f110b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features - Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). +- Added metadata extension (`Info`) with name and content URI slots, and MASM procedures `get_token_metadata`, `get_max_supply`, `get_decimals`, and `get_token_symbol` for the token metadata standard. Aligned fungible faucet token metadata with the metadata standard: faucet now uses the canonical slot `miden::standards::metadata::token_metadata` so MASM metadata getters work with faucet storage.([#2439](https://github.com/0xMiden/miden-base/pull/2439)) + - Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)). - Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)). - Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)). @@ -34,6 +36,7 @@ - [BREAKING] The native hash function changed from RPO256 to Poseidon2 - see PR description ([#2508](https://github.com/0xMiden/miden-base/pull/2508)). - Introduced `StorageMapKey` and `StorageMapKeyHash` Word wrappers for type-safe storage map key handling ([#2431](https://github.com/0xMiden/miden-base/pull/2431)). - Restructured `miden-agglayer/asm` directory to separate bridge and faucet into per-component libraries, preventing cross-component procedure exposure ([#2294](https://github.com/0xMiden/miden-base/issues/2294)). +- `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). - Fixed MASM inline comment casing to adhere to commenting conventions ([#2398](https://github.com/0xMiden/miden-base/pull/2398)). diff --git a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index 5d3b13a920..c73217e179 100644 --- a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -4,3 +4,19 @@ pub use ::miden::standards::faucets::basic_fungible::distribute pub use ::miden::standards::faucets::basic_fungible::burn + +# Metadata — getters only (no setters for basic faucet) +pub use ::miden::standards::metadata::fungible::get_name +pub use ::miden::standards::metadata::fungible::get_config +pub use ::miden::standards::metadata::fungible::is_max_supply_mutable +pub use ::miden::standards::metadata::fungible::is_description_mutable +pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable +pub use ::miden::standards::metadata::fungible::is_external_link_mutable +pub use ::miden::standards::metadata::fungible::get_description +pub use ::miden::standards::metadata::fungible::get_logo_uri +pub use ::miden::standards::metadata::fungible::get_external_link +pub use ::miden::standards::metadata::fungible::get_token_metadata +pub use ::miden::standards::metadata::fungible::get_max_supply +pub use ::miden::standards::metadata::fungible::get_decimals +pub use ::miden::standards::metadata::fungible::get_token_symbol +pub use ::miden::standards::metadata::fungible::get_token_supply diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index 7d350a4224..8148e978d6 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -6,3 +6,24 @@ pub use ::miden::standards::faucets::network_fungible::distribute pub use ::miden::standards::faucets::network_fungible::burn pub use ::miden::standards::faucets::network_fungible::transfer_ownership pub use ::miden::standards::faucets::network_fungible::renounce_ownership + +# Metadata — all from metadata::fungible +pub use ::miden::standards::metadata::fungible::get_owner +pub use ::miden::standards::metadata::fungible::get_name +pub use ::miden::standards::metadata::fungible::get_config +pub use ::miden::standards::metadata::fungible::is_max_supply_mutable +pub use ::miden::standards::metadata::fungible::is_description_mutable +pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable +pub use ::miden::standards::metadata::fungible::is_external_link_mutable +pub use ::miden::standards::metadata::fungible::get_description +pub use ::miden::standards::metadata::fungible::get_logo_uri +pub use ::miden::standards::metadata::fungible::get_external_link +pub use ::miden::standards::metadata::fungible::get_token_metadata +pub use ::miden::standards::metadata::fungible::get_max_supply +pub use ::miden::standards::metadata::fungible::get_decimals +pub use ::miden::standards::metadata::fungible::get_token_symbol +pub use ::miden::standards::metadata::fungible::get_token_supply +pub use ::miden::standards::metadata::fungible::optional_set_description +pub use ::miden::standards::metadata::fungible::optional_set_logo_uri +pub use ::miden::standards::metadata::fungible::optional_set_external_link +pub use ::miden::standards::metadata::fungible::optional_set_max_supply diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index bee1948a2c..8d5bd9b8f1 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -12,6 +12,10 @@ use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT const ASSET_PTR=0 const PRIVATE_NOTE=2 +# Token metadata slot — duplicated here so distribute can read it via exec without +# going through the call-safe get_max_supply wrapper. +const TOKEN_METADATA_SLOT = word("miden::standards::metadata::token_metadata") + # ERRORS # ================================================================================================= @@ -31,9 +35,9 @@ const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS="burn requires exactly 1 no # The local memory address at which the metadata slot content is stored. const METADATA_SLOT_LOCAL=0 -# The standard slot where fungible faucet metadata like token symbol or decimals are stored. +# The standard slot where fungible faucet metadata is stored (canonical with metadata module). # Layout: [token_supply, max_supply, decimals, token_symbol] -const METADATA_SLOT=word("miden::standards::fungible_faucets::metadata") +const METADATA_SLOT=word("miden::standards::metadata::token_metadata") #! Distributes freshly minted fungible assets to the provided recipient by creating a note. #! @@ -69,19 +73,9 @@ pub proc distribute # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] # Assert that minting does not violate any supply constraints. - # - # To make sure we cannot mint more than intended, we need to check: - # 1) (max_supply - token_supply) <= max_supply, i.e. the subtraction does not wrap around - # 2) amount + token_supply does not exceed max_supply - # 3) amount + token_supply is less than FUNGIBLE_ASSET_MAX_AMOUNT - # - # This is done with the following concrete assertions: - # - assert token_supply <= max_supply which ensures 1) - # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT to help ensure 3) - # - assert amount <= max_mint_amount to ensure 2) as well as 3) - # - this ensures 3) because token_supply + max_mint_amount at most ends up being equal to - # max_supply and we already asserted that max_supply does not exceed - # FUNGIBLE_ASSET_MAX_AMOUNT + # - assert token_supply <= max_supply + # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT + # - assert amount <= max_mint_amount # --------------------------------------------------------------------------------------------- dup.1 dup.1 diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm new file mode 100644 index 0000000000..3bdb12a60c --- /dev/null +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -0,0 +1,706 @@ +# miden::standards::metadata::fungible +# +# Metadata for fungible-style accounts: slots, getters (name, description, logo_uri, +# external_link, token_metadata), get_owner, and optional setters. +# Depends on ownable for owner and mutable fields. + +use miden::protocol::active_account +use miden::protocol::native_account +use miden::standards::access::ownable + +# ================================================================================================= +# CONSTANTS — slot names +# ================================================================================================= + +const TOKEN_METADATA_SLOT = word("miden::standards::metadata::token_metadata") +const NAME_CHUNK_0_SLOT = word("miden::standards::metadata::name_0") +const NAME_CHUNK_1_SLOT = word("miden::standards::metadata::name_1") +const CONFIG_SLOT = word("miden::standards::metadata::config") + +const DESCRIPTION_0_SLOT = word("miden::standards::metadata::description_0") +const DESCRIPTION_1_SLOT = word("miden::standards::metadata::description_1") +const DESCRIPTION_2_SLOT = word("miden::standards::metadata::description_2") +const DESCRIPTION_3_SLOT = word("miden::standards::metadata::description_3") +const DESCRIPTION_4_SLOT = word("miden::standards::metadata::description_4") +const DESCRIPTION_5_SLOT = word("miden::standards::metadata::description_5") + +const LOGO_URI_0_SLOT = word("miden::standards::metadata::logo_uri_0") +const LOGO_URI_1_SLOT = word("miden::standards::metadata::logo_uri_1") +const LOGO_URI_2_SLOT = word("miden::standards::metadata::logo_uri_2") +const LOGO_URI_3_SLOT = word("miden::standards::metadata::logo_uri_3") +const LOGO_URI_4_SLOT = word("miden::standards::metadata::logo_uri_4") +const LOGO_URI_5_SLOT = word("miden::standards::metadata::logo_uri_5") + +const EXTERNAL_LINK_0_SLOT = word("miden::standards::metadata::external_link_0") +const EXTERNAL_LINK_1_SLOT = word("miden::standards::metadata::external_link_1") +const EXTERNAL_LINK_2_SLOT = word("miden::standards::metadata::external_link_2") +const EXTERNAL_LINK_3_SLOT = word("miden::standards::metadata::external_link_3") +const EXTERNAL_LINK_4_SLOT = word("miden::standards::metadata::external_link_4") +const EXTERNAL_LINK_5_SLOT = word("miden::standards::metadata::external_link_5") + +const CHUNK_1_LOC = 0 +const CHUNK_0_LOC = 4 +const FIELD_0_LOC = 0 +const FIELD_1_LOC = 4 +const FIELD_2_LOC = 8 +const FIELD_3_LOC = 12 +const FIELD_4_LOC = 16 +const FIELD_5_LOC = 20 + +const ERR_DESCRIPTION_NOT_MUTABLE = "description is not mutable" +const ERR_LOGO_URI_NOT_MUTABLE = "logo URI is not mutable" +const ERR_EXTERNAL_LINK_NOT_MUTABLE = "external link is not mutable" +const ERR_MAX_SUPPLY_IMMUTABLE = "max supply is immutable" +const ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY = "new max supply is less than current token supply" + +# Advice map keys for the 6-word field data. +const DESCRIPTION_DATA_KEY = [0, 0, 0, 1] +const LOGO_URI_DATA_KEY = [0, 0, 0, 2] +const EXTERNAL_LINK_DATA_KEY = [0, 0, 0, 3] + +# ================================================================================================= +# PRIVATE HELPERS — single source of truth for slot access +# ================================================================================================= + +#! Loads token metadata word from storage. Output: [token_symbol, decimals, max_supply, token_supply]. +proc get_token_metadata_word + push.TOKEN_METADATA_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads name chunk 0 (1 Word). Output: [NAME_CHUNK_0]. +proc get_name_chunk_0 + push.NAME_CHUNK_0_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads name chunk 1 (1 Word). Output: [NAME_CHUNK_1]. +proc get_name_chunk_1 + push.NAME_CHUNK_1_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads the config word. Output: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)]. +#! (word[3] on top after get_item) +proc get_config_word + push.CONFIG_SLOT[0..2] + exec.active_account::get_item +end + +# --- Description chunk loaders --- +proc get_description_chunk_0 + push.DESCRIPTION_0_SLOT[0..2] + exec.active_account::get_item +end +proc get_description_chunk_1 + push.DESCRIPTION_1_SLOT[0..2] + exec.active_account::get_item +end +proc get_description_chunk_2 + push.DESCRIPTION_2_SLOT[0..2] + exec.active_account::get_item +end +proc get_description_chunk_3 + push.DESCRIPTION_3_SLOT[0..2] + exec.active_account::get_item +end +proc get_description_chunk_4 + push.DESCRIPTION_4_SLOT[0..2] + exec.active_account::get_item +end +proc get_description_chunk_5 + push.DESCRIPTION_5_SLOT[0..2] + exec.active_account::get_item +end + +# --- Logo URI chunk loaders --- +proc get_logo_uri_chunk_0 + push.LOGO_URI_0_SLOT[0..2] + exec.active_account::get_item +end +proc get_logo_uri_chunk_1 + push.LOGO_URI_1_SLOT[0..2] + exec.active_account::get_item +end +proc get_logo_uri_chunk_2 + push.LOGO_URI_2_SLOT[0..2] + exec.active_account::get_item +end +proc get_logo_uri_chunk_3 + push.LOGO_URI_3_SLOT[0..2] + exec.active_account::get_item +end +proc get_logo_uri_chunk_4 + push.LOGO_URI_4_SLOT[0..2] + exec.active_account::get_item +end +proc get_logo_uri_chunk_5 + push.LOGO_URI_5_SLOT[0..2] + exec.active_account::get_item +end + +# --- External link chunk loaders --- +proc get_external_link_chunk_0 + push.EXTERNAL_LINK_0_SLOT[0..2] + exec.active_account::get_item +end +proc get_external_link_chunk_1 + push.EXTERNAL_LINK_1_SLOT[0..2] + exec.active_account::get_item +end +proc get_external_link_chunk_2 + push.EXTERNAL_LINK_2_SLOT[0..2] + exec.active_account::get_item +end +proc get_external_link_chunk_3 + push.EXTERNAL_LINK_3_SLOT[0..2] + exec.active_account::get_item +end +proc get_external_link_chunk_4 + push.EXTERNAL_LINK_4_SLOT[0..2] + exec.active_account::get_item +end +proc get_external_link_chunk_5 + push.EXTERNAL_LINK_5_SLOT[0..2] + exec.active_account::get_item +end + +# ================================================================================================= +# TOKEN METADATA (token_supply, max_supply, decimals, token_symbol) +# ================================================================================================= + +#! Returns the full token metadata word. +#! +#! The word is stored as [token_supply, max_supply, decimals, token_symbol] and +#! loaded onto the stack with word[3] on top. +#! +#! Inputs: [] +#! Outputs: [token_symbol, decimals, max_supply, token_supply, pad(12)] (word[3] on top) +#! +#! Invocation: call +pub proc get_token_metadata + exec.get_token_metadata_word + swapw dropw + # => [token_symbol, decimals, max_supply, token_supply, pad(12)] +end + +#! Returns the maximum supply. +#! Inputs: [pad(16)] Outputs: [max_supply, pad(15)] Invocation: call +pub proc get_max_supply + exec.get_token_metadata + # => [token_symbol, decimals, max_supply, token_supply, pad(12)] + drop drop swap drop + # => [max_supply, pad(15)] +end + +#! Returns decimals (single felt; e.g. 8). +#! Inputs: [pad(16)] Outputs: [decimals, pad(15)] Invocation: call +pub proc get_decimals + exec.get_token_metadata + # => [token_symbol, decimals, max_supply, token_supply, pad(12)] + drop movdn.2 drop drop + # => [decimals, pad(15)] +end + +#! Returns token_symbol (single felt). +#! Inputs: [pad(16)] Outputs: [token_symbol, pad(15)] Invocation: call +pub proc get_token_symbol + exec.get_token_metadata + # => [token_symbol, decimals, max_supply, token_supply, pad(12)] + movdn.3 drop drop drop + # => [token_symbol, pad(15)] +end + +#! Returns token_supply (single felt). +#! Inputs: [pad(16)] Outputs: [token_supply, pad(15)] Invocation: call +pub proc get_token_supply + exec.get_token_metadata + # => [token_symbol, decimals, max_supply, token_supply, pad(12)] + drop drop drop + # => [token_supply, pad(15)] +end + +# ================================================================================================= +# NAME (2 words) +# ================================================================================================= + +#! Returns the name (2 Words) padded to 16 elements: [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)]. +#! +#! Inputs: [pad(16)] +#! Outputs: [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] (16 felts; chunk_0 on top) +#! +#! Invocation: call +@locals(8) +pub proc get_name + exec.get_name_chunk_1 + loc_storew_be.CHUNK_1_LOC dropw + + exec.get_name_chunk_0 + loc_storew_be.CHUNK_0_LOC dropw + + loc_loadw_be.CHUNK_1_LOC + swapw + loc_loadw_be.CHUNK_0_LOC + # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] +end + +# ================================================================================================= +# CONFIG — [desc_flag, logo_flag, extlink_flag, max_supply_mutable] +# ================================================================================================= + +#! Returns the config word. +#! +#! After get_item, stack has word[3] on top: +#! [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] +#! +#! Inputs: [pad(16)] +#! Outputs: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] +#! +#! Invocation: call +pub proc get_config + exec.get_config_word + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] +end + +#! Returns whether max supply is mutable (single felt: 0 or 1). +#! +#! Reads config word felt[3] (max_supply_mutable), which is on top after get_item. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_max_supply_mutable + exec.get_config_word + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + movdn.3 drop drop drop + # => [max_supply_mutable, pad(15)] +end + +#! Returns whether description is mutable (flag == 2). +#! +#! Reads config word felt[0] (desc_flag). Returns 1 if mutable, 0 otherwise. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_description_mutable + exec.get_config_word + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + drop drop drop + # => [desc_flag, pad(12)] + push.2 eq + # => [is_mutable, pad(12)] +end + +#! Returns whether logo URI is mutable (flag == 2). +#! +#! Reads config word felt[1] (logo_flag). Returns 1 if mutable, 0 otherwise. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_logo_uri_mutable + exec.get_config_word + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + drop drop swap drop + # => [logo_flag, pad(12)] + push.2 eq + # => [is_mutable, pad(12)] +end + +#! Returns whether external link is mutable (flag == 2). +#! +#! Reads config word felt[2] (extlink_flag). Returns 1 if mutable, 0 otherwise. +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_external_link_mutable + exec.get_config_word + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + drop movdn.2 drop drop + # => [extlink_flag, pad(12)] + push.2 eq + # => [is_mutable, pad(12)] +end + +# ================================================================================================= +# OWNER — re-export from ownable +# ================================================================================================= + +#! Returns the owner AccountId. +#! +#! Inputs: [pad(16)] +#! Outputs: [owner_prefix, owner_suffix, pad(14)] +#! +#! Invocation: call +pub use ownable::get_owner + +# ================================================================================================= +# DESCRIPTION (6 words) +# ================================================================================================= + +#! Returns the description (first word of 6). +#! +#! Due to the call convention (depth must be 16 at return), only DESCRIPTION_0 is +#! returned on the visible stack. +#! +#! Inputs: [pad(16)] +#! Outputs: [DESCRIPTION_0, pad(12)] +#! +#! Invocation: call +@locals(24) +pub proc get_description + exec.get_description_chunk_5 + loc_storew_be.FIELD_5_LOC dropw + + exec.get_description_chunk_4 + loc_storew_be.FIELD_4_LOC dropw + + exec.get_description_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + + exec.get_description_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + + exec.get_description_chunk_1 + loc_storew_be.FIELD_1_LOC dropw + + exec.get_description_chunk_0 + loc_storew_be.FIELD_0_LOC dropw + + loc_loadw_be.FIELD_5_LOC + loc_loadw_be.FIELD_4_LOC + loc_loadw_be.FIELD_3_LOC + loc_loadw_be.FIELD_2_LOC + loc_loadw_be.FIELD_1_LOC + loc_loadw_be.FIELD_0_LOC +end + +# ================================================================================================= +# LOGO URI (6 words) +# ================================================================================================= + +#! Returns the logo URI (first word of 6). +#! +#! Inputs: [pad(16)] +#! Outputs: [LOGO_URI_0, pad(12)] +#! +#! Invocation: call +@locals(24) +pub proc get_logo_uri + exec.get_logo_uri_chunk_5 + loc_storew_be.FIELD_5_LOC dropw + + exec.get_logo_uri_chunk_4 + loc_storew_be.FIELD_4_LOC dropw + + exec.get_logo_uri_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + + exec.get_logo_uri_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + + exec.get_logo_uri_chunk_1 + loc_storew_be.FIELD_1_LOC dropw + + exec.get_logo_uri_chunk_0 + loc_storew_be.FIELD_0_LOC dropw + + loc_loadw_be.FIELD_5_LOC + loc_loadw_be.FIELD_4_LOC + loc_loadw_be.FIELD_3_LOC + loc_loadw_be.FIELD_2_LOC + loc_loadw_be.FIELD_1_LOC + loc_loadw_be.FIELD_0_LOC +end + +# ================================================================================================= +# EXTERNAL LINK (6 words) +# ================================================================================================= + +#! Returns the external link (first word of 6). +#! +#! Inputs: [pad(16)] +#! Outputs: [EXTERNAL_LINK_0, pad(12)] +#! +#! Invocation: call +@locals(24) +pub proc get_external_link + exec.get_external_link_chunk_5 + loc_storew_be.FIELD_5_LOC dropw + + exec.get_external_link_chunk_4 + loc_storew_be.FIELD_4_LOC dropw + + exec.get_external_link_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + + exec.get_external_link_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + + exec.get_external_link_chunk_1 + loc_storew_be.FIELD_1_LOC dropw + + exec.get_external_link_chunk_0 + loc_storew_be.FIELD_0_LOC dropw + + loc_loadw_be.FIELD_5_LOC + loc_loadw_be.FIELD_4_LOC + loc_loadw_be.FIELD_3_LOC + loc_loadw_be.FIELD_2_LOC + loc_loadw_be.FIELD_1_LOC + loc_loadw_be.FIELD_0_LOC +end + +# ================================================================================================= +# OPTIONAL SET DESCRIPTION (owner-only when desc_flag == 2) +# ================================================================================================= + +#! Updates the description (6 Words) if the description flag is 2 (present+mutable) +#! and the note sender is the owner. +#! +#! Before executing the transaction, populate the advice map: +#! key: DESCRIPTION_DATA_KEY ([0, 0, 0, 1]) +#! value: [D0, D1, D2, D3, D4, D5] (24 felts in natural order) +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the description flag is not 2 (not present+mutable). +#! - the note sender is not the owner. +#! +#! Invocation: call (from note script context) +pub proc optional_set_description + # Read config word: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_config_word + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # desc_flag is at bottom of the 4 elements; drop the top 3 + drop drop drop + # => [desc_flag, pad(15)] + push.2 eq + assert.err=ERR_DESCRIPTION_NOT_MUTABLE + # => [pad(16)] + + # Verify note sender is the owner + exec.ownable::verify_owner + # => [pad(16)] + + # Load the 6 description words from the advice map + push.DESCRIPTION_DATA_KEY + adv.push_mapval + dropw + # => [pad(16)] + + padw adv_loadw + push.DESCRIPTION_0_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_1_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_2_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_3_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_4_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_5_SLOT[0..2] + exec.native_account::set_item dropw +end + +# ================================================================================================= +# OPTIONAL SET LOGO URI (owner-only when logo_flag == 2) +# ================================================================================================= + +#! Updates the logo URI (6 Words) if the logo URI flag is 2 (present+mutable) +#! and the note sender is the owner. +#! +#! Before executing the transaction, populate the advice map: +#! key: LOGO_URI_DATA_KEY ([0, 0, 0, 2]) +#! value: [L0, L1, L2, L3, L4, L5] (24 felts in natural order) +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the logo URI flag is not 2 (not present+mutable). +#! - the note sender is not the owner. +#! +#! Invocation: call (from note script context) +pub proc optional_set_logo_uri + # Read config word: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_config_word + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # logo_flag is word[1]; drop top 2 (max_supply_mutable, extlink_flag), then swap+drop + drop drop + # => [logo_flag, desc_flag, pad(14)] + swap drop + # => [logo_flag, pad(15)] + push.2 eq + assert.err=ERR_LOGO_URI_NOT_MUTABLE + # => [pad(16)] + + exec.ownable::verify_owner + + push.LOGO_URI_DATA_KEY + adv.push_mapval + dropw + + padw adv_loadw + push.LOGO_URI_0_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_1_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_2_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_3_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_4_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_5_SLOT[0..2] + exec.native_account::set_item dropw +end + +# ================================================================================================= +# OPTIONAL SET EXTERNAL LINK (owner-only when extlink_flag == 2) +# ================================================================================================= + +#! Updates the external link (6 Words) if the external link flag is 2 (present+mutable) +#! and the note sender is the owner. +#! +#! Before executing the transaction, populate the advice map: +#! key: EXTERNAL_LINK_DATA_KEY ([0, 0, 0, 3]) +#! value: [E0, E1, E2, E3, E4, E5] (24 felts in natural order) +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the external link flag is not 2 (not present+mutable). +#! - the note sender is not the owner. +#! +#! Invocation: call (from note script context) +pub proc optional_set_external_link + # Read config word: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_config_word + swapw dropw + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # extlink_flag is word[2]; drop top 1 (max_supply_mutable), then move down and drop + drop + # => [extlink_flag, logo_flag, desc_flag, pad(13)] + movdn.2 drop drop + # => [extlink_flag, pad(15)] + push.2 eq + assert.err=ERR_EXTERNAL_LINK_NOT_MUTABLE + # => [pad(16)] + + exec.ownable::verify_owner + + push.EXTERNAL_LINK_DATA_KEY + adv.push_mapval + dropw + + padw adv_loadw + push.EXTERNAL_LINK_0_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_1_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_2_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_3_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_4_SLOT[0..2] + exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_5_SLOT[0..2] + exec.native_account::set_item dropw +end + +# ================================================================================================= +# OPTIONAL SET MAX SUPPLY (owner-only when max_supply_mutable is 1) +# ================================================================================================= + +#! Updates max_supply if the max_supply_mutable flag is set and the note sender is the owner. +#! The new max_supply is passed on the stack (1 felt). +#! +#! Inputs: [new_max_supply, pad(15)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - max_supply_mutable flag is 0 (immutable) +#! - note sender is not the owner +#! - new_max_supply < current token_supply +#! +#! Invocation: call (from note script context) +pub proc optional_set_max_supply + # 1. Check mutable flag from config word + exec.get_config_word + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, new_max_supply, pad(15)] + push.1 eq + assert.err=ERR_MAX_SUPPLY_IMMUTABLE + # => [extlink_flag, logo_flag, desc_flag, new_max_supply, pad(15)] + drop drop drop + # => [new_max_supply, pad(15)] + + # 2. Verify note sender is the owner + exec.ownable::verify_owner + # => [new_max_supply, pad(15)] + + # 3. Read current metadata word + exec.get_token_metadata_word + # => [token_symbol, decimals, max_supply, token_supply, new_max_supply, pad(15)] + + # 4. Validate: new_max_supply >= token_supply (i.e. token_supply <= new_max_supply) + dup.3 # copy token_supply to top + dup.5 # copy new_max_supply to top + # => [new_max_supply, token_supply, token_symbol, decimals, max_supply, token_supply, new_max_supply, ...] + lte # token_supply <= new_max_supply + assert.err=ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY + # => [token_symbol, decimals, max_supply, token_supply, new_max_supply, pad(15)] + + # 5. Replace max_supply with new_max_supply in the metadata word + movup.4 # bring new_max_supply to top + movdn.2 # => [token_symbol, decimals, new_max_supply, max_supply, token_supply, pad(15)] + movup.3 # bring old max_supply to top + drop # discard old max_supply + # => [token_symbol, decimals, new_max_supply, token_supply, pad(15)] + + # 6. Write updated metadata word back to storage + push.TOKEN_METADATA_SLOT[0..2] + exec.native_account::set_item + # => [OLD_VALUE, pad(15)] + dropw + # => [pad(16)] +end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index edcbea2b4c..62f106ea27 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -8,6 +8,7 @@ use miden_protocol::assembly::{Library, LibraryExport}; use miden_protocol::utils::serde::Deserializable; use miden_protocol::utils::sync::LazyLock; +use crate::StandardsLib; use crate::account::interface::AccountComponentInterface; // WALLET LIBRARIES @@ -88,6 +89,10 @@ static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed") }); +// Metadata Info component uses the standards library (get_name, get_description, etc. from metadata). +static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = + LazyLock::new(|| Library::from(StandardsLib::default())); + /// Returns the Basic Wallet Library. pub fn basic_wallet_library() -> Library { BASIC_WALLET_LIBRARY.clone() @@ -108,6 +113,15 @@ pub fn storage_schema_library() -> Library { STORAGE_SCHEMA_LIBRARY.clone() } +/// Returns the Metadata Info component library. +/// +/// Uses the standards library; the standalone [`Info`](crate::account::metadata::Info) +/// component exposes get_name, get_description, get_logo_uri, get_external_link from +/// `miden::standards::metadata::fungible`. +pub fn metadata_info_component_library() -> Library { + METADATA_INFO_COMPONENT_LIBRARY.clone() +} + /// Returns the Singlesig Library. pub fn singlesig_library() -> Library { SINGLESIG_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 9fab85a86c..4c8e5b5fca 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -17,7 +17,7 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; -use super::{FungibleFaucetError, TokenMetadata}; +use super::{Description, ExternalLink, FungibleFaucetError, LogoURI, TokenMetadata, TokenName}; use crate::account::AuthMethod; use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig}; use crate::account::components::basic_fungible_faucet_library; @@ -25,6 +25,7 @@ use crate::account::components::basic_fungible_faucet_library; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::account::metadata::Info; use crate::procedure_digest; // BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -88,6 +89,10 @@ impl BasicFungibleFaucet { /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata and with /// an initial token supply of zero. /// + /// Optional `description`, `logo_uri`, and `external_link` are stored in the metadata + /// [`Info`][Info] component when building an account (e.g. via + /// [`create_basic_fungible_faucet`]). + /// /// # Errors /// /// Returns an error if: @@ -98,8 +103,20 @@ impl BasicFungibleFaucet { symbol: TokenSymbol, decimals: u8, max_supply: Felt, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { - let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; + let metadata = TokenMetadata::new( + symbol, + decimals, + max_supply, + name, + description, + logo_uri, + external_link, + )?; Ok(Self { metadata }) } @@ -141,7 +158,8 @@ impl BasicFungibleFaucet { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored. + /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored (slot + /// 0). pub fn metadata_slot() -> &'static StorageSlotName { TokenMetadata::metadata_slot() } @@ -257,7 +275,8 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// Creates a new faucet account with basic fungible faucet interface, /// account storage type, specified authentication scheme, and provided meta data (token symbol, -/// decimals, max supply). +/// decimals, max supply). Optional `name`, `description`, `logo_uri`, and `external_link` are +/// stored in the metadata [`Info`][Info] component when provided. /// /// The basic faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -271,6 +290,8 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// components (see their docs for details): /// - [`BasicFungibleFaucet`] /// - [`AuthSingleSigAcl`] +/// - [`Info`][Info] (when `name` or optional fields are provided) +#[allow(clippy::too_many_arguments)] pub fn create_basic_fungible_faucet( init_seed: [u8; 32], symbol: TokenSymbol, @@ -278,6 +299,10 @@ pub fn create_basic_fungible_faucet( max_supply: Felt, account_storage_mode: AccountStorageMode, auth_method: AuthMethod, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { let distribute_proc_root = BasicFungibleFaucet::distribute_digest(); @@ -309,11 +334,33 @@ pub fn create_basic_fungible_faucet( }, }; + let faucet = BasicFungibleFaucet::new( + symbol, + decimals, + max_supply, + name, + description, + logo_uri, + external_link, + )?; + + let mut info = Info::new().with_name(name.as_words()); + if let Some(d) = &description { + info = info.with_description(d.as_words(), 1); + } + if let Some(l) = &logo_uri { + info = info.with_logo_uri(l.as_words(), 1); + } + if let Some(e) = &external_link { + info = info.with_external_link(e.as_words(), 1); + } + let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(account_storage_mode) .with_auth_component(auth_component) - .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?) + .with_component(faucet) + .with_component(info) .build() .map_err(FungibleFaucetError::AccountError)?; @@ -335,12 +382,15 @@ mod tests { AccountType, AuthMethod, BasicFungibleFaucet, + Description, Felt, FungibleFaucetError, + TokenName, TokenSymbol, create_basic_fungible_faucet, }; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; + use crate::account::metadata::{self as account_metadata, Info}; use crate::account::wallets::BasicWallet; #[test] @@ -359,9 +409,13 @@ mod tests { let max_supply = Felt::new(123); let token_symbol_string = "POL"; let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap(); + let token_name_string = "polygon"; + let description_string = "A polygon token"; let decimals = 2u8; let storage_mode = AccountStorageMode::Private; + let token_name = TokenName::try_from(token_name_string).unwrap(); + let description = Description::try_from(description_string).unwrap(); let faucet_account = create_basic_fungible_faucet( init_seed, token_symbol, @@ -369,6 +423,10 @@ mod tests { max_supply, storage_mode, auth_method, + token_name, + Some(description), + None, + None, ) .unwrap(); @@ -408,6 +466,18 @@ mod tests { [Felt::ZERO, Felt::new(123), Felt::new(2), token_symbol.into()].into() ); + // Check that Info component has name and description + let name_0 = faucet_account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = faucet_account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let decoded_name = account_metadata::name_to_utf8(&[name_0, name_1]).unwrap(); + assert_eq!(decoded_name, token_name_string); + let expected_desc_words = + account_metadata::field_from_bytes(description_string.as_bytes()).unwrap(); + for (i, expected) in expected_desc_words.iter().enumerate() { + let chunk = faucet_account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + assert!(faucet_account.is_faucet()); assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); @@ -432,8 +502,16 @@ mod tests { let faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) .with_component( - BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100)) - .expect("failed to create a fungible faucet component"), + BasicFungibleFaucet::new( + token_symbol, + 10, + Felt::new(100), + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .expect("failed to create a fungible faucet component"), ) .with_auth_component(AuthSingleSig::new( mock_public_key, diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 6b66c45697..851cc6678f 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -10,7 +10,7 @@ mod token_metadata; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; -pub use token_metadata::TokenMetadata; +pub use token_metadata::{Description, ExternalLink, LogoURI, TokenMetadata, TokenName}; // FUNGIBLE FAUCET ERROR // ================================================================================================ diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 14eba97c5d..e6ceff29e6 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -17,16 +17,16 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::TokenSymbol; -use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; -use super::{FungibleFaucetError, TokenMetadata}; +use super::{Description, ExternalLink, FungibleFaucetError, LogoURI, TokenMetadata, TokenName}; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::account::metadata::Info; use crate::procedure_digest; // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -46,11 +46,6 @@ procedure_digest!( network_fungible_faucet_library ); -static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::access::ownable::owner_config") - .expect("storage slot name should be valid") -}); - /// An [`AccountComponent`] implementing a network fungible faucet. /// /// It reexports the procedures from `miden::standards::faucets::network_fungible`. When linking @@ -93,6 +88,10 @@ impl NetworkFungibleFaucet { /// Creates a new [`NetworkFungibleFaucet`] component from the given pieces of metadata. /// + /// Optional `description`, `logo_uri`, and `external_link` are stored in the metadata + /// [`Info`][Info] component when building an account (e.g. via + /// [`create_network_fungible_faucet`]). + /// /// # Errors: /// Returns an error if: /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. @@ -103,8 +102,20 @@ impl NetworkFungibleFaucet { decimals: u8, max_supply: Felt, owner_account_id: AccountId, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { - let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; + let metadata = TokenMetadata::new( + symbol, + decimals, + max_supply, + name, + description, + logo_uri, + external_link, + )?; Ok(Self { metadata, owner_account_id }) } @@ -164,15 +175,16 @@ impl NetworkFungibleFaucet { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s metadata is stored. + /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s metadata is stored + /// (slot 0). pub fn metadata_slot() -> &'static StorageSlotName { TokenMetadata::metadata_slot() } /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s owner configuration is - /// stored. + /// stored (slot 1). pub fn owner_config_slot() -> &'static StorageSlotName { - &OWNER_CONFIG_SLOT_NAME + crate::account::metadata::owner_config_slot() } /// Returns the storage slot schema for the metadata slot. @@ -327,7 +339,9 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { } /// Creates a new faucet account with network fungible faucet interface and provided metadata -/// (token symbol, decimals, max supply, owner account ID). +/// (token symbol, decimals, max supply, owner account ID). Optional `name`, `description`, +/// `logo_uri`, and `external_link` are stored in the metadata [`Info`][Info] component when +/// provided. /// /// The network faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -343,20 +357,48 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// /// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] type and /// contains no additional storage slots for its auth ([`NoAuth`]). +#[allow(clippy::too_many_arguments)] pub fn create_network_fungible_faucet( init_seed: [u8; 32], symbol: TokenSymbol, decimals: u8, max_supply: Felt, owner_account_id: AccountId, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); + let faucet = NetworkFungibleFaucet::new( + symbol, + decimals, + max_supply, + owner_account_id, + name, + description, + logo_uri, + external_link, + )?; + + let mut info = Info::new().with_name(name.as_words()); + if let Some(d) = &description { + info = info.with_description(d.as_words(), 1); + } + if let Some(l) = &logo_uri { + info = info.with_logo_uri(l.as_words(), 1); + } + if let Some(e) = &external_link { + info = info.with_external_link(e.as_words(), 1); + } + let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(auth_component) - .with_component(NetworkFungibleFaucet::new(symbol, decimals, max_supply, owner_account_id)?) + .with_component(faucet) + .with_component(info) .build() .map_err(FungibleFaucetError::AccountError)?; diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index d1611b75f5..5053a2fac3 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -1,17 +1,107 @@ use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; use miden_protocol::asset::{FungibleAsset, TokenSymbol}; -use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use super::FungibleFaucetError; +use crate::account::metadata::{self, FieldBytesError, NameUtf8Error}; -// CONSTANTS +/// Schema type ID for the token symbol field in token metadata storage schema. +pub const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; + +// TOKEN NAME +// ================================================================================================ + +/// Token display name (max 32 bytes UTF-8), stored as 2 Words in the metadata Info component. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TokenName([Word; 2]); + +impl TokenName { + /// Creates a token name from a UTF-8 string (at most 32 bytes). + pub fn try_from(s: &str) -> Result { + let words = metadata::name_from_utf8(s)?; + Ok(Self(words)) + } + + /// Returns the name as two Words for storage in the Info component. + pub fn as_words(&self) -> [Word; 2] { + self.0 + } +} + +// DESCRIPTION +// ================================================================================================ + +/// Token description (max 192 bytes), stored as 6 Words in the metadata Info component. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Description([Word; 6]); + +impl Description { + /// Creates a description from a byte slice (at most 192 bytes). + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let words = metadata::field_from_bytes(bytes)?; + Ok(Self(words)) + } + + /// Creates a description from a string (encoded as UTF-8 bytes; at most 192 bytes). + pub fn try_from(s: &str) -> Result { + Self::try_from_bytes(s.as_bytes()) + } + + /// Returns the description as six Words for storage in the Info component. + pub fn as_words(&self) -> [Word; 6] { + self.0 + } +} + +// LOGO URI // ================================================================================================ -static METADATA_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::fungible_faucets::metadata") - .expect("storage slot name should be valid") -}); +/// Token logo URI (max 192 bytes), stored as 6 Words in the metadata Info component. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LogoURI([Word; 6]); + +impl LogoURI { + /// Creates a logo URI from a byte slice (at most 192 bytes). + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let words = metadata::field_from_bytes(bytes)?; + Ok(Self(words)) + } + + /// Creates a logo URI from a string (encoded as UTF-8 bytes; at most 192 bytes). + pub fn try_from(s: &str) -> Result { + Self::try_from_bytes(s.as_bytes()) + } + + /// Returns the logo URI as six Words for storage in the Info component. + pub fn as_words(&self) -> [Word; 6] { + self.0 + } +} + +// EXTERNAL LINK +// ================================================================================================ + +/// Token external link (max 192 bytes), stored as 6 Words in the metadata Info component. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExternalLink([Word; 6]); + +impl ExternalLink { + /// Creates an external link from a byte slice (at most 192 bytes). + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let words = metadata::field_from_bytes(bytes)?; + Ok(Self(words)) + } + + /// Creates an external link from a string (encoded as UTF-8 bytes; at most 192 bytes). + pub fn try_from(s: &str) -> Result { + Self::try_from_bytes(s.as_bytes()) + } + + /// Returns the external link as six Words for storage in the Info component. + pub fn as_words(&self) -> [Word; 6] { + self.0 + } +} // TOKEN METADATA // ================================================================================================ @@ -26,12 +116,19 @@ static METADATA_SLOT_NAME: LazyLock = LazyLock::new(|| { /// /// The metadata is stored in a single storage slot as: /// `[token_supply, max_supply, decimals, symbol]` +/// +/// `name` and optional `description`/`logo_uri`/`external_link` are not stored in that slot; +/// they are used only when building an account to populate the metadata Info component. #[derive(Debug, Clone, Copy)] pub struct TokenMetadata { token_supply: Felt, max_supply: Felt, decimals: u8, symbol: TokenSymbol, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, } impl TokenMetadata { @@ -54,8 +151,21 @@ impl TokenMetadata { symbol: TokenSymbol, decimals: u8, max_supply: Felt, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { - Self::with_supply(symbol, decimals, max_supply, Felt::ZERO) + Self::with_supply( + symbol, + decimals, + max_supply, + Felt::ZERO, + name, + description, + logo_uri, + external_link, + ) } /// Creates a new [`TokenMetadata`] with the specified metadata and token supply. @@ -70,6 +180,10 @@ impl TokenMetadata { decimals: u8, max_supply: Felt, token_supply: Felt, + name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, ) -> Result { if decimals > Self::MAX_DECIMALS { return Err(FungibleFaucetError::TooManyDecimals { @@ -97,6 +211,10 @@ impl TokenMetadata { max_supply, decimals, symbol, + name, + description, + logo_uri, + external_link, }) } @@ -104,8 +222,10 @@ impl TokenMetadata { // -------------------------------------------------------------------------------------------- /// Returns the [`StorageSlotName`] where the token metadata is stored. + /// Returns the storage slot name for token metadata (canonical slot shared with metadata + /// module). pub fn metadata_slot() -> &'static StorageSlotName { - &METADATA_SLOT_NAME + metadata::token_metadata_slot() } /// Returns the current token supply (amount issued). @@ -128,6 +248,26 @@ impl TokenMetadata { self.symbol } + /// Returns the token name (for Info component when building an account). + pub fn name(&self) -> &TokenName { + &self.name + } + + /// Returns the optional description (for Info component when building an account). + pub fn description(&self) -> Option<&Description> { + self.description.as_ref() + } + + /// Returns the optional logo URI (for Info component when building an account). + pub fn logo_uri(&self) -> Option<&LogoURI> { + self.logo_uri.as_ref() + } + + /// Returns the optional external link (for Info component when building an account). + pub fn external_link(&self) -> Option<&ExternalLink> { + self.external_link.as_ref() + } + // MUTATORS // -------------------------------------------------------------------------------------------- @@ -173,7 +313,9 @@ impl TryFrom for TokenMetadata { } })?; - Self::with_supply(symbol, decimals, max_supply, token_supply) + // When parsing from storage, name is not available; use empty string. + let name = TokenName::try_from("").expect("empty string should be valid"); + Self::with_supply(symbol, decimals, max_supply, token_supply, name, None, None, None) } } @@ -246,13 +388,19 @@ mod tests { let symbol = TokenSymbol::new("TEST").unwrap(); let decimals = 8u8; let max_supply = Felt::new(1_000_000); + let name = TokenName::try_from("TEST").unwrap(); - let metadata = TokenMetadata::new(symbol, decimals, max_supply).unwrap(); + let metadata = + TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None).unwrap(); assert_eq!(metadata.symbol(), symbol); assert_eq!(metadata.decimals(), decimals); assert_eq!(metadata.max_supply(), max_supply); assert_eq!(metadata.token_supply(), Felt::ZERO); + assert_eq!(metadata.name(), &name); + assert!(metadata.description().is_none()); + assert!(metadata.logo_uri().is_none()); + assert!(metadata.external_link().is_none()); } #[test] @@ -261,9 +409,19 @@ mod tests { let decimals = 8u8; let max_supply = Felt::new(1_000_000); let token_supply = Felt::new(500_000); + let name = TokenName::try_from("TEST").unwrap(); - let metadata = - TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply).unwrap(); + let metadata = TokenMetadata::with_supply( + symbol, + decimals, + max_supply, + token_supply, + name, + None, + None, + None, + ) + .unwrap(); assert_eq!(metadata.symbol(), symbol); assert_eq!(metadata.decimals(), decimals); @@ -271,13 +429,89 @@ mod tests { assert_eq!(metadata.token_supply(), token_supply); } + #[test] + fn token_metadata_with_name_and_description() { + let symbol = TokenSymbol::new("POL").unwrap(); + let decimals = 2u8; + let max_supply = Felt::new(123); + let name = TokenName::try_from("polygon").unwrap(); + let description = Description::try_from("A polygon token").unwrap(); + + let metadata = TokenMetadata::new( + symbol, + decimals, + max_supply, + name, + Some(description), + None, + None, + ) + .unwrap(); + + assert_eq!(metadata.symbol(), symbol); + assert_eq!(metadata.name(), &name); + assert_eq!(metadata.description(), Some(&description)); + // Word roundtrip does not include name/description + let word: Word = metadata.into(); + let restored = TokenMetadata::try_from(word).unwrap(); + assert_eq!(restored.symbol(), symbol); + assert!(restored.description().is_none()); + } + + #[test] + fn token_name_try_from_valid() { + let name = TokenName::try_from("polygon").unwrap(); + assert_eq!(metadata::name_to_utf8(&name.as_words()).unwrap(), "polygon"); + } + + #[test] + fn token_name_try_from_too_long() { + let s = "a".repeat(33); + assert!(TokenName::try_from(&s).is_err()); + } + + #[test] + fn description_try_from_valid() { + let text = "A short description"; + let desc = Description::try_from(text).unwrap(); + let words = desc.as_words(); + let expected = metadata::field_from_bytes(text.as_bytes()).unwrap(); + assert_eq!(words, expected); + } + + #[test] + fn description_try_from_too_long() { + let bytes = [0u8; 193]; + assert!(Description::try_from_bytes(&bytes).is_err()); + } + + #[test] + fn logo_uri_try_from_valid() { + let url = "https://example.com/logo.png"; + let uri = LogoURI::try_from(url).unwrap(); + let words = uri.as_words(); + let expected = metadata::field_from_bytes(url.as_bytes()).unwrap(); + assert_eq!(words, expected); + } + + #[test] + fn external_link_try_from_valid() { + let url = "https://example.com"; + let link = ExternalLink::try_from(url).unwrap(); + let words = link.as_words(); + let expected = metadata::field_from_bytes(url.as_bytes()).unwrap(); + assert_eq!(words, expected); + } + #[test] fn token_metadata_too_many_decimals() { let symbol = TokenSymbol::new("TEST").unwrap(); let decimals = 13u8; // exceeds MAX_DECIMALS let max_supply = Felt::new(1_000_000); + let name = TokenName::try_from("TEST").unwrap(); - let result = TokenMetadata::new(symbol, decimals, max_supply); + let result = + TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. }))); } @@ -289,8 +523,10 @@ mod tests { let decimals = 8u8; // FungibleAsset::MAX_AMOUNT is 2^63 - 1, so we use MAX_AMOUNT + 1 to exceed it let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1); + let name = TokenName::try_from("TEST").unwrap(); - let result = TokenMetadata::new(symbol, decimals, max_supply); + let result = + TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. }))); } @@ -299,8 +535,10 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let decimals = 2u8; let max_supply = Felt::new(123); + let name = TokenName::try_from("POL").unwrap(); - let metadata = TokenMetadata::new(symbol, decimals, max_supply).unwrap(); + let metadata = + TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None).unwrap(); let word: Word = metadata.into(); // Storage layout: [token_supply, max_supply, decimals, symbol] @@ -315,8 +553,10 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let decimals = 2u8; let max_supply = Felt::new(123); + let name = TokenName::try_from("POL").unwrap(); - let original = TokenMetadata::new(symbol, decimals, max_supply).unwrap(); + let original = + TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None).unwrap(); let slot: StorageSlot = original.into(); let restored = TokenMetadata::try_from(&slot).unwrap(); @@ -333,9 +573,19 @@ mod tests { let decimals = 2u8; let max_supply = Felt::new(1000); let token_supply = Felt::new(500); + let name = TokenName::try_from("POL").unwrap(); - let original = - TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply).unwrap(); + let original = TokenMetadata::with_supply( + symbol, + decimals, + max_supply, + token_supply, + name, + None, + None, + None, + ) + .unwrap(); let word: Word = original.into(); let restored = TokenMetadata::try_from(word).unwrap(); diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index c17bad6928..50824673d1 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -59,6 +59,10 @@ fn test_basic_wallet_default_notes() { TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), + crate::account::faucets::TokenName::try_from("POL").unwrap(), + None, + None, + None, ) .expect("failed to create a fungible faucet component"), ) @@ -325,6 +329,10 @@ fn test_basic_fungible_faucet_custom_notes() { TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), + crate::account::faucets::TokenName::try_from("POL").unwrap(), + None, + None, + None, ) .expect("failed to create a fungible faucet component"), ) diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 9489bc034e..616ca79860 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -1,24 +1,557 @@ +//! Account / contract / faucet metadata (slots 0..22) +//! +//! All of the following are metadata of the account (or faucet): token_symbol, decimals, +//! max_supply, owner, name, config, description, logo URI, and external link. +//! +//! ## Storage layout +//! +//! | Slot name | Contents | +//! |-----------|----------| +//! | `metadata::token_metadata` | `[token_supply, max_supply, decimals, token_symbol]` | +//! | `ownable::owner_config` | owner account id (defined by ownable module) | +//! | `metadata::name_0` | first 4 felts of name | +//! | `metadata::name_1` | last 4 felts of name | +//! | `metadata::config` | `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]` | +//! | `metadata::description_0..5` | description (6 Words, ~192 bytes) | +//! | `metadata::logo_uri_0..5` | logo URI (6 Words, ~192 bytes) | +//! | `metadata::external_link_0..5` | external link (6 Words, ~192 bytes) | +//! +//! Slot names use the `miden::standards::metadata::*` namespace, except for the +//! owner which is defined by the ownable module +//! (`miden::standards::access::ownable::owner_config`). +//! +//! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. +//! Any change to slot indices or names must be applied in both Rust and MASM. +//! +//! ## Config Word +//! +//! The config Word stores per-field flags and the max_supply_mutable flag: +//! `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]` +//! +//! Each flag uses 3 states: +//! - `0` = field not present +//! - `1` = field present, immutable +//! - `2` = field present, mutable (owner can update) +//! +//! `max_supply_mutable` uses 0/1 (always present when the faucet exists). +//! +//! ## MASM modules +//! +//! All metadata procedures (getters, `get_owner`, setters) live in +//! `miden::standards::metadata::fungible`, which depends on ownable. The standalone +//! Info component uses the standards library and exposes `get_name`, `get_description`, +//! `get_logo_uri`, `get_external_link`; for owner and mutable fields use a component +//! that re-exports from fungible (e.g. network fungible faucet). +//! +//! ## Name encoding (UTF-8) +//! +//! The name slots hold opaque words. This crate defines a **convention** for human-readable +//! names: UTF-8 bytes, 4 bytes per felt, little-endian, up to 32 bytes (see [`name_from_utf8`], +//! [`name_to_utf8`]). There is no Miden-wide standard for string→felt encoding; this convention +//! ensures Rust and MASM (or other consumers) can interoperate when they all use these helpers. +//! +//! # Example +//! +//! ```ignore +//! use miden_standards::account::metadata::Info; +//! +//! let info = Info::new() +//! .with_name([name_word_0, name_word_1]) +//! .with_description([d0, d1, d2, d3, d4, d5], 2) // present + mutable +//! .with_logo_uri([l0, l1, l2, l3, l4, l5], 1); // present + immutable +//! +//! let account = AccountBuilder::new(seed) +//! .with_component(info) +//! .build()?; +//! ``` + use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; -use miden_protocol::Word; use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema}; use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, + AccountStorage, StorageSlot, StorageSlotName, }; use miden_protocol::errors::{AccountError, ComponentMetadataError}; use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; +use thiserror::Error; + +use crate::account::components::{metadata_info_component_library, storage_schema_library}; + +// CONSTANTS — canonical layout: slots 0–22 +// ================================================================================================ + +/// Token metadata: `[token_supply, max_supply, decimals, token_symbol]`. +pub static TOKEN_METADATA_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::token_metadata") + .expect("storage slot name should be valid") +}); + +/// Owner config — defined by the ownable module (`miden::standards::access::ownable`). +/// Referenced here so that faucets and other metadata consumers can locate the owner +/// through a single `metadata::owner_config_slot()` accessor, without depending on +/// the ownable module directly. +pub static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::ownable::owner_config") + .expect("storage slot name should be valid") +}); -use crate::account::components::storage_schema_library; +/// Token name (2 Words = 8 felts), split across 2 slots. +/// +/// The encoding is not specified; the value is opaque word data. For human-readable names, +/// use UTF-8 encoding via [`name_from_utf8`] / [`name_to_utf8`] or [`Info::with_name_utf8`]. +pub static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::name_0").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::name_1").expect("valid slot name"), + ] +}); +/// Maximum length of a name in bytes when using the UTF-8 encoding (2 Words = 8 felts × 4 bytes). +pub const NAME_UTF8_MAX_BYTES: usize = 32; + +/// Errors when encoding or decoding the metadata name as UTF-8. +#[derive(Debug, Clone, Error)] +pub enum NameUtf8Error { + /// Name exceeds [`NAME_UTF8_MAX_BYTES`]. + #[error("name must be at most {NAME_UTF8_MAX_BYTES} UTF-8 bytes, got {0}")] + TooLong(usize), + /// Decoded bytes are not valid UTF-8. + #[error("name is not valid UTF-8")] + InvalidUtf8, +} + +/// Encodes a UTF-8 string into the 2-Word name format. +/// +/// Bytes are packed little-endian, 4 bytes per felt (8 felts total). The string is +/// zero-padded to 32 bytes. Returns an error if the UTF-8 byte length exceeds 32. +pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { + let bytes = s.as_bytes(); + if bytes.len() > NAME_UTF8_MAX_BYTES { + return Err(NameUtf8Error::TooLong(bytes.len())); + } + let mut padded = [0u8; NAME_UTF8_MAX_BYTES]; + padded[..bytes.len()].copy_from_slice(bytes); + let felts: [Felt; 8] = padded + .chunks_exact(4) + .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap()))) + .collect::>() + .try_into() + .unwrap(); + Ok([ + Word::from([felts[0], felts[1], felts[2], felts[3]]), + Word::from([felts[4], felts[5], felts[6], felts[7]]), + ]) +} + +/// Decodes the 2-Word name format as UTF-8. +/// +/// Assumes the name was encoded with [`name_from_utf8`] (little-endian, 4 bytes per felt). +/// Trailing zero bytes are trimmed before UTF-8 validation. +pub fn name_to_utf8(words: &[Word; 2]) -> Result { + let mut bytes = [0u8; NAME_UTF8_MAX_BYTES]; + for (i, word) in words.iter().enumerate() { + for (j, f) in word.iter().enumerate() { + let v = f.as_int(); + if v > u32::MAX as u64 { + return Err(NameUtf8Error::InvalidUtf8); + } + bytes[i * 16 + j * 4..][..4].copy_from_slice(&(v as u32).to_le_bytes()); + } + } + let len = bytes.iter().position(|&b| b == 0).unwrap_or(NAME_UTF8_MAX_BYTES); + String::from_utf8(bytes[..len].to_vec()).map_err(|_| NameUtf8Error::InvalidUtf8) +} + +/// Config slot: `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]`. +/// +/// Each flag is 0 (not present), 1 (present+immutable), or 2 (present+mutable). +/// `max_supply_mutable` is 0 or 1. +pub static CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::config") + .expect("storage slot name should be valid") +}); + +/// Maximum length of a metadata field (description, logo_uri, external_link) in bytes. +/// 6 Words = 24 felts × 8 bytes = 192 bytes. +pub const FIELD_MAX_BYTES: usize = 192; + +/// Errors when encoding metadata fields from bytes. +#[derive(Debug, Clone, Error)] +pub enum FieldBytesError { + /// Field exceeds [`FIELD_MAX_BYTES`]. + #[error("field must be at most {FIELD_MAX_BYTES} bytes, got {0}")] + TooLong(usize), +} + +/// Encodes a byte slice into 6 Words (24 felts). +/// +/// Bytes are packed little-endian, 8 bytes per felt (24 felts total). The slice is zero-padded +/// to 192 bytes. Returns an error if the length exceeds 192. +pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 6], FieldBytesError> { + if bytes.len() > FIELD_MAX_BYTES { + return Err(FieldBytesError::TooLong(bytes.len())); + } + let mut padded = [0u8; FIELD_MAX_BYTES]; + padded[..bytes.len()].copy_from_slice(bytes); + let felts: Vec = padded + .chunks_exact(8) + .map(|chunk| { + Felt::try_from(u64::from_le_bytes(chunk.try_into().unwrap())) + .expect("u64 values from 8-byte chunks fit in Felt") + }) + .collect(); + let felts: [Felt; 24] = felts.try_into().unwrap(); + Ok([ + Word::from([felts[0], felts[1], felts[2], felts[3]]), + Word::from([felts[4], felts[5], felts[6], felts[7]]), + Word::from([felts[8], felts[9], felts[10], felts[11]]), + Word::from([felts[12], felts[13], felts[14], felts[15]]), + Word::from([felts[16], felts[17], felts[18], felts[19]]), + Word::from([felts[20], felts[21], felts[22], felts[23]]), + ]) +} + +/// Description (6 Words = 24 felts), split across 6 slots. +pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::description_0") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_1") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_2") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_3") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_4") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_5") + .expect("valid slot name"), + ] +}); + +/// Logo URI (6 Words = 24 felts), split across 6 slots. +pub static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::logo_uri_0").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_1").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_2").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_3").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_4").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_5").expect("valid slot name"), + ] +}); + +/// External link (6 Words = 24 felts), split across 6 slots. +pub static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { + [ + StorageSlotName::new("miden::standards::metadata::external_link_0") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_1") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_2") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_3") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_4") + .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_5") + .expect("valid slot name"), + ] +}); + +/// Schema commitment slot. pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::metadata::storage_schema") .expect("storage slot name should be valid") }); +/// The advice map key used by `optional_set_description` to read the 6 field words. +/// +/// Must match `DESCRIPTION_DATA_KEY` in `fungible.masm`. The value stored under this key +/// should be 24 felts: `[FIELD_0, FIELD_1, FIELD_2, FIELD_3, FIELD_4, FIELD_5]`. +pub const DESCRIPTION_DATA_KEY: Word = + Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]); + +/// The advice map key used by `optional_set_logo_uri` to read the 6 field words. +pub const LOGO_URI_DATA_KEY: Word = + Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]); + +/// The advice map key used by `optional_set_external_link` to read the 6 field words. +pub const EXTERNAL_LINK_DATA_KEY: Word = + Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); + +// SLOT ACCESSORS +// ================================================================================================ + +/// Returns the [`StorageSlotName`] for token metadata (slot 0). +pub fn token_metadata_slot() -> &'static StorageSlotName { + &TOKEN_METADATA_SLOT +} + +/// Returns the [`StorageSlotName`] for owner config (slot 1). +pub fn owner_config_slot() -> &'static StorageSlotName { + &OWNER_CONFIG_SLOT +} + +/// Returns the [`StorageSlotName`] for the config Word. +pub fn config_slot() -> &'static StorageSlotName { + &CONFIG_SLOT +} + +// INFO COMPONENT +// ================================================================================================ + +/// A metadata component storing name, config, and optional fields in fixed value slots. +/// +/// ## Storage Layout +/// +/// - Slot 2–3: name (2 Words = 8 felts) +/// - Slot 4: config `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]` +/// - Slot 5–10: description (6 Words) +/// - Slot 11–16: logo_uri (6 Words) +/// - Slot 17–22: external_link (6 Words) +#[derive(Debug, Clone, Default)] +pub struct Info { + name: Option<[Word; 2]>, + /// Description flag: 0=not present, 1=present+immutable, 2=present+mutable. + description_flag: u8, + /// Logo URI flag: 0=not present, 1=present+immutable, 2=present+mutable. + logo_uri_flag: u8, + /// External link flag: 0=not present, 1=present+immutable, 2=present+mutable. + external_link_flag: u8, + /// If true (1), the owner may call optional_set_max_supply. If false (0), immutable. + max_supply_mutable: bool, + description: Option<[Word; 6]>, + logo_uri: Option<[Word; 6]>, + external_link: Option<[Word; 6]>, +} + +impl Info { + /// Creates a new empty metadata info (all fields absent by default). + pub fn new() -> Self { + Self::default() + } + + /// Sets whether the max supply can be updated by the owner via + /// `optional_set_max_supply`. If `false` (default), the max supply is immutable. + pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { + self.max_supply_mutable = mutable; + self + } + + /// Sets the name metadata (2 Words = 8 felts). + /// + /// Encoding is not specified; for human-readable UTF-8 text use + /// [`with_name_utf8`](Info::with_name_utf8). + pub fn with_name(mut self, name: [Word; 2]) -> Self { + self.name = Some(name); + self + } + + /// Sets the name from a UTF-8 string (at most [`NAME_UTF8_MAX_BYTES`] bytes). + pub fn with_name_utf8(mut self, s: &str) -> Result { + self.name = Some(name_from_utf8(s)?); + Ok(self) + } + + /// Sets the description metadata (6 Words) with mutability flag. + /// + /// `flag`: 1 = present+immutable, 2 = present+mutable. + pub fn with_description(mut self, description: [Word; 6], flag: u8) -> Self { + assert!(flag == 1 || flag == 2, "description flag must be 1 or 2"); + self.description = Some(description); + self.description_flag = flag; + self + } + + /// Sets the description from a byte slice (at most [`FIELD_MAX_BYTES`] bytes). + pub fn with_description_from_bytes( + mut self, + bytes: &[u8], + flag: u8, + ) -> Result { + assert!(flag == 1 || flag == 2, "description flag must be 1 or 2"); + self.description = Some(field_from_bytes(bytes)?); + self.description_flag = flag; + Ok(self) + } + + /// Sets the logo URI metadata (6 Words) with mutability flag. + /// + /// `flag`: 1 = present+immutable, 2 = present+mutable. + pub fn with_logo_uri(mut self, logo_uri: [Word; 6], flag: u8) -> Self { + assert!(flag == 1 || flag == 2, "logo_uri flag must be 1 or 2"); + self.logo_uri = Some(logo_uri); + self.logo_uri_flag = flag; + self + } + + /// Sets the logo URI from a byte slice (at most [`FIELD_MAX_BYTES`] bytes). + pub fn with_logo_uri_from_bytes( + mut self, + bytes: &[u8], + flag: u8, + ) -> Result { + assert!(flag == 1 || flag == 2, "logo_uri flag must be 1 or 2"); + self.logo_uri = Some(field_from_bytes(bytes)?); + self.logo_uri_flag = flag; + Ok(self) + } + + /// Sets the external link metadata (6 Words) with mutability flag. + /// + /// `flag`: 1 = present+immutable, 2 = present+mutable. + pub fn with_external_link(mut self, external_link: [Word; 6], flag: u8) -> Self { + assert!(flag == 1 || flag == 2, "external_link flag must be 1 or 2"); + self.external_link = Some(external_link); + self.external_link_flag = flag; + self + } + + /// Sets the external link from a byte slice (at most [`FIELD_MAX_BYTES`] bytes). + pub fn with_external_link_from_bytes( + mut self, + bytes: &[u8], + flag: u8, + ) -> Result { + assert!(flag == 1 || flag == 2, "external_link flag must be 1 or 2"); + self.external_link = Some(field_from_bytes(bytes)?); + self.external_link_flag = flag; + Ok(self) + } + + /// Returns the slot name for name chunk 0. + pub fn name_chunk_0_slot() -> &'static StorageSlotName { + &NAME_SLOTS[0] + } + + /// Returns the slot name for name chunk 1. + pub fn name_chunk_1_slot() -> &'static StorageSlotName { + &NAME_SLOTS[1] + } + + /// Returns the slot name for a description chunk by index (0..6). + pub fn description_slot(index: usize) -> &'static StorageSlotName { + &DESCRIPTION_SLOTS[index] + } + + /// Returns the slot name for a logo URI chunk by index (0..6). + pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName { + &LOGO_URI_SLOTS[index] + } + + /// Returns the slot name for an external link chunk by index (0..6). + pub fn external_link_slot(index: usize) -> &'static StorageSlotName { + &EXTERNAL_LINK_SLOTS[index] + } + + /// Reads the name and optional metadata fields from account storage. + /// + /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if + /// at least one word is non-zero. + pub fn read_metadata_from_storage( + storage: &AccountStorage, + ) -> (Option<[Word; 2]>, Option<[Word; 6]>, Option<[Word; 6]>, Option<[Word; 6]>) { + // Read name + let name = if let (Ok(chunk_0), Ok(chunk_1)) = ( + storage.get_item(Info::name_chunk_0_slot()), + storage.get_item(Info::name_chunk_1_slot()), + ) { + let name: [Word; 2] = [chunk_0, chunk_1]; + if name != [Word::default(); 2] { Some(name) } else { None } + } else { + None + }; + + let read_field = |slots: &[StorageSlotName; 6]| -> Option<[Word; 6]> { + let mut field = [Word::default(); 6]; + let mut any_set = false; + for (i, slot) in field.iter_mut().enumerate() { + if let Ok(chunk) = storage.get_item(&slots[i]) { + *slot = chunk; + if chunk != Word::default() { + any_set = true; + } + } + } + if any_set { Some(field) } else { None } + }; + + let description = read_field(&DESCRIPTION_SLOTS); + let logo_uri = read_field(&LOGO_URI_SLOTS); + let external_link = read_field(&EXTERNAL_LINK_SLOTS); + + (name, description, logo_uri, external_link) + } +} + +impl From for AccountComponent { + fn from(extension: Info) -> Self { + let mut storage_slots: Vec = Vec::new(); + + if let Some(name) = extension.name { + storage_slots.push(StorageSlot::with_value(Info::name_chunk_0_slot().clone(), name[0])); + storage_slots.push(StorageSlot::with_value(Info::name_chunk_1_slot().clone(), name[1])); + } + + // Config word: [desc_flag, logo_flag, extlink_flag, max_supply_mutable] + let config_word = Word::from([ + Felt::from(extension.description_flag as u32), + Felt::from(extension.logo_uri_flag as u32), + Felt::from(extension.external_link_flag as u32), + Felt::from(extension.max_supply_mutable as u32), + ]); + storage_slots.push(StorageSlot::with_value(config_slot().clone(), config_word)); + + // Description slots (always write 6 slots if flag > 0) + let description = extension.description.unwrap_or([Word::default(); 6]); + if extension.description_flag > 0 { + for (i, word) in description.iter().enumerate() { + storage_slots + .push(StorageSlot::with_value(Info::description_slot(i).clone(), *word)); + } + } + + // Logo URI slots + let logo_uri = extension.logo_uri.unwrap_or([Word::default(); 6]); + if extension.logo_uri_flag > 0 { + for (i, word) in logo_uri.iter().enumerate() { + storage_slots + .push(StorageSlot::with_value(Info::logo_uri_slot(i).clone(), *word)); + } + } + + // External link slots + let external_link = extension.external_link.unwrap_or([Word::default(); 6]); + if extension.external_link_flag > 0 { + for (i, word) in external_link.iter().enumerate() { + storage_slots + .push(StorageSlot::with_value(Info::external_link_slot(i).clone(), *word)); + } + } + + let metadata = AccountComponentMetadata::new("miden::standards::metadata::info") + .with_description( + "Metadata info (name, config, description, logo URI, external link) in fixed value slots", + ) + .with_supports_all_types(); + + AccountComponent::new(metadata_info_component_library(), storage_slots, metadata) + .expect("Info component should satisfy the requirements") + } +} + +// SCHEMA COMMITMENT COMPONENT +// ================================================================================================ + /// An [`AccountComponent`] exposing the account storage schema commitment. /// /// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`], @@ -161,9 +694,162 @@ mod tests { use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; - use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment}; + use super::{ + AccountBuilderSchemaCommitmentExt, + AccountSchemaCommitment, + FIELD_MAX_BYTES, + FieldBytesError, + Info, + NAME_UTF8_MAX_BYTES, + NameUtf8Error, + config_slot, + field_from_bytes, + name_from_utf8, + name_to_utf8, + }; use crate::account::auth::{AuthSingleSig, NoAuth}; + #[test] + fn metadata_info_can_store_name_and_description() { + let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; + let description = [ + Word::from([10u32, 11, 12, 13]), + Word::from([14u32, 15, 16, 17]), + Word::from([18u32, 19, 20, 21]), + Word::from([22u32, 23, 24, 25]), + Word::from([26u32, 27, 28, 29]), + Word::from([30u32, 31, 32, 33]), + ]; + + let extension = Info::new().with_name(name).with_description(description, 1); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build() + .unwrap(); + + // Verify name chunks + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name[0]); + assert_eq!(name_1, name[1]); + + // Verify description chunks + for (i, expected) in description.iter().enumerate() { + let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + } + + #[test] + fn metadata_info_empty_works() { + let extension = Info::new(); + + let _account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build() + .unwrap(); + } + + #[test] + fn config_slot_set_correctly() { + use miden_protocol::Felt; + + // Info with description mutable, max_supply_mutable = true + let info = Info::new() + .with_description([Word::default(); 6], 2) + .with_max_supply_mutable(true); + let account = AccountBuilder::new([2u8; 32]) + .with_auth_component(NoAuth) + .with_component(info) + .build() + .unwrap(); + let word = account.storage().get_item(config_slot()).unwrap(); + assert_eq!(word[0], Felt::from(2u32), "desc_flag should be 2"); + assert_eq!(word[1], Felt::from(0u32), "logo_flag should be 0"); + assert_eq!(word[2], Felt::from(0u32), "extlink_flag should be 0"); + assert_eq!(word[3], Felt::from(1u32), "max_supply_mutable should be 1"); + + // Info with defaults (all flags 0) + let account_default = AccountBuilder::new([3u8; 32]) + .with_auth_component(NoAuth) + .with_component(Info::new()) + .build() + .unwrap(); + let word_default = account_default.storage().get_item(config_slot()).unwrap(); + assert_eq!(word_default[0], Felt::from(0u32), "desc_flag should be 0 by default"); + assert_eq!(word_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); + } + + #[test] + fn name_utf8_roundtrip() { + let s = "POL Faucet"; + let words = name_from_utf8(s).unwrap(); + let decoded = name_to_utf8(&words).unwrap(); + assert_eq!(decoded, s); + } + + #[test] + fn name_utf8_max_32_bytes_accepted() { + let s = "a".repeat(NAME_UTF8_MAX_BYTES); + assert_eq!(s.len(), 32); + let words = name_from_utf8(&s).unwrap(); + let decoded = name_to_utf8(&words).unwrap(); + assert_eq!(decoded, s); + } + + #[test] + fn name_utf8_too_long_errors() { + let s = "a".repeat(33); + assert!(matches!(name_from_utf8(&s), Err(NameUtf8Error::TooLong(33)))); + } + + #[test] + fn field_192_bytes_accepted() { + let bytes = [0u8; FIELD_MAX_BYTES]; + let words = field_from_bytes(&bytes).unwrap(); + assert_eq!(words.len(), 6); + } + + #[test] + fn field_193_bytes_rejected() { + let bytes = [0u8; 193]; + assert!(matches!(field_from_bytes(&bytes), Err(FieldBytesError::TooLong(193)))); + } + + #[test] + fn metadata_info_with_name_utf8() { + let extension = Info::new().with_name_utf8("My Token").unwrap(); + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build() + .unwrap(); + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let decoded = name_to_utf8(&[name_0, name_1]).unwrap(); + assert_eq!(decoded, "My Token"); + } + + #[test] + fn metadata_info_name_only_works() { + let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; + let extension = Info::new().with_name(name); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build() + .unwrap(); + + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name[0]); + assert_eq!(name_1, name[1]); + } + #[test] fn storage_schema_commitment_is_order_independent() { let toml_a = r#" diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index b06430f07d..ab3da3534a 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -46,7 +46,8 @@ use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, OutputNote, TransactionKernel}; use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word}; -use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; +use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet, TokenName}; +use miden_standards::account::metadata::Info; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote}; use miden_standards::testing::account_component::MockAccountComponent; @@ -305,11 +306,13 @@ impl MockChainBuilder { token_symbol: &str, max_supply: u64, ) -> anyhow::Result { + let name = TokenName::try_from(token_symbol) + .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; let basic_faucet = - BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt) + BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt, name, None, None, None) .context("failed to create BasicFungibleFaucet")?; let account_builder = AccountBuilder::new(self.rng.random()) @@ -333,11 +336,12 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; + let name = TokenName::try_from(token_symbol) + .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let basic_faucet = - BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply) + BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, name, None, None, None) .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) .context("failed to create basic fungible faucet")?; @@ -361,6 +365,8 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; + let name = TokenName::try_from(token_symbol) + .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; @@ -369,19 +375,85 @@ impl MockChainBuilder { DEFAULT_FAUCET_DECIMALS, max_supply, owner_account_id, + name, + None, + None, + None, ) .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) .context("failed to create network fungible faucet")?; + let info = Info::new().with_name(name.as_words()); + let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) .with_component(network_faucet) + .with_component(info) .account_type(AccountType::FungibleFaucet); // Network faucets always use IncrNonce auth (no authentication) self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) } + /// Adds an existing network fungible faucet account with a metadata Info component + /// (for testing metadata::fungible procedures: owner can update description / logo_uri / + /// external_link / max supply when mutable). + #[allow(clippy::too_many_arguments)] + pub fn add_existing_network_faucet_with_metadata_info( + &mut self, + token_symbol: &str, + max_supply: u64, + owner_account_id: AccountId, + token_supply: Option, + max_supply_mutable: bool, + description: Option<([Word; 6], u8)>, + logo_uri: Option<([Word; 6], u8)>, + external_link: Option<([Word; 6], u8)>, + ) -> anyhow::Result { + let max_supply = Felt::try_from(max_supply) + .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?; + let token_supply = Felt::try_from(token_supply.unwrap_or(0)) + .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?; + let name = TokenName::try_from(token_symbol) + .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); + let token_symbol = + TokenSymbol::new(token_symbol).context("failed to create token symbol")?; + + let network_faucet = NetworkFungibleFaucet::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply, + owner_account_id, + name, + None, + None, + None, + ) + .and_then(|f| f.with_token_supply(token_supply)) + .context("failed to create network fungible faucet")?; + + let mut info = Info::new() + .with_name(name.as_words()) + .with_max_supply_mutable(max_supply_mutable); + if let Some((words, flag)) = description { + info = info.with_description(words, flag); + } + if let Some((words, flag)) = logo_uri { + info = info.with_logo_uri(words, flag); + } + if let Some((words, flag)) = external_link { + info = info.with_external_link(words, flag); + } + + let account_builder = AccountBuilder::new(self.rng.random()) + .storage_mode(AccountStorageMode::Network) + .with_component(network_faucet) + .with_component(info) + .account_type(AccountType::FungibleFaucet); + + self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) + } + /// Creates a new public account with an [`MockAccountComponent`] and registers the /// authenticator (if any). pub fn create_new_mock_account(&mut self, auth_method: Auth) -> anyhow::Result { diff --git a/crates/miden-testing/tests/lib.rs b/crates/miden-testing/tests/lib.rs index f664213429..3a8b1141e9 100644 --- a/crates/miden-testing/tests/lib.rs +++ b/crates/miden-testing/tests/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; // TODO(bele): Deactivated - to be upgraded later. // mod agglayer; mod auth; +mod metadata; mod scripts; mod wallet; diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs new file mode 100644 index 0000000000..361318ee4e --- /dev/null +++ b/crates/miden-testing/tests/metadata.rs @@ -0,0 +1,1663 @@ +//! Integration tests for the Metadata Extension component. + +extern crate alloc; + +use alloc::sync::Arc; + +use miden_processor::crypto::RpoRandomCoin; +use miden_protocol::account::{ + AccountBuilder, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, +}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::{Felt, Word}; +use miden_standards::account::auth::NoAuth; +use miden_standards::account::faucets::TokenName; +use miden_standards::account::metadata::{ + DESCRIPTION_DATA_KEY, + FieldBytesError, + Info, + NAME_UTF8_MAX_BYTES, + config_slot, + field_from_bytes, +}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::{ + ERR_DESCRIPTION_NOT_MUTABLE, + ERR_MAX_SUPPLY_IMMUTABLE, + ERR_SENDER_NOT_OWNER, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{MockChain, TransactionContextBuilder, assert_transaction_executor_error}; + +/// Tests that the metadata extension can store and retrieve name via MASM. +#[tokio::test] +async fn metadata_info_get_name_from_masm() -> anyhow::Result<()> { + let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; + + let extension = Info::new().with_name(name); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build()?; + + // MASM script to read name and verify values + let tx_script = format!( + r#" + begin + # Get name (returns [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)]) + call.::miden::standards::metadata::fungible::get_name + # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] + + # Verify chunk 0 (on top) + push.{expected_name_0} + assert_eqw.err="name chunk 0 does not match" + # => [NAME_CHUNK_1, pad(12)] + + # Verify chunk 1 + push.{expected_name_1} + assert_eqw.err="name chunk 1 does not match" + # => [pad(16)] + end + "#, + expected_name_0 = name[0], + expected_name_1 = name[1], + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Tests that reading zero-valued name returns empty words. +#[tokio::test] +async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { + // Create extension with zero-valued name (slots exist, but contain zeros) + let name = [Word::default(), Word::default()]; + let extension = Info::new().with_name(name); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build()?; + + let tx_script = r#" + begin + call.::miden::standards::metadata::fungible::get_name + # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] + padw assert_eqw.err="name chunk 0 should be empty" + padw assert_eqw.err="name chunk 1 should be empty" + end + "# + .to_string(); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Tests that get_description (first word of 6) can be read via MASM. +#[tokio::test] +async fn metadata_info_get_description_from_masm() -> anyhow::Result<()> { + let description = [ + Word::from([10u32, 11, 12, 13]), + Word::from([14u32, 15, 16, 17]), + Word::from([18u32, 19, 20, 21]), + Word::from([22u32, 23, 24, 25]), + Word::from([26u32, 27, 28, 29]), + Word::from([30u32, 31, 32, 33]), + ]; + + let extension = Info::new().with_description(description, 1); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build()?; + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_description + # => [DESCRIPTION_0, pad(12)] + + push.{expected_0} + assert_eqw.err="description_0 does not match" + end + "#, + expected_0 = description[0], + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Tests that the metadata extension works alongside a fungible faucet. +#[test] +fn metadata_info_with_faucet_storage() { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let name = [Word::from([111u32, 222, 333, 444]), Word::from([555u32, 666, 777, 888])]; + let description = [ + Word::from([10u32, 20, 30, 40]), + Word::from([50u32, 60, 70, 80]), + Word::from([90u32, 100, 110, 120]), + Word::from([130u32, 140, 150, 160]), + Word::from([170u32, 180, 190, 200]), + Word::from([210u32, 220, 230, 240]), + ]; + + let faucet = BasicFungibleFaucet::new( + "TST".try_into().unwrap(), + 8, // decimals + Felt::new(1_000_000), // max_supply + TokenName::try_from("TST").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let extension = Info::new().with_name(name).with_description(description, 1); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .with_component(extension) + .build() + .unwrap(); + + // Verify faucet metadata is intact (Word layout: [token_supply, max_supply, decimals, symbol]) + let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata[0], Felt::new(0)); // token_supply + assert_eq!(faucet_metadata[1], Felt::new(1_000_000)); // max_supply + assert_eq!(faucet_metadata[2], Felt::new(8)); // decimals + + // Verify name + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name[0]); + assert_eq!(name_1, name[1]); + + // Verify description + for (i, expected) in description.iter().enumerate() { + let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } +} + +/// Tests that a name at the maximum allowed length (32 bytes, 2 slots) is accepted. +#[test] +fn name_32_bytes_accepted() { + let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); + let extension = Info::new().with_name_utf8(&max_name).unwrap(); + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build() + .unwrap(); + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let decoded = miden_standards::account::metadata::name_to_utf8(&[name_0, name_1]).unwrap(); + assert_eq!(decoded, max_name); +} + +/// Tests that a name longer than the maximum (33 bytes) is rejected. +#[test] +fn name_33_bytes_rejected() { + let too_long = "a".repeat(33); + let result = Info::new().with_name_utf8(&too_long); + assert!(result.is_err()); + assert!(matches!( + result, + Err(miden_standards::account::metadata::NameUtf8Error::TooLong(33)) + )); +} + +/// Tests that description at full capacity (6 Words) is supported. +#[test] +fn description_6_words_full_capacity() { + let description = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let extension = Info::new().with_description(description, 1); + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build() + .unwrap(); + for (i, expected) in description.iter().enumerate() { + let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } +} + +/// Tests that field longer than 192 bytes (193 bytes) is rejected. +#[test] +fn field_193_bytes_rejected() { + let result = field_from_bytes(&[0u8; 193]); + assert!(result.is_err()); + assert!(matches!(result, Err(FieldBytesError::TooLong(193)))); +} + +/// Tests that BasicFungibleFaucet with Info component (name/description) works correctly. +#[test] +fn faucet_with_integrated_metadata() { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let name = [Word::from([11u32, 22, 33, 44]), Word::from([55u32, 66, 77, 88])]; + let description = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + + let faucet = BasicFungibleFaucet::new( + "INT".try_into().unwrap(), + 6, // decimals + Felt::new(500_000), // max_supply + TokenName::try_from("INT").unwrap(), + None, + None, + None, + ) + .unwrap(); + let extension = Info::new().with_name(name).with_description(description, 1); + + let account = AccountBuilder::new([2u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .with_component(extension) + .build() + .unwrap(); + + // Verify faucet metadata is intact (Word layout: [token_supply, max_supply, decimals, symbol]) + let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata[0], Felt::new(0)); // token_supply + assert_eq!(faucet_metadata[1], Felt::new(500_000)); // max_supply + assert_eq!(faucet_metadata[2], Felt::new(6)); // decimals + + // Verify name + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name[0]); + assert_eq!(name_1, name[1]); + + // Verify description + for (i, expected) in description.iter().enumerate() { + let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + + // Verify the faucet can be recovered from the account (metadata only; name/desc are in Info) + let recovered_faucet = BasicFungibleFaucet::try_from(&account).unwrap(); + assert_eq!(recovered_faucet.max_supply(), Felt::new(500_000)); + assert_eq!(recovered_faucet.decimals(), 6); +} + +/// Tests initializing a fungible faucet with maximum-length name and full description. +#[test] +fn faucet_initialized_with_max_name_and_full_description() { + use miden_protocol::account::AccountStorageMode; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let max_name = "0".repeat(NAME_UTF8_MAX_BYTES); + let description = [ + Word::from([101u32, 102, 103, 104]), + Word::from([105u32, 106, 107, 108]), + Word::from([109u32, 110, 111, 112]), + Word::from([113u32, 114, 115, 116]), + Word::from([117u32, 118, 119, 120]), + Word::from([121u32, 122, 123, 124]), + ]; + + let faucet = BasicFungibleFaucet::new( + "MAX".try_into().unwrap(), + 6, + Felt::new(1_000_000), + TokenName::try_from("MAX").unwrap(), + None, + None, + None, + ) + .unwrap(); + let extension = + Info::new().with_name_utf8(&max_name).unwrap().with_description(description, 1); + + let account = AccountBuilder::new([5u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .with_component(extension) + .build() + .unwrap(); + + let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); + for (i, expected) in description.iter().enumerate() { + let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata[1], Felt::new(1_000_000)); +} + +/// Tests initializing a network fungible faucet with max name and full description. +#[test] +fn network_faucet_initialized_with_max_name_and_full_description() { + use miden_protocol::account::AccountStorageMode; + use miden_standards::account::faucets::NetworkFungibleFaucet; + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); + let description = [ + Word::from([201u32, 202, 203, 204]), + Word::from([205u32, 206, 207, 208]), + Word::from([209u32, 210, 211, 212]), + Word::from([213u32, 214, 215, 216]), + Word::from([217u32, 218, 219, 220]), + Word::from([221u32, 222, 223, 224]), + ]; + + let network_faucet = NetworkFungibleFaucet::new( + "NET".try_into().unwrap(), + 6, + Felt::new(2_000_000), + owner_account_id, + TokenName::try_from("NET").unwrap(), + None, + None, + None, + ) + .unwrap() + .with_token_supply(Felt::new(0)) + .unwrap(); + + let extension = + Info::new().with_name_utf8(&max_name).unwrap().with_description(description, 1); + + let account = AccountBuilder::new([6u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Network) + .with_auth_component(NoAuth) + .with_component(network_faucet) + .with_component(extension) + .build() + .unwrap(); + + let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); + for (i, expected) in description.iter().enumerate() { + let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + let faucet_metadata = + account.storage().get_item(NetworkFungibleFaucet::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata[1], Felt::new(2_000_000)); +} + +/// Tests that a network fungible faucet with description can be read from MASM. +#[tokio::test] +async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<()> { + use miden_protocol::account::AccountStorageMode; + use miden_standards::account::faucets::NetworkFungibleFaucet; + + let owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let max_name = "b".repeat(NAME_UTF8_MAX_BYTES); + let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + let description = [ + Word::from([301u32, 302, 303, 304]), + Word::from([305u32, 306, 307, 308]), + Word::from([309u32, 310, 311, 312]), + Word::from([313u32, 314, 315, 316]), + Word::from([317u32, 318, 319, 320]), + Word::from([321u32, 322, 323, 324]), + ]; + + let network_faucet = NetworkFungibleFaucet::new( + "MAS".try_into().unwrap(), + 6, + Felt::new(1_000_000), + owner_account_id, + TokenName::try_from("MAS").unwrap(), + None, + None, + None, + ) + .unwrap() + .with_token_supply(Felt::new(0)) + .unwrap(); + + let extension = + Info::new().with_name_utf8(&max_name).unwrap().with_description(description, 1); + + let account = AccountBuilder::new([7u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Network) + .with_auth_component(NoAuth) + .with_component(network_faucet) + .with_component(extension) + .build()?; + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{expected_name_0} + assert_eqw.err="network faucet name chunk 0 does not match" + push.{expected_name_1} + assert_eqw.err="network faucet name chunk 1 does not match" + + call.::miden::standards::metadata::fungible::get_description + # => [DESCRIPTION_0, pad(12)] + push.{expected_desc_0} + assert_eqw.err="network faucet description_0 does not match" + end + "#, + expected_name_0 = name_words[0], + expected_name_1 = name_words[1], + expected_desc_0 = description[0], + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_decimals. +#[tokio::test] +async fn faucet_get_decimals_only() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + + let faucet = BasicFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + let expected_decimals = Felt::from(decimals).as_int(); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_decimals + push.{expected_decimals} + assert_eq.err="decimals does not match" + push.0 + assert_eq.err="clean stack: pad must be 0" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_token_symbol. +#[tokio::test] +async fn faucet_get_token_symbol_only() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + + let faucet = BasicFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + let expected_symbol = Felt::from(token_symbol).as_int(); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_symbol + push.{expected_symbol} + assert_eq.err="token_symbol does not match" + push.0 + assert_eq.err="clean stack: pad must be 0" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_token_supply. +#[tokio::test] +async fn faucet_get_token_supply_only() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + let token_supply = Felt::new(0); // initial supply + + let faucet = BasicFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + let expected_token_supply = token_supply.as_int(); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_supply + push.{expected_token_supply} + assert_eq.err="token_supply does not match" + push.0 + assert_eq.err="clean stack: pad must be 0" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_token_metadata (full word). +#[tokio::test] +async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + + let faucet = BasicFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + let expected_symbol = Felt::from(token_symbol).as_int(); + let expected_decimals = Felt::from(decimals).as_int(); + let expected_max_supply = max_supply.as_int(); + let expected_token_supply = 0u64; + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_metadata + # => [token_symbol, decimals, max_supply, token_supply, pad(12)] + push.{expected_symbol} assert_eq.err="token_symbol does not match" + push.{expected_decimals} assert_eq.err="decimals does not match" + push.{expected_max_supply} assert_eq.err="max_supply does not match" + push.{expected_token_supply} assert_eq.err="token_supply does not match" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_name. +#[tokio::test] +async fn metadata_get_name_only() -> anyhow::Result<()> { + let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; + let extension = Info::new().with_name(name); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build()?; + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{expected_name_0} + assert_eqw.err="name chunk 0 does not match" + push.{expected_name_1} + assert_eqw.err="name chunk 1 does not match" + end + "#, + expected_name_0 = name[0], + expected_name_1 = name[1], + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_description. +#[tokio::test] +async fn metadata_get_description_only() -> anyhow::Result<()> { + let description = [ + Word::from([10u32, 11, 12, 13]), + Word::from([14u32, 15, 16, 17]), + Word::from([18u32, 19, 20, 21]), + Word::from([22u32, 23, 24, 25]), + Word::from([26u32, 27, 28, 29]), + Word::from([30u32, 31, 32, 33]), + ]; + let extension = Info::new().with_description(description, 1); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build()?; + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_description + # => [DESCRIPTION_0, pad(12)] + push.{expected_0} + assert_eqw.err="description_0 does not match" + end + "#, + expected_0 = description[0], + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_config. +#[tokio::test] +async fn metadata_get_config_only() -> anyhow::Result<()> { + let extension = Info::new() + .with_description([Word::default(); 6], 2) // desc_flag=2 (mutable) + .with_max_supply_mutable(true); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(extension) + .build()?; + + let tx_script = r#" + begin + call.::miden::standards::metadata::fungible::get_config + # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + push.1 + assert_eq.err="max_supply_mutable should be 1" + push.0 + assert_eq.err="extlink_flag should be 0" + push.0 + assert_eq.err="logo_flag should be 0" + push.2 + assert_eq.err="desc_flag should be 2" + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_owner (account must have ownable, e.g. NetworkFungibleFaucet). +#[tokio::test] +async fn metadata_get_owner_only() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::NetworkFungibleFaucet; + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + miden_protocol::account::AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + + let faucet = NetworkFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + owner_account_id, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + let expected_prefix = owner_account_id.prefix().as_felt().as_int(); + let expected_suffix = owner_account_id.suffix().as_int(); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_owner + # => [owner_prefix, owner_suffix, pad(14)] + push.{expected_prefix} + assert_eq.err="owner prefix does not match" + push.{expected_suffix} + assert_eq.err="owner suffix does not match" + push.0 + assert_eq.err="clean stack: pad must be 0" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Isolated test: only get_max_supply. +#[tokio::test] +async fn faucet_get_max_supply_only() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + + let faucet = BasicFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + let expected_max_supply = max_supply.as_int(); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_max_supply + push.{expected_max_supply} + assert_eq.err="max_supply does not match" + push.0 + assert_eq.err="clean stack: pad must be 0" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Tests that get_decimals and get_token_symbol return the correct individual values from MASM. +#[tokio::test] +async fn faucet_get_decimals_and_symbol_from_masm() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_protocol::asset::TokenSymbol; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let token_symbol = TokenSymbol::new("POL").unwrap(); + let decimals: u8 = 8; + let max_supply = Felt::new(1_000_000); + + let faucet = BasicFungibleFaucet::new( + token_symbol, + decimals, + max_supply, + TokenName::try_from("POL").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .build()?; + + // Compute expected felt values + let expected_decimals = Felt::from(decimals).as_int(); + let expected_symbol = Felt::from(token_symbol).as_int(); + let expected_max_supply = max_supply.as_int(); + + let tx_script = format!( + r#" + begin + # Test get_decimals + call.::miden::standards::metadata::fungible::get_decimals + # => [decimals, pad(15)] + push.{expected_decimals} + assert_eq.err="decimals does not match" + # => [pad(15)]; pad to 16 before next call + push.0 + + # Test get_token_symbol + call.::miden::standards::metadata::fungible::get_token_symbol + # => [token_symbol, pad(15)] + push.{expected_symbol} + assert_eq.err="token_symbol does not match" + # => [pad(15)]; pad to 16 before next call + push.0 + + # Test get_max_supply (sanity check) + call.::miden::standards::metadata::fungible::get_max_supply + # => [max_supply, pad(15)] + push.{expected_max_supply} + assert_eq.err="max_supply does not match" + end + "#, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Tests that BasicFungibleFaucet metadata can be read from MASM using the faucet's procedures. +#[tokio::test] +async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { + use miden_protocol::Felt; + use miden_protocol::account::AccountStorageMode; + use miden_standards::account::faucets::BasicFungibleFaucet; + + let name = [Word::from([100u32, 200, 300, 400]), Word::from([500u32, 600, 700, 800])]; + let description = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + + let faucet = BasicFungibleFaucet::new( + "MAS".try_into().unwrap(), + 10, // decimals + Felt::new(999_999), // max_supply + TokenName::try_from("MAS").unwrap(), + None, + None, + None, + ) + .unwrap(); + let extension = Info::new().with_name(name).with_description(description, 1); + + let account = AccountBuilder::new([3u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet) + .with_component(extension) + .build()?; + + // MASM script to read name and description via the metadata procedures and verify + let tx_script = format!( + r#" + begin + # Get name and verify + call.::miden::standards::metadata::fungible::get_name + # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] + push.{expected_name_0} + assert_eqw.err="faucet name chunk 0 does not match" + push.{expected_name_1} + assert_eqw.err="faucet name chunk 1 does not match" + + # Get description and verify first chunk + call.::miden::standards::metadata::fungible::get_description + # => [DESCRIPTION_0, pad(12)] + push.{expected_desc_0} + assert_eqw.err="faucet description_0 does not match" + end + "#, + expected_name_0 = name[0], + expected_name_1 = name[1], + expected_desc_0 = description[0], + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +// ================================================================================================= +// optional_set_description: mutable flag and verify_owner +// ================================================================================================= + +/// Builds the advice map value for field setters. +fn field_advice_map_value(field: &[Word; 6]) -> Vec { + let mut value = Vec::with_capacity(24); + for word in field.iter() { + value.extend(word.iter()); + } + value +} + +/// When description flag is 1 (immutable), optional_set_description panics. +#[tokio::test] +async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let description = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "DSC", + 1000, + owner_account_id, + Some(0), + false, + Some((description, 1)), // flag=1 → immutable + None, + None, + )?; + let mock_chain = builder.build()?; + + let new_desc = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let tx_script = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_description + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .extend_advice_map([(DESCRIPTION_DATA_KEY, field_advice_map_value(&new_desc))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_DESCRIPTION_NOT_MUTABLE); + + Ok(()) +} + +/// When description flag is 2 and note sender is the owner, optional_set_description succeeds. +#[tokio::test] +async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_desc = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let new_desc = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "DSC", + 1000, + owner_account_id, + Some(0), + false, + Some((initial_desc, 2)), // flag=2 → mutable + None, + None, + )?; + let mock_chain = builder.build()?; + + let committed = mock_chain.committed_account(faucet.id())?; + let config_word = committed.storage().get_item(config_slot())?; + assert_eq!( + config_word[0], + Felt::from(2u32), + "committed account must have desc_flag = 2" + ); + + let set_desc_note_script_code = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_description + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_desc_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(set_desc_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let set_desc_note = NoteBuilder::new(owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([7, 8, 9, 10u32])) + .code(set_desc_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_desc_note])? + .add_note_script(set_desc_note_script) + .extend_advice_map([(DESCRIPTION_DATA_KEY, field_advice_map_value(&new_desc))]) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + for (i, expected) in new_desc.iter().enumerate() { + let chunk = updated_faucet.storage().get_item(Info::description_slot(i))?; + assert_eq!(chunk, *expected, "description_{i} should be updated"); + } + + Ok(()) +} + +/// When description flag is 2 but note sender is not the owner, optional_set_description panics. +#[tokio::test] +async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let non_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_desc = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let new_desc = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "DSC", + 1000, + owner_account_id, + Some(0), + false, + Some((initial_desc, 2)), + None, + None, + )?; + let mock_chain = builder.build()?; + + let set_desc_note_script_code = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_description + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_desc_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(set_desc_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let set_desc_note = NoteBuilder::new(non_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([11, 12, 13, 14u32])) + .code(set_desc_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_desc_note])? + .add_note_script(set_desc_note_script) + .extend_advice_map([(DESCRIPTION_DATA_KEY, field_advice_map_value(&new_desc))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} + +// ================================================================================================= +// optional_set_max_supply: mutable flag and verify_owner +// ================================================================================================= + +/// When max_supply_mutable is 0 (immutable), optional_set_max_supply panics. +#[tokio::test] +async fn optional_set_max_supply_immutable_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner_account_id, + Some(0), + false, // max_supply_mutable = false + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let new_max_supply: u64 = 2000; + let tx_script = format!( + r#" + begin + push.{new_max_supply} + call.::miden::standards::metadata::fungible::optional_set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_MAX_SUPPLY_IMMUTABLE); + + Ok(()) +} + +/// When max_supply_mutable is 1 and note sender is the owner, optional_set_max_supply succeeds. +#[tokio::test] +async fn optional_set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { + use miden_standards::account::faucets::NetworkFungibleFaucet; + + let mut builder = MockChain::builder(); + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_max_supply: u64 = 1000; + let new_max_supply: u64 = 2000; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + initial_max_supply, + owner_account_id, + Some(0), + true, // max_supply_mutable = true + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let set_max_supply_note_script_code = format!( + r#" + begin + push.{new_max_supply} + swap drop + call.::miden::standards::metadata::fungible::optional_set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_max_supply_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(&set_max_supply_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let set_max_supply_note = NoteBuilder::new(owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([20, 21, 22, 23u32])) + .code(&set_max_supply_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_max_supply_note])? + .add_note_script(set_max_supply_note_script) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + // Verify the metadata word: [token_supply, max_supply, decimals, symbol] + let metadata_word = updated_faucet.storage().get_item(NetworkFungibleFaucet::metadata_slot())?; + assert_eq!( + metadata_word[1], + Felt::new(new_max_supply), + "max_supply should be updated to {new_max_supply}" + ); + // token_supply should remain 0 + assert_eq!(metadata_word[0], Felt::new(0), "token_supply should remain unchanged"); + + Ok(()) +} + +/// When max_supply_mutable is 1 but note sender is not the owner, optional_set_max_supply panics. +#[tokio::test] +async fn optional_set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let non_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner_account_id, + Some(0), + true, // max_supply_mutable = true + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let new_max_supply: u64 = 2000; + let set_max_supply_note_script_code = format!( + r#" + begin + push.{new_max_supply} + swap drop + call.::miden::standards::metadata::fungible::optional_set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_max_supply_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(&set_max_supply_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let set_max_supply_note = NoteBuilder::new(non_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([30, 31, 32, 33u32])) + .code(&set_max_supply_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_max_supply_note])? + .add_note_script(set_max_supply_note_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} + +// ================================================================================================= +// is_max_supply_mutable: getter test +// ================================================================================================= + +/// Tests that all is_*_mutable procedures correctly read the config flags. +/// Each field is tested with flag=2 (mutable, expects 1) and flag=1 (immutable, expects 0). +/// Also tests is_max_supply_mutable with true (expects 1). +#[tokio::test] +async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { + let data = field_from_bytes(b"test").unwrap(); + + let cases: Vec<(Info, &str, u8)> = vec![ + (Info::new().with_max_supply_mutable(true), "is_max_supply_mutable", 1), + (Info::new().with_description(data, 2), "is_description_mutable", 1), + (Info::new().with_description(data, 1), "is_description_mutable", 0), + (Info::new().with_logo_uri(data, 2), "is_logo_uri_mutable", 1), + (Info::new().with_logo_uri(data, 1), "is_logo_uri_mutable", 0), + (Info::new().with_external_link(data, 2), "is_external_link_mutable", 1), + (Info::new().with_external_link(data, 1), "is_external_link_mutable", 0), + ]; + + for (info, proc_name, expected) in cases { + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(info) + .build()?; + + let tx_script = format!( + "begin + call.::miden::standards::metadata::fungible::{proc_name} + push.{expected} + assert_eq.err=\"{proc_name} returned unexpected value\" + end" + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(&tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + } + + Ok(()) +} From 728e1452bed132436893ef6d763ad956003dc6e6 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 26 Feb 2026 12:19:59 -0300 Subject: [PATCH 02/56] fix: fixing after rebase. --- crates/miden-agglayer/src/lib.rs | 6 ++++-- .../miden-standards/src/account/faucets/token_metadata.rs | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 21070e557b..3f2d3065ba 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -22,7 +22,7 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata}; +use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata, TokenName}; use miden_utils_sync::LazyLock; pub mod b2agg_note; @@ -356,7 +356,9 @@ impl AggLayerFaucet { origin_network: u32, scale: u8, ) -> Result { - let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply)?; + // Use empty name for agglayer faucets (name is stored in Info component, not here). + let name = TokenName::try_from("").expect("empty string is valid"); + let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply, name, None, None, None)?; Ok(Self { metadata, bridge_account_id, diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 5053a2fac3..fa06878cd9 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -5,9 +5,6 @@ use miden_protocol::{Felt, Word}; use super::FungibleFaucetError; use crate::account::metadata::{self, FieldBytesError, NameUtf8Error}; -/// Schema type ID for the token symbol field in token metadata storage schema. -pub const TOKEN_SYMBOL_TYPE_ID: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; - // TOKEN NAME // ================================================================================================ From d0eae16903e1d633c12cbf487ce4176f1e52df0e Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 26 Feb 2026 13:01:35 -0300 Subject: [PATCH 03/56] refactor: improve formatting and readability in various files --- crates/miden-agglayer/src/lib.rs | 11 +++++++++- .../src/account/components/mod.rs | 3 ++- .../src/account/faucets/token_metadata.rs | 19 +++++----------- .../src/account/metadata/mod.rs | 22 +++++++------------ 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 3f2d3065ba..0e77885810 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -358,7 +358,16 @@ impl AggLayerFaucet { ) -> Result { // Use empty name for agglayer faucets (name is stored in Info component, not here). let name = TokenName::try_from("").expect("empty string is valid"); - let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply, name, None, None, None)?; + let metadata = TokenMetadata::with_supply( + symbol, + decimals, + max_supply, + token_supply, + name, + None, + None, + None, + )?; Ok(Self { metadata, bridge_account_id, diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 62f106ea27..3d4f7a0efc 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -89,7 +89,8 @@ static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed") }); -// Metadata Info component uses the standards library (get_name, get_description, etc. from metadata). +// Metadata Info component uses the standards library (get_name, get_description, etc. from +// metadata). static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = LazyLock::new(|| Library::from(StandardsLib::default())); diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index fa06878cd9..9ed95f6071 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -434,16 +434,9 @@ mod tests { let name = TokenName::try_from("polygon").unwrap(); let description = Description::try_from("A polygon token").unwrap(); - let metadata = TokenMetadata::new( - symbol, - decimals, - max_supply, - name, - Some(description), - None, - None, - ) - .unwrap(); + let metadata = + TokenMetadata::new(symbol, decimals, max_supply, name, Some(description), None, None) + .unwrap(); assert_eq!(metadata.symbol(), symbol); assert_eq!(metadata.name(), &name); @@ -507,8 +500,7 @@ mod tests { let max_supply = Felt::new(1_000_000); let name = TokenName::try_from("TEST").unwrap(); - let result = - TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); + let result = TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. }))); } @@ -522,8 +514,7 @@ mod tests { let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1); let name = TokenName::try_from("TEST").unwrap(); - let result = - TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); + let result = TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. }))); } diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 616ca79860..c8d301e1ef 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -222,18 +222,12 @@ pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 6], FieldBytesError> { /// Description (6 Words = 24 felts), split across 6 slots. pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { [ - StorageSlotName::new("miden::standards::metadata::description_0") - .expect("valid slot name"), - StorageSlotName::new("miden::standards::metadata::description_1") - .expect("valid slot name"), - StorageSlotName::new("miden::standards::metadata::description_2") - .expect("valid slot name"), - StorageSlotName::new("miden::standards::metadata::description_3") - .expect("valid slot name"), - StorageSlotName::new("miden::standards::metadata::description_4") - .expect("valid slot name"), - StorageSlotName::new("miden::standards::metadata::description_5") - .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_0").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_1").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_2").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_3").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_4").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_5").expect("valid slot name"), ] }); @@ -457,6 +451,7 @@ impl Info { /// /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if /// at least one word is non-zero. + #[allow(clippy::type_complexity)] pub fn read_metadata_from_storage( storage: &AccountStorage, ) -> (Option<[Word; 2]>, Option<[Word; 6]>, Option<[Word; 6]>, Option<[Word; 6]>) { @@ -524,8 +519,7 @@ impl From for AccountComponent { let logo_uri = extension.logo_uri.unwrap_or([Word::default(); 6]); if extension.logo_uri_flag > 0 { for (i, word) in logo_uri.iter().enumerate() { - storage_slots - .push(StorageSlot::with_value(Info::logo_uri_slot(i).clone(), *word)); + storage_slots.push(StorageSlot::with_value(Info::logo_uri_slot(i).clone(), *word)); } } From 146d102cd796cec4925386527c603e973f14622e Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 26 Feb 2026 16:03:23 -0300 Subject: [PATCH 04/56] Refactor metadata handling in fungible faucets - Updated the `create_basic_fungible_faucet` and `create_network_fungible_faucet` functions to use boolean flags for description, logo URI, and external link mutability instead of integer flags. - Modified the `Info` struct to replace the previous flag system with separate boolean fields for initialized and mutable states. - Adjusted storage layout documentation to reflect changes in metadata configuration. - Updated tests to align with the new boolean flag system for mutability and initialization. - Ensured backward compatibility by updating mock chain builder and test cases to use the new structure. --- .../faucets/basic_fungible_faucet.masm | 6 +- .../faucets/network_fungible_faucet.masm | 6 +- .../asm/standards/metadata/fungible.masm | 217 ++++++++++++------ .../src/account/faucets/basic_fungible.rs | 6 +- .../src/account/faucets/network_fungible.rs | 6 +- .../src/account/metadata/mod.rs | 203 +++++++++------- .../src/mock_chain/chain_builder.rs | 33 +-- crates/miden-testing/tests/metadata.rs | 92 ++++---- 8 files changed, 360 insertions(+), 209 deletions(-) diff --git a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index c73217e179..8d8d3abf99 100644 --- a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -7,8 +7,12 @@ pub use ::miden::standards::faucets::basic_fungible::burn # Metadata — getters only (no setters for basic faucet) pub use ::miden::standards::metadata::fungible::get_name -pub use ::miden::standards::metadata::fungible::get_config +pub use ::miden::standards::metadata::fungible::get_initialized_config +pub use ::miden::standards::metadata::fungible::get_mutability_config pub use ::miden::standards::metadata::fungible::is_max_supply_mutable +pub use ::miden::standards::metadata::fungible::is_description_initialized +pub use ::miden::standards::metadata::fungible::is_logo_uri_initialized +pub use ::miden::standards::metadata::fungible::is_external_link_initialized pub use ::miden::standards::metadata::fungible::is_description_mutable pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable pub use ::miden::standards::metadata::fungible::is_external_link_mutable diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index 8148e978d6..0699567fe4 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -10,8 +10,12 @@ pub use ::miden::standards::faucets::network_fungible::renounce_ownership # Metadata — all from metadata::fungible pub use ::miden::standards::metadata::fungible::get_owner pub use ::miden::standards::metadata::fungible::get_name -pub use ::miden::standards::metadata::fungible::get_config +pub use ::miden::standards::metadata::fungible::get_initialized_config +pub use ::miden::standards::metadata::fungible::get_mutability_config pub use ::miden::standards::metadata::fungible::is_max_supply_mutable +pub use ::miden::standards::metadata::fungible::is_description_initialized +pub use ::miden::standards::metadata::fungible::is_logo_uri_initialized +pub use ::miden::standards::metadata::fungible::is_external_link_initialized pub use ::miden::standards::metadata::fungible::is_description_mutable pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable pub use ::miden::standards::metadata::fungible::is_external_link_mutable diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index 3bdb12a60c..c24005aa09 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -15,7 +15,8 @@ use miden::standards::access::ownable const TOKEN_METADATA_SLOT = word("miden::standards::metadata::token_metadata") const NAME_CHUNK_0_SLOT = word("miden::standards::metadata::name_0") const NAME_CHUNK_1_SLOT = word("miden::standards::metadata::name_1") -const CONFIG_SLOT = word("miden::standards::metadata::config") +const INITIALIZED_CONFIG_SLOT = word("miden::standards::metadata::initialized_config") +const MUTABILITY_CONFIG_SLOT = word("miden::standards::metadata::mutability_config") const DESCRIPTION_0_SLOT = word("miden::standards::metadata::description_0") const DESCRIPTION_1_SLOT = word("miden::standards::metadata::description_1") @@ -80,10 +81,19 @@ proc get_name_chunk_1 exec.active_account::get_item end -#! Loads the config word. Output: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)]. +#! Loads the initialized config word. +#! Output: [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)]. #! (word[3] on top after get_item) -proc get_config_word - push.CONFIG_SLOT[0..2] +proc get_initialized_config_word + push.INITIALIZED_CONFIG_SLOT[0..2] + exec.active_account::get_item +end + +#! Loads the mutability config word. +#! Output: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)]. +#! (word[3] on top after get_item) +proc get_mutability_config_word + push.MUTABILITY_CONFIG_SLOT[0..2] exec.active_account::get_item end @@ -245,83 +255,152 @@ pub proc get_name end # ================================================================================================= -# CONFIG — [desc_flag, logo_flag, extlink_flag, max_supply_mutable] +# INITIALIZED CONFIG — [desc_init, logo_init, extlink_init, max_supply_mutable] # ================================================================================================= -#! Returns the config word. +#! Returns the initialized config word. #! #! After get_item, stack has word[3] on top: -#! [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] +#! [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] #! #! Inputs: [pad(16)] -#! Outputs: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] +#! Outputs: [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] #! #! Invocation: call -pub proc get_config - exec.get_config_word +pub proc get_initialized_config + exec.get_initialized_config_word swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] end +# ================================================================================================= +# MUTABILITY CONFIG — [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable] +# ================================================================================================= + +#! Returns the mutability config word. +#! +#! After get_item, stack has word[3] on top: +#! [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] +#! +#! Inputs: [pad(16)] +#! Outputs: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] +#! +#! Invocation: call +pub proc get_mutability_config + exec.get_mutability_config_word + swapw dropw + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] +end + +# ================================================================================================= +# INITIALIZED CHECKS — read initialized_config +# ================================================================================================= + +#! Returns whether description is initialized (1 or 0). +#! +#! Reads initialized_config word felt[0] (desc_init). +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_description_initialized + exec.get_initialized_config_word + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] + drop drop drop + # => [desc_init, pad(12)] +end + +#! Returns whether logo URI is initialized (1 or 0). +#! +#! Reads initialized_config word felt[1] (logo_init). +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_logo_uri_initialized + exec.get_initialized_config_word + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] + drop drop swap drop + # => [logo_init, pad(12)] +end + +#! Returns whether external link is initialized (1 or 0). +#! +#! Reads initialized_config word felt[2] (extlink_init). +#! +#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call +pub proc is_external_link_initialized + exec.get_initialized_config_word + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] + swapw dropw + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] + drop movdn.2 drop drop + # => [extlink_init, pad(12)] +end + +# ================================================================================================= +# MUTABILITY CHECKS — read mutability_config +# ================================================================================================= + #! Returns whether max supply is mutable (single felt: 0 or 1). #! -#! Reads config word felt[3] (max_supply_mutable), which is on top after get_item. +#! Reads mutability_config word felt[3] (max_supply_mutable), which is on top after get_item. #! #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_max_supply_mutable - exec.get_config_word - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_mutability_config_word + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] movdn.3 drop drop drop # => [max_supply_mutable, pad(15)] end -#! Returns whether description is mutable (flag == 2). +#! Returns whether description is mutable (flag == 1). #! -#! Reads config word felt[0] (desc_flag). Returns 1 if mutable, 0 otherwise. +#! Reads mutability_config word felt[0] (desc_mutable). Returns 1 if mutable, 0 otherwise. #! #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_description_mutable - exec.get_config_word - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_mutability_config_word + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] drop drop drop - # => [desc_flag, pad(12)] - push.2 eq + # => [desc_mutable, pad(12)] + push.1 eq # => [is_mutable, pad(12)] end -#! Returns whether logo URI is mutable (flag == 2). +#! Returns whether logo URI is mutable (flag == 1). #! -#! Reads config word felt[1] (logo_flag). Returns 1 if mutable, 0 otherwise. +#! Reads mutability_config word felt[1] (logo_mutable). Returns 1 if mutable, 0 otherwise. #! #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_logo_uri_mutable - exec.get_config_word - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_mutability_config_word + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] drop drop swap drop - # => [logo_flag, pad(12)] - push.2 eq + # => [logo_mutable, pad(12)] + push.1 eq # => [is_mutable, pad(12)] end -#! Returns whether external link is mutable (flag == 2). +#! Returns whether external link is mutable (flag == 1). #! -#! Reads config word felt[2] (extlink_flag). Returns 1 if mutable, 0 otherwise. +#! Reads mutability_config word felt[2] (extlink_mutable). Returns 1 if mutable, 0 otherwise. #! #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_external_link_mutable - exec.get_config_word - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] + exec.get_mutability_config_word + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] drop movdn.2 drop drop - # => [extlink_flag, pad(12)] - push.2 eq + # => [extlink_mutable, pad(12)] + push.1 eq # => [is_mutable, pad(12)] end @@ -455,10 +534,10 @@ pub proc get_external_link end # ================================================================================================= -# OPTIONAL SET DESCRIPTION (owner-only when desc_flag == 2) +# OPTIONAL SET DESCRIPTION (owner-only when desc_mutable == 1) # ================================================================================================= -#! Updates the description (6 Words) if the description flag is 2 (present+mutable) +#! Updates the description (6 Words) if the description mutability flag is 1 #! and the note sender is the owner. #! #! Before executing the transaction, populate the advice map: @@ -469,19 +548,19 @@ end #! Outputs: [pad(16)] #! #! Panics if: -#! - the description flag is not 2 (not present+mutable). +#! - the description mutability flag is not 1. #! - the note sender is not the owner. #! #! Invocation: call (from note script context) pub proc optional_set_description - # Read config word: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] - exec.get_config_word + # Read mutability config word: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] - # desc_flag is at bottom of the 4 elements; drop the top 3 + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] + # desc_mutable is at bottom of the 4 elements; drop the top 3 drop drop drop - # => [desc_flag, pad(15)] - push.2 eq + # => [desc_mutable, pad(15)] + push.1 eq assert.err=ERR_DESCRIPTION_NOT_MUTABLE # => [pad(16)] @@ -521,10 +600,10 @@ pub proc optional_set_description end # ================================================================================================= -# OPTIONAL SET LOGO URI (owner-only when logo_flag == 2) +# OPTIONAL SET LOGO URI (owner-only when logo_mutable == 1) # ================================================================================================= -#! Updates the logo URI (6 Words) if the logo URI flag is 2 (present+mutable) +#! Updates the logo URI (6 Words) if the logo URI mutability flag is 1 #! and the note sender is the owner. #! #! Before executing the transaction, populate the advice map: @@ -535,21 +614,21 @@ end #! Outputs: [pad(16)] #! #! Panics if: -#! - the logo URI flag is not 2 (not present+mutable). +#! - the logo URI mutability flag is not 1. #! - the note sender is not the owner. #! #! Invocation: call (from note script context) pub proc optional_set_logo_uri - # Read config word: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] - exec.get_config_word + # Read mutability config word: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] - # logo_flag is word[1]; drop top 2 (max_supply_mutable, extlink_flag), then swap+drop + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] + # logo_mutable is word[1]; drop top 2 (max_supply_mutable, extlink_mutable), then swap+drop drop drop - # => [logo_flag, desc_flag, pad(14)] + # => [logo_mutable, desc_mutable, pad(14)] swap drop - # => [logo_flag, pad(15)] - push.2 eq + # => [logo_mutable, pad(15)] + push.1 eq assert.err=ERR_LOGO_URI_NOT_MUTABLE # => [pad(16)] @@ -585,10 +664,10 @@ pub proc optional_set_logo_uri end # ================================================================================================= -# OPTIONAL SET EXTERNAL LINK (owner-only when extlink_flag == 2) +# OPTIONAL SET EXTERNAL LINK (owner-only when extlink_mutable == 1) # ================================================================================================= -#! Updates the external link (6 Words) if the external link flag is 2 (present+mutable) +#! Updates the external link (6 Words) if the external link mutability flag is 1 #! and the note sender is the owner. #! #! Before executing the transaction, populate the advice map: @@ -599,21 +678,21 @@ end #! Outputs: [pad(16)] #! #! Panics if: -#! - the external link flag is not 2 (not present+mutable). +#! - the external link mutability flag is not 1. #! - the note sender is not the owner. #! #! Invocation: call (from note script context) pub proc optional_set_external_link - # Read config word: [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(16)] - exec.get_config_word + # Read mutability config word: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] - # extlink_flag is word[2]; drop top 1 (max_supply_mutable), then move down and drop + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] + # extlink_mutable is word[2]; drop top 1 (max_supply_mutable), then move down and drop drop - # => [extlink_flag, logo_flag, desc_flag, pad(13)] + # => [extlink_mutable, logo_mutable, desc_mutable, pad(13)] movdn.2 drop drop - # => [extlink_flag, pad(15)] - push.2 eq + # => [extlink_mutable, pad(15)] + push.1 eq assert.err=ERR_EXTERNAL_LINK_NOT_MUTABLE # => [pad(16)] @@ -665,12 +744,12 @@ end #! #! Invocation: call (from note script context) pub proc optional_set_max_supply - # 1. Check mutable flag from config word - exec.get_config_word - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, new_max_supply, pad(15)] + # 1. Check mutable flag from mutability config word + exec.get_mutability_config_word + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, new_max_supply, pad(15)] push.1 eq assert.err=ERR_MAX_SUPPLY_IMMUTABLE - # => [extlink_flag, logo_flag, desc_flag, new_max_supply, pad(15)] + # => [extlink_mutable, logo_mutable, desc_mutable, new_max_supply, pad(15)] drop drop drop # => [new_max_supply, pad(15)] diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 4c8e5b5fca..148a21c57d 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -346,13 +346,13 @@ pub fn create_basic_fungible_faucet( let mut info = Info::new().with_name(name.as_words()); if let Some(d) = &description { - info = info.with_description(d.as_words(), 1); + info = info.with_description(d.as_words(), false); } if let Some(l) = &logo_uri { - info = info.with_logo_uri(l.as_words(), 1); + info = info.with_logo_uri(l.as_words(), false); } if let Some(e) = &external_link { - info = info.with_external_link(e.as_words(), 1); + info = info.with_external_link(e.as_words(), false); } let account = AccountBuilder::new(init_seed) diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index e6ceff29e6..87b3f13fbc 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -384,13 +384,13 @@ pub fn create_network_fungible_faucet( let mut info = Info::new().with_name(name.as_words()); if let Some(d) = &description { - info = info.with_description(d.as_words(), 1); + info = info.with_description(d.as_words(), false); } if let Some(l) = &logo_uri { - info = info.with_logo_uri(l.as_words(), 1); + info = info.with_logo_uri(l.as_words(), false); } if let Some(e) = &external_link { - info = info.with_external_link(e.as_words(), 1); + info = info.with_external_link(e.as_words(), false); } let account = AccountBuilder::new(init_seed) diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index c8d301e1ef..4bb804c1fb 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -1,7 +1,8 @@ -//! Account / contract / faucet metadata (slots 0..22) +//! Account / contract / faucet metadata (slots 0..23) //! //! All of the following are metadata of the account (or faucet): token_symbol, decimals, -//! max_supply, owner, name, config, description, logo URI, and external link. +//! max_supply, owner, name, initialized_config, mutability_config, description, logo URI, +//! and external link. //! //! ## Storage layout //! @@ -11,7 +12,8 @@ //! | `ownable::owner_config` | owner account id (defined by ownable module) | //! | `metadata::name_0` | first 4 felts of name | //! | `metadata::name_1` | last 4 felts of name | -//! | `metadata::config` | `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]` | +//! | `metadata::initialized_config` | `[desc_init, logo_init, extlink_init, max_supply_mutable]` | +//! | `metadata::mutability_config` | `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` | //! | `metadata::description_0..5` | description (6 Words, ~192 bytes) | //! | `metadata::logo_uri_0..5` | logo URI (6 Words, ~192 bytes) | //! | `metadata::external_link_0..5` | external link (6 Words, ~192 bytes) | @@ -23,17 +25,17 @@ //! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. //! Any change to slot indices or names must be applied in both Rust and MASM. //! -//! ## Config Word +//! ## Config Words //! -//! The config Word stores per-field flags and the max_supply_mutable flag: -//! `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]` +//! Two config Words store per-field boolean flags: //! -//! Each flag uses 3 states: -//! - `0` = field not present -//! - `1` = field present, immutable -//! - `2` = field present, mutable (owner can update) +//! **initialized_config**: `[desc_init, logo_init, extlink_init, max_supply_mutable]` +//! - Each flag is 0 (not initialized) or 1 (initialized). //! -//! `max_supply_mutable` uses 0/1 (always present when the faucet exists). +//! **mutability_config**: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +//! - Each flag is 0 (immutable) or 1 (mutable / owner can update). +//! +//! `max_supply_mutable` appears in both words for convenient access. //! //! ## MASM modules //! @@ -57,8 +59,8 @@ //! //! let info = Info::new() //! .with_name([name_word_0, name_word_1]) -//! .with_description([d0, d1, d2, d3, d4, d5], 2) // present + mutable -//! .with_logo_uri([l0, l1, l2, l3, l4, l5], 1); // present + immutable +//! .with_description([d0, d1, d2, d3, d4, d5], true) // initialized + mutable +//! .with_logo_uri([l0, l1, l2, l3, l4, l5], false); // initialized + immutable //! //! let account = AccountBuilder::new(seed) //! .with_component(info) @@ -170,12 +172,21 @@ pub fn name_to_utf8(words: &[Word; 2]) -> Result { String::from_utf8(bytes[..len].to_vec()).map_err(|_| NameUtf8Error::InvalidUtf8) } -/// Config slot: `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]`. +/// Initialized config slot: `[desc_init, logo_init, extlink_init, max_supply_mutable]`. +/// +/// Each flag is 0 (not initialized) or 1 (initialized). +/// `max_supply_mutable` appears here for convenient access. +pub static INITIALIZED_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::initialized_config") + .expect("storage slot name should be valid") +}); + +/// Mutability config slot: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]`. /// -/// Each flag is 0 (not present), 1 (present+immutable), or 2 (present+mutable). -/// `max_supply_mutable` is 0 or 1. -pub static CONFIG_SLOT: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::metadata::config") +/// Each flag is 0 (immutable) or 1 (mutable / owner can update). +/// `max_supply_mutable` appears here for convenient access. +pub static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::metadata::mutability_config") .expect("storage slot name should be valid") }); @@ -295,9 +306,14 @@ pub fn owner_config_slot() -> &'static StorageSlotName { &OWNER_CONFIG_SLOT } -/// Returns the [`StorageSlotName`] for the config Word. -pub fn config_slot() -> &'static StorageSlotName { - &CONFIG_SLOT +/// Returns the [`StorageSlotName`] for the initialized config Word. +pub fn initialized_config_slot() -> &'static StorageSlotName { + &INITIALIZED_CONFIG_SLOT +} + +/// Returns the [`StorageSlotName`] for the mutability config Word. +pub fn mutability_config_slot() -> &'static StorageSlotName { + &MUTABILITY_CONFIG_SLOT } // INFO COMPONENT @@ -308,19 +324,20 @@ pub fn config_slot() -> &'static StorageSlotName { /// ## Storage Layout /// /// - Slot 2–3: name (2 Words = 8 felts) -/// - Slot 4: config `[desc_flag, logo_flag, extlink_flag, max_supply_mutable]` -/// - Slot 5–10: description (6 Words) -/// - Slot 11–16: logo_uri (6 Words) -/// - Slot 17–22: external_link (6 Words) +/// - Slot 4: initialized_config `[desc_init, logo_init, extlink_init, max_supply_mutable]` +/// - Slot 5: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +/// - Slot 6–11: description (6 Words) +/// - Slot 12–17: logo_uri (6 Words) +/// - Slot 18–23: external_link (6 Words) #[derive(Debug, Clone, Default)] pub struct Info { name: Option<[Word; 2]>, - /// Description flag: 0=not present, 1=present+immutable, 2=present+mutable. - description_flag: u8, - /// Logo URI flag: 0=not present, 1=present+immutable, 2=present+mutable. - logo_uri_flag: u8, - /// External link flag: 0=not present, 1=present+immutable, 2=present+mutable. - external_link_flag: u8, + /// Whether the description field is mutable (owner can update). + description_mutable: bool, + /// Whether the logo URI field is mutable (owner can update). + logo_uri_mutable: bool, + /// Whether the external link field is mutable (owner can update). + external_link_mutable: bool, /// If true (1), the owner may call optional_set_max_supply. If false (0), immutable. max_supply_mutable: bool, description: Option<[Word; 6]>, @@ -356,13 +373,13 @@ impl Info { Ok(self) } - /// Sets the description metadata (6 Words) with mutability flag. + /// Sets the description metadata (6 Words) with mutability. /// - /// `flag`: 1 = present+immutable, 2 = present+mutable. - pub fn with_description(mut self, description: [Word; 6], flag: u8) -> Self { - assert!(flag == 1 || flag == 2, "description flag must be 1 or 2"); + /// When `mutable` is `true`, the owner can update the description later. + /// The field is always marked as initialized when data is provided. + pub fn with_description(mut self, description: [Word; 6], mutable: bool) -> Self { self.description = Some(description); - self.description_flag = flag; + self.description_mutable = mutable; self } @@ -370,21 +387,20 @@ impl Info { pub fn with_description_from_bytes( mut self, bytes: &[u8], - flag: u8, + mutable: bool, ) -> Result { - assert!(flag == 1 || flag == 2, "description flag must be 1 or 2"); self.description = Some(field_from_bytes(bytes)?); - self.description_flag = flag; + self.description_mutable = mutable; Ok(self) } - /// Sets the logo URI metadata (6 Words) with mutability flag. + /// Sets the logo URI metadata (6 Words) with mutability. /// - /// `flag`: 1 = present+immutable, 2 = present+mutable. - pub fn with_logo_uri(mut self, logo_uri: [Word; 6], flag: u8) -> Self { - assert!(flag == 1 || flag == 2, "logo_uri flag must be 1 or 2"); + /// When `mutable` is `true`, the owner can update the logo URI later. + /// The field is always marked as initialized when data is provided. + pub fn with_logo_uri(mut self, logo_uri: [Word; 6], mutable: bool) -> Self { self.logo_uri = Some(logo_uri); - self.logo_uri_flag = flag; + self.logo_uri_mutable = mutable; self } @@ -392,21 +408,20 @@ impl Info { pub fn with_logo_uri_from_bytes( mut self, bytes: &[u8], - flag: u8, + mutable: bool, ) -> Result { - assert!(flag == 1 || flag == 2, "logo_uri flag must be 1 or 2"); self.logo_uri = Some(field_from_bytes(bytes)?); - self.logo_uri_flag = flag; + self.logo_uri_mutable = mutable; Ok(self) } - /// Sets the external link metadata (6 Words) with mutability flag. + /// Sets the external link metadata (6 Words) with mutability. /// - /// `flag`: 1 = present+immutable, 2 = present+mutable. - pub fn with_external_link(mut self, external_link: [Word; 6], flag: u8) -> Self { - assert!(flag == 1 || flag == 2, "external_link flag must be 1 or 2"); + /// When `mutable` is `true`, the owner can update the external link later. + /// The field is always marked as initialized when data is provided. + pub fn with_external_link(mut self, external_link: [Word; 6], mutable: bool) -> Self { self.external_link = Some(external_link); - self.external_link_flag = flag; + self.external_link_mutable = mutable; self } @@ -414,11 +429,10 @@ impl Info { pub fn with_external_link_from_bytes( mut self, bytes: &[u8], - flag: u8, + mutable: bool, ) -> Result { - assert!(flag == 1 || flag == 2, "external_link flag must be 1 or 2"); self.external_link = Some(field_from_bytes(bytes)?); - self.external_link_flag = flag; + self.external_link_mutable = mutable; Ok(self) } @@ -497,18 +511,36 @@ impl From for AccountComponent { storage_slots.push(StorageSlot::with_value(Info::name_chunk_1_slot().clone(), name[1])); } - // Config word: [desc_flag, logo_flag, extlink_flag, max_supply_mutable] - let config_word = Word::from([ - Felt::from(extension.description_flag as u32), - Felt::from(extension.logo_uri_flag as u32), - Felt::from(extension.external_link_flag as u32), + let desc_initialized = extension.description.is_some(); + let logo_initialized = extension.logo_uri.is_some(); + let extlink_initialized = extension.external_link.is_some(); + + // Initialized config word: [desc_init, logo_init, extlink_init, max_supply_mutable] + let initialized_config_word = Word::from([ + Felt::from(desc_initialized as u32), + Felt::from(logo_initialized as u32), + Felt::from(extlink_initialized as u32), + Felt::from(extension.max_supply_mutable as u32), + ]); + storage_slots.push(StorageSlot::with_value( + initialized_config_slot().clone(), + initialized_config_word, + )); + + // Mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable] + let mutability_config_word = Word::from([ + Felt::from(extension.description_mutable as u32), + Felt::from(extension.logo_uri_mutable as u32), + Felt::from(extension.external_link_mutable as u32), Felt::from(extension.max_supply_mutable as u32), ]); - storage_slots.push(StorageSlot::with_value(config_slot().clone(), config_word)); + storage_slots.push(StorageSlot::with_value( + mutability_config_slot().clone(), + mutability_config_word, + )); - // Description slots (always write 6 slots if flag > 0) - let description = extension.description.unwrap_or([Word::default(); 6]); - if extension.description_flag > 0 { + // Description slots (always write 6 slots if initialized) + if let Some(description) = extension.description { for (i, word) in description.iter().enumerate() { storage_slots .push(StorageSlot::with_value(Info::description_slot(i).clone(), *word)); @@ -516,16 +548,14 @@ impl From for AccountComponent { } // Logo URI slots - let logo_uri = extension.logo_uri.unwrap_or([Word::default(); 6]); - if extension.logo_uri_flag > 0 { + if let Some(logo_uri) = extension.logo_uri { for (i, word) in logo_uri.iter().enumerate() { storage_slots.push(StorageSlot::with_value(Info::logo_uri_slot(i).clone(), *word)); } } // External link slots - let external_link = extension.external_link.unwrap_or([Word::default(); 6]); - if extension.external_link_flag > 0 { + if let Some(external_link) = extension.external_link { for (i, word) in external_link.iter().enumerate() { storage_slots .push(StorageSlot::with_value(Info::external_link_slot(i).clone(), *word)); @@ -696,8 +726,9 @@ mod tests { Info, NAME_UTF8_MAX_BYTES, NameUtf8Error, - config_slot, field_from_bytes, + initialized_config_slot, + mutability_config_slot, name_from_utf8, name_to_utf8, }; @@ -715,7 +746,7 @@ mod tests { Word::from([30u32, 31, 32, 33]), ]; - let extension = Info::new().with_name(name).with_description(description, 1); + let extension = Info::new().with_name(name).with_description(description, false); let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) @@ -748,23 +779,32 @@ mod tests { } #[test] - fn config_slot_set_correctly() { + fn config_slots_set_correctly() { use miden_protocol::Felt; // Info with description mutable, max_supply_mutable = true let info = Info::new() - .with_description([Word::default(); 6], 2) + .with_description([Word::default(); 6], true) .with_max_supply_mutable(true); let account = AccountBuilder::new([2u8; 32]) .with_auth_component(NoAuth) .with_component(info) .build() .unwrap(); - let word = account.storage().get_item(config_slot()).unwrap(); - assert_eq!(word[0], Felt::from(2u32), "desc_flag should be 2"); - assert_eq!(word[1], Felt::from(0u32), "logo_flag should be 0"); - assert_eq!(word[2], Felt::from(0u32), "extlink_flag should be 0"); - assert_eq!(word[3], Felt::from(1u32), "max_supply_mutable should be 1"); + + // Check initialized config + let init_word = account.storage().get_item(initialized_config_slot()).unwrap(); + assert_eq!(init_word[0], Felt::from(1u32), "desc_init should be 1"); + assert_eq!(init_word[1], Felt::from(0u32), "logo_init should be 0"); + assert_eq!(init_word[2], Felt::from(0u32), "extlink_init should be 0"); + assert_eq!(init_word[3], Felt::from(1u32), "max_supply_mutable should be 1"); + + // Check mutability config + let mut_word = account.storage().get_item(mutability_config_slot()).unwrap(); + assert_eq!(mut_word[0], Felt::from(1u32), "desc_mutable should be 1"); + assert_eq!(mut_word[1], Felt::from(0u32), "logo_mutable should be 0"); + assert_eq!(mut_word[2], Felt::from(0u32), "extlink_mutable should be 0"); + assert_eq!(mut_word[3], Felt::from(1u32), "max_supply_mutable should be 1"); // Info with defaults (all flags 0) let account_default = AccountBuilder::new([3u8; 32]) @@ -772,9 +812,12 @@ mod tests { .with_component(Info::new()) .build() .unwrap(); - let word_default = account_default.storage().get_item(config_slot()).unwrap(); - assert_eq!(word_default[0], Felt::from(0u32), "desc_flag should be 0 by default"); - assert_eq!(word_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); + let init_default = account_default.storage().get_item(initialized_config_slot()).unwrap(); + assert_eq!(init_default[0], Felt::from(0u32), "desc_init should be 0 by default"); + assert_eq!(init_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); + let mut_default = account_default.storage().get_item(mutability_config_slot()).unwrap(); + assert_eq!(mut_default[0], Felt::from(0u32), "desc_mutable should be 0 by default"); + assert_eq!(mut_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); } #[test] diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index ab3da3534a..ac30e6201c 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -340,10 +340,17 @@ impl MockChainBuilder { .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let basic_faucet = - BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, name, None, None, None) - .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create basic fungible faucet")?; + let basic_faucet = BasicFungibleFaucet::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply, + name, + None, + None, + None, + ) + .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) + .context("failed to create basic fungible faucet")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) @@ -406,9 +413,9 @@ impl MockChainBuilder { owner_account_id: AccountId, token_supply: Option, max_supply_mutable: bool, - description: Option<([Word; 6], u8)>, - logo_uri: Option<([Word; 6], u8)>, - external_link: Option<([Word; 6], u8)>, + description: Option<([Word; 6], bool)>, + logo_uri: Option<([Word; 6], bool)>, + external_link: Option<([Word; 6], bool)>, ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply) .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?; @@ -435,14 +442,14 @@ impl MockChainBuilder { let mut info = Info::new() .with_name(name.as_words()) .with_max_supply_mutable(max_supply_mutable); - if let Some((words, flag)) = description { - info = info.with_description(words, flag); + if let Some((words, mutable)) = description { + info = info.with_description(words, mutable); } - if let Some((words, flag)) = logo_uri { - info = info.with_logo_uri(words, flag); + if let Some((words, mutable)) = logo_uri { + info = info.with_logo_uri(words, mutable); } - if let Some((words, flag)) = external_link { - info = info.with_external_link(words, flag); + if let Some((words, mutable)) = external_link { + info = info.with_external_link(words, mutable); } let account_builder = AccountBuilder::new(self.rng.random()) diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 361318ee4e..d95856800e 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -22,8 +22,8 @@ use miden_standards::account::metadata::{ FieldBytesError, Info, NAME_UTF8_MAX_BYTES, - config_slot, field_from_bytes, + mutability_config_slot, }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ @@ -131,7 +131,7 @@ async fn metadata_info_get_description_from_masm() -> anyhow::Result<()> { Word::from([30u32, 31, 32, 33]), ]; - let extension = Info::new().with_description(description, 1); + let extension = Info::new().with_description(description, false); let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) @@ -193,7 +193,7 @@ fn metadata_info_with_faucet_storage() { ) .unwrap(); - let extension = Info::new().with_name(name).with_description(description, 1); + let extension = Info::new().with_name(name).with_description(description, false); let account = AccountBuilder::new([1u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) @@ -262,7 +262,7 @@ fn description_6_words_full_capacity() { Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), ]; - let extension = Info::new().with_description(description, 1); + let extension = Info::new().with_description(description, false); let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) .with_component(extension) @@ -309,7 +309,7 @@ fn faucet_with_integrated_metadata() { None, ) .unwrap(); - let extension = Info::new().with_name(name).with_description(description, 1); + let extension = Info::new().with_name(name).with_description(description, false); let account = AccountBuilder::new([2u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) @@ -370,8 +370,10 @@ fn faucet_initialized_with_max_name_and_full_description() { None, ) .unwrap(); - let extension = - Info::new().with_name_utf8(&max_name).unwrap().with_description(description, 1); + let extension = Info::new() + .with_name_utf8(&max_name) + .unwrap() + .with_description(description, false); let account = AccountBuilder::new([5u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) @@ -432,8 +434,10 @@ fn network_faucet_initialized_with_max_name_and_full_description() { .with_token_supply(Felt::new(0)) .unwrap(); - let extension = - Info::new().with_name_utf8(&max_name).unwrap().with_description(description, 1); + let extension = Info::new() + .with_name_utf8(&max_name) + .unwrap() + .with_description(description, false); let account = AccountBuilder::new([6u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -496,8 +500,10 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( .with_token_supply(Felt::new(0)) .unwrap(); - let extension = - Info::new().with_name_utf8(&max_name).unwrap().with_description(description, 1); + let extension = Info::new() + .with_name_utf8(&max_name) + .unwrap() + .with_description(description, false); let account = AccountBuilder::new([7u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -828,7 +834,7 @@ async fn metadata_get_description_only() -> anyhow::Result<()> { Word::from([26u32, 27, 28, 29]), Word::from([30u32, 31, 32, 33]), ]; - let extension = Info::new().with_description(description, 1); + let extension = Info::new().with_description(description, false); let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) @@ -861,11 +867,11 @@ async fn metadata_get_description_only() -> anyhow::Result<()> { Ok(()) } -/// Isolated test: only get_config. +/// Isolated test: get_initialized_config and get_mutability_config. #[tokio::test] async fn metadata_get_config_only() -> anyhow::Result<()> { let extension = Info::new() - .with_description([Word::default(); 6], 2) // desc_flag=2 (mutable) + .with_description([Word::default(); 6], true) // mutable .with_max_supply_mutable(true); let account = AccountBuilder::new([1u8; 32]) @@ -875,16 +881,29 @@ async fn metadata_get_config_only() -> anyhow::Result<()> { let tx_script = r#" begin - call.::miden::standards::metadata::fungible::get_config - # => [max_supply_mutable, extlink_flag, logo_flag, desc_flag, pad(12)] + # Check initialized config + call.::miden::standards::metadata::fungible::get_initialized_config + # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] + push.1 + assert_eq.err="max_supply_mutable should be 1" + push.0 + assert_eq.err="extlink_init should be 0" + push.0 + assert_eq.err="logo_init should be 0" + push.1 + assert_eq.err="desc_init should be 1" + + # Check mutability config + call.::miden::standards::metadata::fungible::get_mutability_config + # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] push.1 assert_eq.err="max_supply_mutable should be 1" push.0 - assert_eq.err="extlink_flag should be 0" + assert_eq.err="extlink_mutable should be 0" push.0 - assert_eq.err="logo_flag should be 0" - push.2 - assert_eq.err="desc_flag should be 2" + assert_eq.err="logo_mutable should be 0" + push.1 + assert_eq.err="desc_mutable should be 1" end "#; @@ -1133,7 +1152,7 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { None, ) .unwrap(); - let extension = Info::new().with_name(name).with_description(description, 1); + let extension = Info::new().with_name(name).with_description(description, false); let account = AccountBuilder::new([3u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) @@ -1218,7 +1237,7 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { owner_account_id, Some(0), false, - Some((description, 1)), // flag=1 → immutable + Some((description, false)), // immutable None, None, )?; @@ -1291,19 +1310,15 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> owner_account_id, Some(0), false, - Some((initial_desc, 2)), // flag=2 → mutable + Some((initial_desc, true)), // mutable None, None, )?; let mock_chain = builder.build()?; let committed = mock_chain.committed_account(faucet.id())?; - let config_word = committed.storage().get_item(config_slot())?; - assert_eq!( - config_word[0], - Felt::from(2u32), - "committed account must have desc_flag = 2" - ); + let mut_word = committed.storage().get_item(mutability_config_slot())?; + assert_eq!(mut_word[0], Felt::from(1u32), "committed account must have desc_mutable = 1"); let set_desc_note_script_code = r#" begin @@ -1383,7 +1398,7 @@ async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<() owner_account_id, Some(0), false, - Some((initial_desc, 2)), + Some((initial_desc, true)), None, None, )?; @@ -1624,12 +1639,12 @@ async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { let cases: Vec<(Info, &str, u8)> = vec![ (Info::new().with_max_supply_mutable(true), "is_max_supply_mutable", 1), - (Info::new().with_description(data, 2), "is_description_mutable", 1), - (Info::new().with_description(data, 1), "is_description_mutable", 0), - (Info::new().with_logo_uri(data, 2), "is_logo_uri_mutable", 1), - (Info::new().with_logo_uri(data, 1), "is_logo_uri_mutable", 0), - (Info::new().with_external_link(data, 2), "is_external_link_mutable", 1), - (Info::new().with_external_link(data, 1), "is_external_link_mutable", 0), + (Info::new().with_description(data, true), "is_description_mutable", 1), + (Info::new().with_description(data, false), "is_description_mutable", 0), + (Info::new().with_logo_uri(data, true), "is_logo_uri_mutable", 1), + (Info::new().with_logo_uri(data, false), "is_logo_uri_mutable", 0), + (Info::new().with_external_link(data, true), "is_external_link_mutable", 1), + (Info::new().with_external_link(data, false), "is_external_link_mutable", 0), ]; for (info, proc_name, expected) in cases { @@ -1647,9 +1662,8 @@ async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { ); let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()) - .compile_tx_script(&tx_script)?; + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(&tx_script)?; let tx_context = TransactionContextBuilder::new(account) .tx_script(tx_script) From 17359319a9c0314fcd2b0c4613adbf4a09cb531e Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Sat, 28 Feb 2026 19:06:26 -0300 Subject: [PATCH 05/56] refactor: update token metadata references in faucet tests to use FungibleTokenMetadata --- crates/miden-agglayer/src/lib.rs | 16 +- .../faucets/basic_fungible_faucet.masm | 7 +- .../faucets/network_fungible_faucet.masm | 7 +- .../asm/standards/metadata/fungible.masm | 312 +++--- .../src/account/components/mod.rs | 15 - .../src/account/faucets/basic_fungible.rs | 112 ++- .../src/account/faucets/mod.rs | 2 +- .../src/account/faucets/network_fungible.rs | 88 +- .../src/account/faucets/token_metadata.rs | 426 +++++--- .../src/account/interface/test.rs | 4 +- .../src/account/metadata/mod.rs | 511 ++++------ .../src/mock_chain/chain_builder.rs | 74 +- .../tests/agglayer/bridge_out.rs | 6 +- crates/miden-testing/tests/metadata.rs | 952 ++++++++++++++---- crates/miden-testing/tests/scripts/faucet.rs | 12 +- 15 files changed, 1609 insertions(+), 935 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 0e77885810..5215142e58 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -22,7 +22,7 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata, TokenName}; +use miden_standards::account::faucets::{FungibleFaucetError, FungibleTokenMetadata, TokenName}; use miden_utils_sync::LazyLock; pub mod b2agg_note; @@ -324,14 +324,14 @@ static CONVERSION_INFO_2_SLOT_NAME: LazyLock = LazyLock::new(|| /// /// ## Storage Layout /// -/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::metadata_slot`]: Stores [`FungibleTokenMetadata`]. /// - [`Self::bridge_account_id_slot`]: Stores the AggLayer bridge account ID. /// - [`Self::conversion_info_1_slot`]: Stores the first 4 felts of the origin token address. /// - [`Self::conversion_info_2_slot`]: Stores the remaining 5th felt of the origin token address + /// origin network + scale. #[derive(Debug, Clone)] pub struct AggLayerFaucet { - metadata: TokenMetadata, + metadata: FungibleTokenMetadata, bridge_account_id: AccountId, origin_token_address: EthAddressFormat, origin_network: u32, @@ -343,7 +343,7 @@ impl AggLayerFaucet { /// /// # Errors /// Returns an error if: - /// - The decimals parameter exceeds maximum value of [`TokenMetadata::MAX_DECIMALS`]. + /// - The decimals parameter exceeds maximum value of [`FungibleTokenMetadata::MAX_DECIMALS`]. /// - The max supply exceeds maximum possible amount for a fungible asset. /// - The token supply exceeds the max supply. pub fn new( @@ -357,8 +357,8 @@ impl AggLayerFaucet { scale: u8, ) -> Result { // Use empty name for agglayer faucets (name is stored in Info component, not here). - let name = TokenName::try_from("").expect("empty string is valid"); - let metadata = TokenMetadata::with_supply( + let name = TokenName::new("").expect("empty string is valid"); + let metadata = FungibleTokenMetadata::with_supply( symbol, decimals, max_supply, @@ -386,9 +386,9 @@ impl AggLayerFaucet { Ok(self) } - /// Storage slot name for [`TokenMetadata`]. + /// Storage slot name for [`FungibleTokenMetadata`]. pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() + FungibleTokenMetadata::metadata_slot() } /// Storage slot name for the AggLayer bridge account ID. diff --git a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index 8d8d3abf99..adab99b10a 100644 --- a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -7,17 +7,16 @@ pub use ::miden::standards::faucets::basic_fungible::burn # Metadata — getters only (no setters for basic faucet) pub use ::miden::standards::metadata::fungible::get_name -pub use ::miden::standards::metadata::fungible::get_initialized_config pub use ::miden::standards::metadata::fungible::get_mutability_config pub use ::miden::standards::metadata::fungible::is_max_supply_mutable -pub use ::miden::standards::metadata::fungible::is_description_initialized -pub use ::miden::standards::metadata::fungible::is_logo_uri_initialized -pub use ::miden::standards::metadata::fungible::is_external_link_initialized pub use ::miden::standards::metadata::fungible::is_description_mutable pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable pub use ::miden::standards::metadata::fungible::is_external_link_mutable +pub use ::miden::standards::metadata::fungible::get_description_commitment pub use ::miden::standards::metadata::fungible::get_description +pub use ::miden::standards::metadata::fungible::get_logo_uri_commitment pub use ::miden::standards::metadata::fungible::get_logo_uri +pub use ::miden::standards::metadata::fungible::get_external_link_commitment pub use ::miden::standards::metadata::fungible::get_external_link pub use ::miden::standards::metadata::fungible::get_token_metadata pub use ::miden::standards::metadata::fungible::get_max_supply diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index 0699567fe4..1743a47d5a 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -10,17 +10,16 @@ pub use ::miden::standards::faucets::network_fungible::renounce_ownership # Metadata — all from metadata::fungible pub use ::miden::standards::metadata::fungible::get_owner pub use ::miden::standards::metadata::fungible::get_name -pub use ::miden::standards::metadata::fungible::get_initialized_config pub use ::miden::standards::metadata::fungible::get_mutability_config pub use ::miden::standards::metadata::fungible::is_max_supply_mutable -pub use ::miden::standards::metadata::fungible::is_description_initialized -pub use ::miden::standards::metadata::fungible::is_logo_uri_initialized -pub use ::miden::standards::metadata::fungible::is_external_link_initialized pub use ::miden::standards::metadata::fungible::is_description_mutable pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable pub use ::miden::standards::metadata::fungible::is_external_link_mutable +pub use ::miden::standards::metadata::fungible::get_description_commitment pub use ::miden::standards::metadata::fungible::get_description +pub use ::miden::standards::metadata::fungible::get_logo_uri_commitment pub use ::miden::standards::metadata::fungible::get_logo_uri +pub use ::miden::standards::metadata::fungible::get_external_link_commitment pub use ::miden::standards::metadata::fungible::get_external_link pub use ::miden::standards::metadata::fungible::get_token_metadata pub use ::miden::standards::metadata::fungible::get_max_supply diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index c24005aa09..653ac9fc4d 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -7,6 +7,8 @@ use miden::protocol::active_account use miden::protocol::native_account use miden::standards::access::ownable +use miden::core::crypto::hashes::rpo256 +use miden::core::mem # ================================================================================================= # CONSTANTS — slot names @@ -15,7 +17,6 @@ use miden::standards::access::ownable const TOKEN_METADATA_SLOT = word("miden::standards::metadata::token_metadata") const NAME_CHUNK_0_SLOT = word("miden::standards::metadata::name_0") const NAME_CHUNK_1_SLOT = word("miden::standards::metadata::name_1") -const INITIALIZED_CONFIG_SLOT = word("miden::standards::metadata::initialized_config") const MUTABILITY_CONFIG_SLOT = word("miden::standards::metadata::mutability_config") const DESCRIPTION_0_SLOT = word("miden::standards::metadata::description_0") @@ -81,14 +82,6 @@ proc get_name_chunk_1 exec.active_account::get_item end -#! Loads the initialized config word. -#! Output: [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)]. -#! (word[3] on top after get_item) -proc get_initialized_config_word - push.INITIALIZED_CONFIG_SLOT[0..2] - exec.active_account::get_item -end - #! Loads the mutability config word. #! Output: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)]. #! (word[3] on top after get_item) @@ -254,25 +247,6 @@ pub proc get_name # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] end -# ================================================================================================= -# INITIALIZED CONFIG — [desc_init, logo_init, extlink_init, max_supply_mutable] -# ================================================================================================= - -#! Returns the initialized config word. -#! -#! After get_item, stack has word[3] on top: -#! [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] -#! -#! Inputs: [pad(16)] -#! Outputs: [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] -#! -#! Invocation: call -pub proc get_initialized_config - exec.get_initialized_config_word - swapw dropw - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] -end - # ================================================================================================= # MUTABILITY CONFIG — [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable] # ================================================================================================= @@ -292,52 +266,6 @@ pub proc get_mutability_config # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] end -# ================================================================================================= -# INITIALIZED CHECKS — read initialized_config -# ================================================================================================= - -#! Returns whether description is initialized (1 or 0). -#! -#! Reads initialized_config word felt[0] (desc_init). -#! -#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call -pub proc is_description_initialized - exec.get_initialized_config_word - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] - swapw dropw - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] - drop drop drop - # => [desc_init, pad(12)] -end - -#! Returns whether logo URI is initialized (1 or 0). -#! -#! Reads initialized_config word felt[1] (logo_init). -#! -#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call -pub proc is_logo_uri_initialized - exec.get_initialized_config_word - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] - swapw dropw - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] - drop drop swap drop - # => [logo_init, pad(12)] -end - -#! Returns whether external link is initialized (1 or 0). -#! -#! Reads initialized_config word felt[2] (extlink_init). -#! -#! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call -pub proc is_external_link_initialized - exec.get_initialized_config_word - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(16)] - swapw dropw - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] - drop movdn.2 drop drop - # => [extlink_init, pad(12)] -end - # ================================================================================================= # MUTABILITY CHECKS — read mutability_config # ================================================================================================= @@ -417,120 +345,244 @@ end pub use ownable::get_owner # ================================================================================================= -# DESCRIPTION (6 words) +# DESCRIPTION (6 words) — commitment/unhashing pattern # ================================================================================================= -#! Returns the description (first word of 6). +#! Returns an RPO256 commitment to the description and inserts the 6 words into the advice map. #! -#! Due to the call convention (depth must be 16 at return), only DESCRIPTION_0 is -#! returned on the visible stack. +#! The commitment is computed over the 24 felts of the description. The preimage (6 words) +#! is placed in the advice map keyed by the commitment so that a subsequent +#! `get_description` call can retrieve and verify it. #! #! Inputs: [pad(16)] -#! Outputs: [DESCRIPTION_0, pad(12)] +#! Outputs: [COMMITMENT, pad(12)] #! #! Invocation: call @locals(24) -pub proc get_description +pub proc get_description_commitment + exec.get_description_chunk_0 + loc_storew_be.FIELD_0_LOC dropw + exec.get_description_chunk_1 + loc_storew_be.FIELD_1_LOC dropw + exec.get_description_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + exec.get_description_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + exec.get_description_chunk_4 + loc_storew_be.FIELD_4_LOC dropw exec.get_description_chunk_5 loc_storew_be.FIELD_5_LOC dropw + # => [pad(16)] - exec.get_description_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + # Hash the 6 words (24 felts) in local memory via RPO256. + # hash_elements expects [ptr, num_felts]. + push.24 locaddr.FIELD_0_LOC + exec.rpo256::hash_elements + # => [COMMITMENT, pad(16)] depth=20 - exec.get_description_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + # Insert the 6-word preimage into the advice map keyed by COMMITMENT. + # adv.insert_mem expects [COMMITMENT, start_addr, end_addr] on stack. + locaddr.FIELD_0_LOC dup add.24 + movdn.5 movdn.4 + adv.insert_mem + movup.4 drop movup.4 drop + # => [COMMITMENT, pad(16)] depth=20 - exec.get_description_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + swapw dropw + # => [COMMITMENT, pad(12)] depth=16 +end +#! Writes the description (6 Words) to memory at the given pointer. +#! +#! Loads the description from storage, computes its RPO256 commitment, inserts the preimage +#! into the advice map, then pipes it into destination memory and verifies the commitment. +#! +#! Inputs: [dest_ptr, pad(15)] +#! Outputs: [dest_ptr, pad(15)] +#! +#! Invocation: exec +@locals(24) +pub proc get_description + exec.get_description_chunk_0 + loc_storew_be.FIELD_0_LOC dropw exec.get_description_chunk_1 loc_storew_be.FIELD_1_LOC dropw + exec.get_description_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + exec.get_description_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + exec.get_description_chunk_4 + loc_storew_be.FIELD_4_LOC dropw + exec.get_description_chunk_5 + loc_storew_be.FIELD_5_LOC dropw + # => [dest_ptr, pad(15)] depth=16 + + # Hash the 6 words (24 felts) in local memory. + push.24 locaddr.FIELD_0_LOC + # => [start_ptr, 24, dest_ptr, pad(15)] depth=18 + exec.rpo256::hash_elements + # => [COMMITMENT, dest_ptr, pad(15)] depth=20 + + # Insert preimage into advice map keyed by COMMITMENT. + locaddr.FIELD_0_LOC dup add.24 + # => [end_ptr, start_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 + movdn.5 movdn.4 + # => [COMMITMENT, start_ptr, end_ptr, dest_ptr, pad(15)] + adv.insert_mem + movup.4 drop movup.4 drop + # => [COMMITMENT, dest_ptr, pad(15)] depth=20 + + # Load preimage onto the advice stack. + adv.push_mapval + # AS => [[DESCRIPTION_DATA]] - exec.get_description_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + # Set up pipe_preimage_to_memory: [num_words, dest_ptr, COMMITMENT, ...] + # dest_ptr is at position 4 (right after COMMITMENT) + dup.4 push.6 + # => [6, dest_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 - loc_loadw_be.FIELD_5_LOC - loc_loadw_be.FIELD_4_LOC - loc_loadw_be.FIELD_3_LOC - loc_loadw_be.FIELD_2_LOC - loc_loadw_be.FIELD_1_LOC - loc_loadw_be.FIELD_0_LOC + exec.mem::pipe_preimage_to_memory drop + # => [dest_ptr, pad(15)] depth=16 (consumed 6 [6,dest_ptr,COMMITMENT], pushed 1 [updated_ptr], dropped it) end # ================================================================================================= -# LOGO URI (6 words) +# LOGO URI (6 words) — commitment/unhashing pattern # ================================================================================================= -#! Returns the logo URI (first word of 6). +#! Returns an RPO256 commitment to the logo URI and inserts the 6 words into the advice map. #! #! Inputs: [pad(16)] -#! Outputs: [LOGO_URI_0, pad(12)] +#! Outputs: [COMMITMENT, pad(12)] #! #! Invocation: call @locals(24) -pub proc get_logo_uri +pub proc get_logo_uri_commitment + exec.get_logo_uri_chunk_0 + loc_storew_be.FIELD_0_LOC dropw + exec.get_logo_uri_chunk_1 + loc_storew_be.FIELD_1_LOC dropw + exec.get_logo_uri_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + exec.get_logo_uri_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + exec.get_logo_uri_chunk_4 + loc_storew_be.FIELD_4_LOC dropw exec.get_logo_uri_chunk_5 loc_storew_be.FIELD_5_LOC dropw - exec.get_logo_uri_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + push.24 locaddr.FIELD_0_LOC + exec.rpo256::hash_elements - exec.get_logo_uri_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + locaddr.FIELD_0_LOC dup add.24 + movdn.5 movdn.4 + adv.insert_mem + movup.4 drop movup.4 drop - exec.get_logo_uri_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + swapw dropw +end +#! Writes the logo URI (6 Words) to memory at the given pointer. +#! +#! Inputs: [dest_ptr, pad(15)] +#! Outputs: [dest_ptr, pad(15)] +#! +#! Invocation: exec +@locals(24) +pub proc get_logo_uri + exec.get_logo_uri_chunk_0 + loc_storew_be.FIELD_0_LOC dropw exec.get_logo_uri_chunk_1 loc_storew_be.FIELD_1_LOC dropw + exec.get_logo_uri_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + exec.get_logo_uri_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + exec.get_logo_uri_chunk_4 + loc_storew_be.FIELD_4_LOC dropw + exec.get_logo_uri_chunk_5 + loc_storew_be.FIELD_5_LOC dropw - exec.get_logo_uri_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + push.24 locaddr.FIELD_0_LOC + exec.rpo256::hash_elements + + locaddr.FIELD_0_LOC dup add.24 + movdn.5 movdn.4 + adv.insert_mem + movup.4 drop movup.4 drop - loc_loadw_be.FIELD_5_LOC - loc_loadw_be.FIELD_4_LOC - loc_loadw_be.FIELD_3_LOC - loc_loadw_be.FIELD_2_LOC - loc_loadw_be.FIELD_1_LOC - loc_loadw_be.FIELD_0_LOC + adv.push_mapval + dup.4 push.6 + exec.mem::pipe_preimage_to_memory drop end # ================================================================================================= -# EXTERNAL LINK (6 words) +# EXTERNAL LINK (6 words) — commitment/unhashing pattern # ================================================================================================= -#! Returns the external link (first word of 6). +#! Returns an RPO256 commitment to the external link and inserts the 6 words into the advice map. #! #! Inputs: [pad(16)] -#! Outputs: [EXTERNAL_LINK_0, pad(12)] +#! Outputs: [COMMITMENT, pad(12)] #! #! Invocation: call @locals(24) -pub proc get_external_link +pub proc get_external_link_commitment + exec.get_external_link_chunk_0 + loc_storew_be.FIELD_0_LOC dropw + exec.get_external_link_chunk_1 + loc_storew_be.FIELD_1_LOC dropw + exec.get_external_link_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + exec.get_external_link_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + exec.get_external_link_chunk_4 + loc_storew_be.FIELD_4_LOC dropw exec.get_external_link_chunk_5 loc_storew_be.FIELD_5_LOC dropw - exec.get_external_link_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + push.24 locaddr.FIELD_0_LOC + exec.rpo256::hash_elements - exec.get_external_link_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + locaddr.FIELD_0_LOC dup add.24 + movdn.5 movdn.4 + adv.insert_mem + movup.4 drop movup.4 drop - exec.get_external_link_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + swapw dropw +end +#! Writes the external link (6 Words) to memory at the given pointer. +#! +#! Inputs: [dest_ptr, pad(15)] +#! Outputs: [dest_ptr, pad(15)] +#! +#! Invocation: exec +@locals(24) +pub proc get_external_link + exec.get_external_link_chunk_0 + loc_storew_be.FIELD_0_LOC dropw exec.get_external_link_chunk_1 loc_storew_be.FIELD_1_LOC dropw + exec.get_external_link_chunk_2 + loc_storew_be.FIELD_2_LOC dropw + exec.get_external_link_chunk_3 + loc_storew_be.FIELD_3_LOC dropw + exec.get_external_link_chunk_4 + loc_storew_be.FIELD_4_LOC dropw + exec.get_external_link_chunk_5 + loc_storew_be.FIELD_5_LOC dropw - exec.get_external_link_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + push.24 locaddr.FIELD_0_LOC + exec.rpo256::hash_elements - loc_loadw_be.FIELD_5_LOC - loc_loadw_be.FIELD_4_LOC - loc_loadw_be.FIELD_3_LOC - loc_loadw_be.FIELD_2_LOC - loc_loadw_be.FIELD_1_LOC - loc_loadw_be.FIELD_0_LOC + locaddr.FIELD_0_LOC dup add.24 + movdn.5 movdn.4 + adv.insert_mem + movup.4 drop movup.4 drop + + adv.push_mapval + dup.4 push.6 + exec.mem::pipe_preimage_to_memory drop end # ================================================================================================= diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 3d4f7a0efc..edcbea2b4c 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -8,7 +8,6 @@ use miden_protocol::assembly::{Library, LibraryExport}; use miden_protocol::utils::serde::Deserializable; use miden_protocol::utils::sync::LazyLock; -use crate::StandardsLib; use crate::account::interface::AccountComponentInterface; // WALLET LIBRARIES @@ -89,11 +88,6 @@ static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed") }); -// Metadata Info component uses the standards library (get_name, get_description, etc. from -// metadata). -static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = - LazyLock::new(|| Library::from(StandardsLib::default())); - /// Returns the Basic Wallet Library. pub fn basic_wallet_library() -> Library { BASIC_WALLET_LIBRARY.clone() @@ -114,15 +108,6 @@ pub fn storage_schema_library() -> Library { STORAGE_SCHEMA_LIBRARY.clone() } -/// Returns the Metadata Info component library. -/// -/// Uses the standards library; the standalone [`Info`](crate::account::metadata::Info) -/// component exposes get_name, get_description, get_logo_uri, get_external_link from -/// `miden::standards::metadata::fungible`. -pub fn metadata_info_component_library() -> Library { - METADATA_INFO_COMPONENT_LIBRARY.clone() -} - /// Returns the Singlesig Library. pub fn singlesig_library() -> Library { SINGLESIG_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 148a21c57d..929ad843a5 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -17,7 +17,14 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; -use super::{Description, ExternalLink, FungibleFaucetError, LogoURI, TokenMetadata, TokenName}; +use super::{ + Description, + ExternalLink, + FungibleFaucetError, + FungibleTokenMetadata, + LogoURI, + TokenName, +}; use crate::account::AuthMethod; use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig}; use crate::account::components::basic_fungible_faucet_library; @@ -25,7 +32,7 @@ use crate::account::components::basic_fungible_faucet_library; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::Info; +use crate::account::metadata::TokenMetadata as TokenMetadataInfo; use crate::procedure_digest; // BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -63,11 +70,12 @@ procedure_digest!( /// /// ## Storage Layout /// -/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::metadata_slot`]: Stores [`FungibleTokenMetadata`]. /// /// [builder]: crate::code_builder::CodeBuilder pub struct BasicFungibleFaucet { - metadata: TokenMetadata, + metadata: FungibleTokenMetadata, + info: Option, } impl BasicFungibleFaucet { @@ -78,7 +86,7 @@ impl BasicFungibleFaucet { pub const NAME: &'static str = "miden::basic_fungible_faucet"; /// The maximum number of decimals supported by the component. - pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; + pub const MAX_DECIMALS: u8 = FungibleTokenMetadata::MAX_DECIMALS; const DISTRIBUTE_PROC_NAME: &str = "basic_fungible_faucet::distribute"; const BURN_PROC_NAME: &str = "basic_fungible_faucet::burn"; @@ -90,7 +98,7 @@ impl BasicFungibleFaucet { /// an initial token supply of zero. /// /// Optional `description`, `logo_uri`, and `external_link` are stored in the metadata - /// [`Info`][Info] component when building an account (e.g. via + /// the faucet's storage slots when building an account (e.g. via /// [`create_basic_fungible_faucet`]). /// /// # Errors @@ -108,7 +116,7 @@ impl BasicFungibleFaucet { logo_uri: Option, external_link: Option, ) -> Result { - let metadata = TokenMetadata::new( + let metadata = FungibleTokenMetadata::new( symbol, decimals, max_supply, @@ -117,15 +125,22 @@ impl BasicFungibleFaucet { logo_uri, external_link, )?; - Ok(Self { metadata }) + Ok(Self { metadata, info: None }) } - /// Creates a new [`BasicFungibleFaucet`] component from the given [`TokenMetadata`]. + /// Creates a new [`BasicFungibleFaucet`] component from the given [`FungibleTokenMetadata`]. /// /// This is a convenience constructor that allows creating a faucet from pre-validated /// metadata. - pub fn from_metadata(metadata: TokenMetadata) -> Self { - Self { metadata } + pub fn from_metadata(metadata: FungibleTokenMetadata) -> Self { + Self { metadata, info: None } + } + + /// Attaches token metadata (name, description, logo, link, mutability flags) to the + /// faucet. These storage slots will be included in the component when built. + pub fn with_info(mut self, info: TokenMetadataInfo) -> Self { + self.info = Some(info); + self } /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account @@ -151,8 +166,8 @@ impl BasicFungibleFaucet { return Err(FungibleFaucetError::MissingBasicFungibleFaucetInterface); } - let metadata = TokenMetadata::try_from(storage)?; - Ok(Self { metadata }) + let metadata = FungibleTokenMetadata::try_from(storage)?; + Ok(Self { metadata, info: None }) } // PUBLIC ACCESSORS @@ -161,7 +176,7 @@ impl BasicFungibleFaucet { /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored (slot /// 0). pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() + FungibleTokenMetadata::metadata_slot() } /// Returns the storage slot schema for the metadata slot. @@ -182,7 +197,7 @@ impl BasicFungibleFaucet { } /// Returns the token metadata. - pub fn metadata(&self) -> &TokenMetadata { + pub fn metadata(&self) -> &FungibleTokenMetadata { &self.metadata } @@ -240,6 +255,11 @@ impl From for AccountComponent { fn from(faucet: BasicFungibleFaucet) -> Self { let storage_slot = faucet.metadata.into(); + let mut slots = vec![storage_slot]; + if let Some(info) = &faucet.info { + slots.extend(info.storage_slots()); + } + let storage_schema = StorageSchema::new([BasicFungibleFaucet::metadata_slot_schema()]) .expect("storage schema should be valid"); @@ -248,7 +268,7 @@ impl From for AccountComponent { .with_supported_type(AccountType::FungibleFaucet) .with_storage_schema(storage_schema); - AccountComponent::new(basic_fungible_faucet_library(), vec![storage_slot], metadata) + AccountComponent::new(basic_fungible_faucet_library(), slots, metadata) .expect("basic fungible faucet component should satisfy the requirements of a valid account component") } } @@ -276,7 +296,7 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// Creates a new faucet account with basic fungible faucet interface, /// account storage type, specified authentication scheme, and provided meta data (token symbol, /// decimals, max supply). Optional `name`, `description`, `logo_uri`, and `external_link` are -/// stored in the metadata [`Info`][Info] component when provided. +/// stored in the faucet's metadata storage slots when provided. /// /// The basic faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -290,7 +310,7 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// components (see their docs for details): /// - [`BasicFungibleFaucet`] /// - [`AuthSingleSigAcl`] -/// - [`Info`][Info] (when `name` or optional fields are provided) +/// - Token metadata (name, description, etc.) when provided via [`BasicFungibleFaucet::with_info`] #[allow(clippy::too_many_arguments)] pub fn create_basic_fungible_faucet( init_seed: [u8; 32], @@ -334,6 +354,17 @@ pub fn create_basic_fungible_faucet( }, }; + let mut info = TokenMetadataInfo::new().with_name(name.clone()); + if let Some(d) = &description { + info = info.with_description(d.clone(), false); + } + if let Some(l) = &logo_uri { + info = info.with_logo_uri(l.clone(), false); + } + if let Some(e) = &external_link { + info = info.with_external_link(e.clone(), false); + } + let faucet = BasicFungibleFaucet::new( symbol, decimals, @@ -342,25 +373,14 @@ pub fn create_basic_fungible_faucet( description, logo_uri, external_link, - )?; - - let mut info = Info::new().with_name(name.as_words()); - if let Some(d) = &description { - info = info.with_description(d.as_words(), false); - } - if let Some(l) = &logo_uri { - info = info.with_logo_uri(l.as_words(), false); - } - if let Some(e) = &external_link { - info = info.with_external_link(e.as_words(), false); - } + )? + .with_info(info); let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(account_storage_mode) .with_auth_component(auth_component) .with_component(faucet) - .with_component(info) .build() .map_err(FungibleFaucetError::AccountError)?; @@ -390,7 +410,7 @@ mod tests { create_basic_fungible_faucet, }; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; - use crate::account::metadata::{self as account_metadata, Info}; + use crate::account::metadata::TokenMetadata as TokenMetadataInfo; use crate::account::wallets::BasicWallet; #[test] @@ -414,8 +434,8 @@ mod tests { let decimals = 2u8; let storage_mode = AccountStorageMode::Private; - let token_name = TokenName::try_from(token_name_string).unwrap(); - let description = Description::try_from(description_string).unwrap(); + let token_name = TokenName::new(token_name_string).unwrap(); + let description = Description::new(description_string).unwrap(); let faucet_account = create_basic_fungible_faucet( init_seed, token_symbol, @@ -467,14 +487,22 @@ mod tests { ); // Check that Info component has name and description - let name_0 = faucet_account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = faucet_account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); - let decoded_name = account_metadata::name_to_utf8(&[name_0, name_1]).unwrap(); - assert_eq!(decoded_name, token_name_string); - let expected_desc_words = - account_metadata::field_from_bytes(description_string.as_bytes()).unwrap(); + let name_0 = faucet_account + .storage() + .get_item(TokenMetadataInfo::name_chunk_0_slot()) + .unwrap(); + let name_1 = faucet_account + .storage() + .get_item(TokenMetadataInfo::name_chunk_1_slot()) + .unwrap(); + let decoded_name = TokenName::try_from_words(&[name_0, name_1]).unwrap(); + assert_eq!(decoded_name.as_str(), token_name_string); + let expected_desc_words = Description::new(description_string).unwrap().to_words(); for (i, expected) in expected_desc_words.iter().enumerate() { - let chunk = faucet_account.storage().get_item(Info::description_slot(i)).unwrap(); + let chunk = faucet_account + .storage() + .get_item(TokenMetadataInfo::description_slot(i)) + .unwrap(); assert_eq!(chunk, *expected); } @@ -506,7 +534,7 @@ mod tests { token_symbol, 10, Felt::new(100), - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 851cc6678f..65c902ea88 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -10,7 +10,7 @@ mod token_metadata; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; -pub use token_metadata::{Description, ExternalLink, LogoURI, TokenMetadata, TokenName}; +pub use token_metadata::{Description, ExternalLink, FungibleTokenMetadata, LogoURI, TokenName}; // FUNGIBLE FAUCET ERROR // ================================================================================================ diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 87b3f13fbc..03982d561a 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -19,14 +19,21 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; -use super::{Description, ExternalLink, FungibleFaucetError, LogoURI, TokenMetadata, TokenName}; +use super::{ + Description, + ExternalLink, + FungibleFaucetError, + FungibleTokenMetadata, + LogoURI, + TokenName, +}; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::Info; +use crate::account::metadata::TokenMetadata as TokenMetadataInfo; use crate::procedure_digest; // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -66,8 +73,9 @@ procedure_digest!( /// /// [builder]: crate::code_builder::CodeBuilder pub struct NetworkFungibleFaucet { - metadata: TokenMetadata, + metadata: FungibleTokenMetadata, owner_account_id: AccountId, + info: Option, } impl NetworkFungibleFaucet { @@ -78,7 +86,7 @@ impl NetworkFungibleFaucet { pub const NAME: &'static str = "miden::network_fungible_faucet"; /// The maximum number of decimals supported by the component. - pub const MAX_DECIMALS: u8 = TokenMetadata::MAX_DECIMALS; + pub const MAX_DECIMALS: u8 = FungibleTokenMetadata::MAX_DECIMALS; const DISTRIBUTE_PROC_NAME: &str = "network_fungible_faucet::distribute"; const BURN_PROC_NAME: &str = "network_fungible_faucet::burn"; @@ -88,9 +96,8 @@ impl NetworkFungibleFaucet { /// Creates a new [`NetworkFungibleFaucet`] component from the given pieces of metadata. /// - /// Optional `description`, `logo_uri`, and `external_link` are stored in the metadata - /// [`Info`][Info] component when building an account (e.g. via - /// [`create_network_fungible_faucet`]). + /// Optional `description`, `logo_uri`, and `external_link` are stored in the component's + /// storage slots when building an account. /// /// # Errors: /// Returns an error if: @@ -107,7 +114,7 @@ impl NetworkFungibleFaucet { logo_uri: Option, external_link: Option, ) -> Result { - let metadata = TokenMetadata::new( + let metadata = FungibleTokenMetadata::new( symbol, decimals, max_supply, @@ -116,15 +123,22 @@ impl NetworkFungibleFaucet { logo_uri, external_link, )?; - Ok(Self { metadata, owner_account_id }) + Ok(Self { metadata, owner_account_id, info: None }) } - /// Creates a new [`NetworkFungibleFaucet`] component from the given [`TokenMetadata`]. + /// Creates a new [`NetworkFungibleFaucet`] component from the given [`FungibleTokenMetadata`]. /// /// This is a convenience constructor that allows creating a faucet from pre-validated /// metadata. - pub fn from_metadata(metadata: TokenMetadata, owner_account_id: AccountId) -> Self { - Self { metadata, owner_account_id } + pub fn from_metadata(metadata: FungibleTokenMetadata, owner_account_id: AccountId) -> Self { + Self { metadata, owner_account_id, info: None } + } + + /// Attaches token metadata (name, description, logo, link, mutability flags) to the + /// faucet. These storage slots will be included in the component when built. + pub fn with_info(mut self, info: TokenMetadataInfo) -> Self { + self.info = Some(info); + self } /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account @@ -153,7 +167,7 @@ impl NetworkFungibleFaucet { } // Read token metadata from storage - let metadata = TokenMetadata::try_from(storage)?; + let metadata = FungibleTokenMetadata::try_from(storage)?; // obtain owner account ID from the next storage slot let owner_account_id_word: Word = storage @@ -169,7 +183,7 @@ impl NetworkFungibleFaucet { let suffix = owner_account_id_word[2]; let owner_account_id = AccountId::new_unchecked([prefix, suffix]); - Ok(Self { metadata, owner_account_id }) + Ok(Self { metadata, owner_account_id, info: None }) } // PUBLIC ACCESSORS @@ -178,7 +192,7 @@ impl NetworkFungibleFaucet { /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s metadata is stored /// (slot 0). pub fn metadata_slot() -> &'static StorageSlotName { - TokenMetadata::metadata_slot() + FungibleTokenMetadata::metadata_slot() } /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s owner configuration is @@ -221,7 +235,7 @@ impl NetworkFungibleFaucet { } /// Returns the token metadata. - pub fn metadata(&self) -> &TokenMetadata { + pub fn metadata(&self) -> &FungibleTokenMetadata { &self.metadata } @@ -284,7 +298,6 @@ impl From for AccountComponent { fn from(network_faucet: NetworkFungibleFaucet) -> Self { let metadata_slot = network_faucet.metadata.into(); - // Convert AccountId into its Word encoding for storage. let owner_account_id_word: Word = [ Felt::new(0), Felt::new(0), @@ -298,6 +311,11 @@ impl From for AccountComponent { owner_account_id_word, ); + let mut slots = vec![metadata_slot, owner_slot]; + if let Some(info) = &network_faucet.info { + slots.extend(info.storage_slots()); + } + let storage_schema = StorageSchema::new([ NetworkFungibleFaucet::metadata_slot_schema(), NetworkFungibleFaucet::owner_config_slot_schema(), @@ -309,12 +327,8 @@ impl From for AccountComponent { .with_supported_type(AccountType::FungibleFaucet) .with_storage_schema(storage_schema); - AccountComponent::new( - network_fungible_faucet_library(), - vec![metadata_slot, owner_slot], - metadata, - ) - .expect("network fungible faucet component should satisfy the requirements of a valid account component") + AccountComponent::new(network_fungible_faucet_library(), slots, metadata) + .expect("network fungible faucet component should satisfy the requirements of a valid account component") } } @@ -340,7 +354,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// Creates a new faucet account with network fungible faucet interface and provided metadata /// (token symbol, decimals, max supply, owner account ID). Optional `name`, `description`, -/// `logo_uri`, and `external_link` are stored in the metadata [`Info`][Info] component when +/// `logo_uri`, and `external_link` are stored in the faucet's metadata storage slots when /// provided. /// /// The network faucet interface exposes two procedures: @@ -371,6 +385,17 @@ pub fn create_network_fungible_faucet( ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); + let mut info = TokenMetadataInfo::new().with_name(name.clone()); + if let Some(d) = &description { + info = info.with_description(d.clone(), false); + } + if let Some(l) = &logo_uri { + info = info.with_logo_uri(l.clone(), false); + } + if let Some(e) = &external_link { + info = info.with_external_link(e.clone(), false); + } + let faucet = NetworkFungibleFaucet::new( symbol, decimals, @@ -380,25 +405,14 @@ pub fn create_network_fungible_faucet( description, logo_uri, external_link, - )?; - - let mut info = Info::new().with_name(name.as_words()); - if let Some(d) = &description { - info = info.with_description(d.as_words(), false); - } - if let Some(l) = &logo_uri { - info = info.with_logo_uri(l.as_words(), false); - } - if let Some(e) = &external_link { - info = info.with_external_link(e.as_words(), false); - } + )? + .with_info(info); let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(auth_component) .with_component(faucet) - .with_component(info) .build() .map_err(FungibleFaucetError::AccountError)?; diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 9ed95f6071..aca959ad3c 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -1,3 +1,7 @@ +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; use miden_protocol::asset::{FungibleAsset, TokenSymbol}; use miden_protocol::{Felt, Word}; @@ -8,96 +12,227 @@ use crate::account::metadata::{self, FieldBytesError, NameUtf8Error}; // TOKEN NAME // ================================================================================================ -/// Token display name (max 32 bytes UTF-8), stored as 2 Words in the metadata Info component. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TokenName([Word; 2]); +/// Token display name (max 32 bytes UTF-8). +/// +/// Internally stores the un-encoded string for cheap access via [`as_str`](Self::as_str). +/// The invariant that the string can be encoded into 2 Words (8 felts, 4 bytes/felt) is +/// enforced at construction time. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TokenName(Box); impl TokenName { - /// Creates a token name from a UTF-8 string (at most 32 bytes). - pub fn try_from(s: &str) -> Result { - let words = metadata::name_from_utf8(s)?; - Ok(Self(words)) - } + /// Maximum byte length for a token name (2 Words = 8 felts x 4 bytes). + pub const MAX_BYTES: usize = metadata::NAME_UTF8_MAX_BYTES; - /// Returns the name as two Words for storage in the Info component. - pub fn as_words(&self) -> [Word; 2] { - self.0 + /// Creates a token name from a UTF-8 string (at most 32 bytes). + pub fn new(s: &str) -> Result { + if s.len() > Self::MAX_BYTES { + return Err(NameUtf8Error::TooLong(s.len())); + } + Ok(Self(s.into())) + } + + /// Returns the name as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Encodes the name into 2 Words for storage (4 bytes/felt, little-endian, zero-padded). + pub fn to_words(&self) -> [Word; 2] { + let bytes = self.0.as_bytes(); + let mut padded = [0u8; Self::MAX_BYTES]; + padded[..bytes.len()].copy_from_slice(bytes); + let felts: [Felt; 8] = padded + .chunks_exact(4) + .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap()))) + .collect::>() + .try_into() + .unwrap(); + [ + Word::from([felts[0], felts[1], felts[2], felts[3]]), + Word::from([felts[4], felts[5], felts[6], felts[7]]), + ] + } + + /// Decodes a token name from 2 Words (4 bytes/felt, little-endian). + pub fn try_from_words(words: &[Word; 2]) -> Result { + let mut bytes = [0u8; Self::MAX_BYTES]; + for (i, word) in words.iter().enumerate() { + for (j, f) in word.iter().enumerate() { + let v = f.as_int(); + if v > u32::MAX as u64 { + return Err(NameUtf8Error::InvalidUtf8); + } + bytes[i * 16 + j * 4..][..4].copy_from_slice(&(v as u32).to_le_bytes()); + } + } + let len = bytes.iter().position(|&b| b == 0).unwrap_or(Self::MAX_BYTES); + let s = String::from_utf8(bytes[..len].to_vec()).map_err(|_| NameUtf8Error::InvalidUtf8)?; + Ok(Self(s.into())) } } // DESCRIPTION // ================================================================================================ -/// Token description (max 192 bytes), stored as 6 Words in the metadata Info component. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Description([Word; 6]); +/// Token description (max 192 bytes UTF-8). +/// +/// Internally stores the un-encoded string. The invariant that it can be encoded into 6 Words +/// (24 felts, 8 bytes/felt) is enforced at construction time. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Description(Box); impl Description { - /// Creates a description from a byte slice (at most 192 bytes). - pub fn try_from_bytes(bytes: &[u8]) -> Result { - let words = metadata::field_from_bytes(bytes)?; - Ok(Self(words)) + /// Maximum byte length for a description (6 Words = 24 felts x 8 bytes). + pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; + + /// Creates a description from a UTF-8 string (at most 192 bytes). + pub fn new(s: &str) -> Result { + if s.len() > Self::MAX_BYTES { + return Err(FieldBytesError::TooLong(s.len())); + } + Ok(Self(s.into())) } - /// Creates a description from a string (encoded as UTF-8 bytes; at most 192 bytes). - pub fn try_from(s: &str) -> Result { - Self::try_from_bytes(s.as_bytes()) + /// Returns the description as a string slice. + pub fn as_str(&self) -> &str { + &self.0 } - /// Returns the description as six Words for storage in the Info component. - pub fn as_words(&self) -> [Word; 6] { - self.0 + /// Encodes the description into 6 Words for storage (8 bytes/felt, little-endian). + pub fn to_words(&self) -> [Word; 6] { + encode_field_to_words(self.0.as_bytes()) + } + + /// Decodes a description from 6 Words (8 bytes/felt, little-endian). + pub fn try_from_words(words: &[Word; 6]) -> Result { + let s = decode_field_from_words(words)?; + Ok(Self(s.into())) } } // LOGO URI // ================================================================================================ -/// Token logo URI (max 192 bytes), stored as 6 Words in the metadata Info component. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LogoURI([Word; 6]); +/// Token logo URI (max 192 bytes UTF-8). +/// +/// Internally stores the un-encoded string. The invariant that it can be encoded into 6 Words +/// (24 felts, 8 bytes/felt) is enforced at construction time. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogoURI(Box); impl LogoURI { - /// Creates a logo URI from a byte slice (at most 192 bytes). - pub fn try_from_bytes(bytes: &[u8]) -> Result { - let words = metadata::field_from_bytes(bytes)?; - Ok(Self(words)) + /// Maximum byte length for a logo URI (6 Words = 24 felts x 8 bytes). + pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; + + /// Creates a logo URI from a UTF-8 string (at most 192 bytes). + pub fn new(s: &str) -> Result { + if s.len() > Self::MAX_BYTES { + return Err(FieldBytesError::TooLong(s.len())); + } + Ok(Self(s.into())) + } + + /// Returns the logo URI as a string slice. + pub fn as_str(&self) -> &str { + &self.0 } - /// Creates a logo URI from a string (encoded as UTF-8 bytes; at most 192 bytes). - pub fn try_from(s: &str) -> Result { - Self::try_from_bytes(s.as_bytes()) + /// Encodes the logo URI into 6 Words for storage (8 bytes/felt, little-endian). + pub fn to_words(&self) -> [Word; 6] { + encode_field_to_words(self.0.as_bytes()) } - /// Returns the logo URI as six Words for storage in the Info component. - pub fn as_words(&self) -> [Word; 6] { - self.0 + /// Decodes a logo URI from 6 Words (8 bytes/felt, little-endian). + pub fn try_from_words(words: &[Word; 6]) -> Result { + let s = decode_field_from_words(words)?; + Ok(Self(s.into())) } } // EXTERNAL LINK // ================================================================================================ -/// Token external link (max 192 bytes), stored as 6 Words in the metadata Info component. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ExternalLink([Word; 6]); +/// Token external link (max 192 bytes UTF-8). +/// +/// Internally stores the un-encoded string. The invariant that it can be encoded into 6 Words +/// (24 felts, 8 bytes/felt) is enforced at construction time. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExternalLink(Box); impl ExternalLink { - /// Creates an external link from a byte slice (at most 192 bytes). - pub fn try_from_bytes(bytes: &[u8]) -> Result { - let words = metadata::field_from_bytes(bytes)?; - Ok(Self(words)) + /// Maximum byte length for an external link (6 Words = 24 felts x 8 bytes). + pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; + + /// Creates an external link from a UTF-8 string (at most 192 bytes). + pub fn new(s: &str) -> Result { + if s.len() > Self::MAX_BYTES { + return Err(FieldBytesError::TooLong(s.len())); + } + Ok(Self(s.into())) + } + + /// Returns the external link as a string slice. + pub fn as_str(&self) -> &str { + &self.0 } - /// Creates an external link from a string (encoded as UTF-8 bytes; at most 192 bytes). - pub fn try_from(s: &str) -> Result { - Self::try_from_bytes(s.as_bytes()) + /// Encodes the external link into 6 Words for storage (8 bytes/felt, little-endian). + pub fn to_words(&self) -> [Word; 6] { + encode_field_to_words(self.0.as_bytes()) } - /// Returns the external link as six Words for storage in the Info component. - pub fn as_words(&self) -> [Word; 6] { - self.0 + /// Decodes an external link from 6 Words (8 bytes/felt, little-endian). + pub fn try_from_words(words: &[Word; 6]) -> Result { + let s = decode_field_from_words(words)?; + Ok(Self(s.into())) + } +} + +// ENCODING HELPERS +// ================================================================================================ + +/// Encodes a byte slice into 6 Words (24 felts, 8 bytes/felt, little-endian, zero-padded). +/// +/// # Panics +/// +/// Panics (debug-only) if `bytes.len() > FIELD_MAX_BYTES`. Callers must validate length first. +fn encode_field_to_words(bytes: &[u8]) -> [Word; 6] { + debug_assert!(bytes.len() <= metadata::FIELD_MAX_BYTES); + let mut padded = [0u8; metadata::FIELD_MAX_BYTES]; + padded[..bytes.len()].copy_from_slice(bytes); + let felts: Vec = padded + .chunks_exact(8) + .map(|chunk| { + // SAFETY: Valid UTF-8 bytes have values in 0x00..=0xF4. A u64 formed from 8 such + // bytes can never reach the Goldilocks prime (2^64 - 2^32 + 1) because that would + // require all 8 bytes to be >= 0xFF, which is impossible in valid UTF-8. + Felt::try_from(u64::from_le_bytes(chunk.try_into().unwrap())) + .expect("UTF-8 bytes cannot overflow Felt") + }) + .collect(); + let felts: [Felt; 24] = felts.try_into().unwrap(); + [ + Word::from([felts[0], felts[1], felts[2], felts[3]]), + Word::from([felts[4], felts[5], felts[6], felts[7]]), + Word::from([felts[8], felts[9], felts[10], felts[11]]), + Word::from([felts[12], felts[13], felts[14], felts[15]]), + Word::from([felts[16], felts[17], felts[18], felts[19]]), + Word::from([felts[20], felts[21], felts[22], felts[23]]), + ] +} + +/// Decodes 6 Words (8 bytes/felt, little-endian) back to a UTF-8 string. +fn decode_field_from_words(words: &[Word; 6]) -> Result { + let mut bytes = [0u8; metadata::FIELD_MAX_BYTES]; + for (i, word) in words.iter().enumerate() { + for (j, f) in word.iter().enumerate() { + let v = f.as_int(); + bytes[i * 32 + j * 8..][..8].copy_from_slice(&v.to_le_bytes()); + } } + let len = bytes.iter().position(|&b| b == 0).unwrap_or(metadata::FIELD_MAX_BYTES); + String::from_utf8(bytes[..len].to_vec()).map_err(|_| FieldBytesError::InvalidUtf8) } // TOKEN METADATA @@ -114,10 +249,13 @@ impl ExternalLink { /// The metadata is stored in a single storage slot as: /// `[token_supply, max_supply, decimals, symbol]` /// -/// `name` and optional `description`/`logo_uri`/`external_link` are not stored in that slot; -/// they are used only when building an account to populate the metadata Info component. -#[derive(Debug, Clone, Copy)] -pub struct TokenMetadata { +/// `name` and optional `description`/`logo_uri`/`external_link` are not serialized into that +/// slot. They are kept here as convenience accessors and for use when constructing the +/// [`TokenMetadata`](crate::account::metadata::TokenMetadata) storage slots via +/// [`BasicFungibleFaucet::with_info`](super::BasicFungibleFaucet::with_info) or +/// [`NetworkFungibleFaucet::with_info`](super::NetworkFungibleFaucet::with_info). +#[derive(Debug, Clone)] +pub struct FungibleTokenMetadata { token_supply: Felt, max_supply: Felt, decimals: u8, @@ -128,7 +266,7 @@ pub struct TokenMetadata { external_link: Option, } -impl TokenMetadata { +impl FungibleTokenMetadata { // CONSTANTS // -------------------------------------------------------------------------------------------- @@ -138,7 +276,7 @@ impl TokenMetadata { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`TokenMetadata`] with the specified metadata and zero token supply. + /// Creates a new [`FungibleTokenMetadata`] with the specified metadata and zero token supply. /// /// # Errors /// Returns an error if: @@ -165,7 +303,7 @@ impl TokenMetadata { ) } - /// Creates a new [`TokenMetadata`] with the specified metadata and token supply. + /// Creates a new [`FungibleTokenMetadata`] with the specified metadata and token supply. /// /// # Errors /// Returns an error if: @@ -291,12 +429,17 @@ impl TokenMetadata { // TRAIT IMPLEMENTATIONS // ================================================================================================ -impl TryFrom for TokenMetadata { +impl TryFrom for FungibleTokenMetadata { type Error = FungibleFaucetError; /// Parses token metadata from a Word. /// - /// The Word is expected to be in the format: `[token_supply, max_supply, decimals, symbol]` + /// The Word is expected to be in the format: `[token_supply, max_supply, decimals, symbol]`. + /// + /// **Note:** The name is set to an empty string and optional fields (description, + /// logo_uri, external_link) are `None`, because these are stored in separate + /// storage slots (via [`TokenMetadata`](crate::account::metadata::TokenMetadata)), + /// not in the metadata Word itself. fn try_from(word: Word) -> Result { let [token_supply, max_supply, decimals, token_symbol] = *word; @@ -310,15 +453,13 @@ impl TryFrom for TokenMetadata { } })?; - // When parsing from storage, name is not available; use empty string. - let name = TokenName::try_from("").expect("empty string should be valid"); + let name = TokenName::new("").expect("empty string should be valid"); Self::with_supply(symbol, decimals, max_supply, token_supply, name, None, None, None) } } -impl From for Word { - fn from(metadata: TokenMetadata) -> Self { - // Storage layout: [token_supply, max_supply, decimals, symbol] +impl From for Word { + fn from(metadata: FungibleTokenMetadata) -> Self { Word::new([ metadata.token_supply, metadata.max_supply, @@ -328,16 +469,16 @@ impl From for Word { } } -impl From for StorageSlot { - fn from(metadata: TokenMetadata) -> Self { - StorageSlot::with_value(TokenMetadata::metadata_slot().clone(), metadata.into()) +impl From for StorageSlot { + fn from(metadata: FungibleTokenMetadata) -> Self { + StorageSlot::with_value(FungibleTokenMetadata::metadata_slot().clone(), metadata.into()) } } -impl TryFrom<&StorageSlot> for TokenMetadata { +impl TryFrom<&StorageSlot> for FungibleTokenMetadata { type Error = FungibleFaucetError; - /// Tries to create [`TokenMetadata`] from a storage slot. + /// Tries to create [`FungibleTokenMetadata`] from a storage slot. /// /// # Errors /// Returns an error if: @@ -350,23 +491,24 @@ impl TryFrom<&StorageSlot> for TokenMetadata { actual: slot.name().clone(), }); } - TokenMetadata::try_from(slot.value()) + FungibleTokenMetadata::try_from(slot.value()) } } -impl TryFrom<&AccountStorage> for TokenMetadata { +impl TryFrom<&AccountStorage> for FungibleTokenMetadata { type Error = FungibleFaucetError; - /// Tries to create [`TokenMetadata`] from account storage. + /// Tries to create [`FungibleTokenMetadata`] from account storage. fn try_from(storage: &AccountStorage) -> Result { - let metadata_word = storage.get_item(TokenMetadata::metadata_slot()).map_err(|err| { - FungibleFaucetError::StorageLookupFailed { - slot_name: TokenMetadata::metadata_slot().clone(), - source: err, - } - })?; + let metadata_word = + storage.get_item(FungibleTokenMetadata::metadata_slot()).map_err(|err| { + FungibleFaucetError::StorageLookupFailed { + slot_name: FungibleTokenMetadata::metadata_slot().clone(), + source: err, + } + })?; - TokenMetadata::try_from(metadata_word) + FungibleTokenMetadata::try_from(metadata_word) } } @@ -385,10 +527,18 @@ mod tests { let symbol = TokenSymbol::new("TEST").unwrap(); let decimals = 8u8; let max_supply = Felt::new(1_000_000); - let name = TokenName::try_from("TEST").unwrap(); + let name = TokenName::new("TEST").unwrap(); - let metadata = - TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None).unwrap(); + let metadata = FungibleTokenMetadata::new( + symbol, + decimals, + max_supply, + name.clone(), + None, + None, + None, + ) + .unwrap(); assert_eq!(metadata.symbol(), symbol); assert_eq!(metadata.decimals(), decimals); @@ -406,9 +556,9 @@ mod tests { let decimals = 8u8; let max_supply = Felt::new(1_000_000); let token_supply = Felt::new(500_000); - let name = TokenName::try_from("TEST").unwrap(); + let name = TokenName::new("TEST").unwrap(); - let metadata = TokenMetadata::with_supply( + let metadata = FungibleTokenMetadata::with_supply( symbol, decimals, max_supply, @@ -431,76 +581,91 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let decimals = 2u8; let max_supply = Felt::new(123); - let name = TokenName::try_from("polygon").unwrap(); - let description = Description::try_from("A polygon token").unwrap(); + let name = TokenName::new("polygon").unwrap(); + let description = Description::new("A polygon token").unwrap(); - let metadata = - TokenMetadata::new(symbol, decimals, max_supply, name, Some(description), None, None) - .unwrap(); + let metadata = FungibleTokenMetadata::new( + symbol, + decimals, + max_supply, + name.clone(), + Some(description.clone()), + None, + None, + ) + .unwrap(); assert_eq!(metadata.symbol(), symbol); assert_eq!(metadata.name(), &name); assert_eq!(metadata.description(), Some(&description)); - // Word roundtrip does not include name/description let word: Word = metadata.into(); - let restored = TokenMetadata::try_from(word).unwrap(); + let restored = FungibleTokenMetadata::try_from(word).unwrap(); assert_eq!(restored.symbol(), symbol); assert!(restored.description().is_none()); } #[test] - fn token_name_try_from_valid() { - let name = TokenName::try_from("polygon").unwrap(); - assert_eq!(metadata::name_to_utf8(&name.as_words()).unwrap(), "polygon"); + fn token_name_roundtrip() { + let name = TokenName::new("polygon").unwrap(); + let words = name.to_words(); + let decoded = TokenName::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), "polygon"); } #[test] - fn token_name_try_from_too_long() { + fn token_name_as_str() { + let name = TokenName::new("my_token").unwrap(); + assert_eq!(name.as_str(), "my_token"); + } + + #[test] + fn token_name_too_long() { let s = "a".repeat(33); - assert!(TokenName::try_from(&s).is_err()); + assert!(TokenName::new(&s).is_err()); } #[test] - fn description_try_from_valid() { + fn description_roundtrip() { let text = "A short description"; - let desc = Description::try_from(text).unwrap(); - let words = desc.as_words(); - let expected = metadata::field_from_bytes(text.as_bytes()).unwrap(); - assert_eq!(words, expected); + let desc = Description::new(text).unwrap(); + let words = desc.to_words(); + let decoded = Description::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), text); } #[test] - fn description_try_from_too_long() { - let bytes = [0u8; 193]; - assert!(Description::try_from_bytes(&bytes).is_err()); + fn description_too_long() { + let s = "a".repeat(193); + assert!(Description::new(&s).is_err()); } #[test] - fn logo_uri_try_from_valid() { + fn logo_uri_roundtrip() { let url = "https://example.com/logo.png"; - let uri = LogoURI::try_from(url).unwrap(); - let words = uri.as_words(); - let expected = metadata::field_from_bytes(url.as_bytes()).unwrap(); - assert_eq!(words, expected); + let uri = LogoURI::new(url).unwrap(); + let words = uri.to_words(); + let decoded = LogoURI::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), url); } #[test] - fn external_link_try_from_valid() { + fn external_link_roundtrip() { let url = "https://example.com"; - let link = ExternalLink::try_from(url).unwrap(); - let words = link.as_words(); - let expected = metadata::field_from_bytes(url.as_bytes()).unwrap(); - assert_eq!(words, expected); + let link = ExternalLink::new(url).unwrap(); + let words = link.to_words(); + let decoded = ExternalLink::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), url); } #[test] fn token_metadata_too_many_decimals() { let symbol = TokenSymbol::new("TEST").unwrap(); - let decimals = 13u8; // exceeds MAX_DECIMALS + let decimals = 13u8; let max_supply = Felt::new(1_000_000); - let name = TokenName::try_from("TEST").unwrap(); + let name = TokenName::new("TEST").unwrap(); - let result = TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); + let result = + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. }))); } @@ -510,11 +675,11 @@ mod tests { let symbol = TokenSymbol::new("TEST").unwrap(); let decimals = 8u8; - // FungibleAsset::MAX_AMOUNT is 2^63 - 1, so we use MAX_AMOUNT + 1 to exceed it let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1); - let name = TokenName::try_from("TEST").unwrap(); + let name = TokenName::new("TEST").unwrap(); - let result = TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); + let result = + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. }))); } @@ -523,14 +688,14 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let decimals = 2u8; let max_supply = Felt::new(123); - let name = TokenName::try_from("POL").unwrap(); + let name = TokenName::new("POL").unwrap(); let metadata = - TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None).unwrap(); + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None) + .unwrap(); let word: Word = metadata.into(); - // Storage layout: [token_supply, max_supply, decimals, symbol] - assert_eq!(word[0], Felt::ZERO); // token_supply + assert_eq!(word[0], Felt::ZERO); assert_eq!(word[1], max_supply); assert_eq!(word[2], Felt::from(decimals)); assert_eq!(word[3], Felt::from(symbol)); @@ -541,13 +706,14 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let decimals = 2u8; let max_supply = Felt::new(123); - let name = TokenName::try_from("POL").unwrap(); + let name = TokenName::new("POL").unwrap(); let original = - TokenMetadata::new(symbol, decimals, max_supply, name, None, None, None).unwrap(); + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None) + .unwrap(); let slot: StorageSlot = original.into(); - let restored = TokenMetadata::try_from(&slot).unwrap(); + let restored = FungibleTokenMetadata::try_from(&slot).unwrap(); assert_eq!(restored.symbol(), symbol); assert_eq!(restored.decimals(), decimals); @@ -561,9 +727,9 @@ mod tests { let decimals = 2u8; let max_supply = Felt::new(1000); let token_supply = Felt::new(500); - let name = TokenName::try_from("POL").unwrap(); + let name = TokenName::new("POL").unwrap(); - let original = TokenMetadata::with_supply( + let original = FungibleTokenMetadata::with_supply( symbol, decimals, max_supply, @@ -575,7 +741,7 @@ mod tests { ) .unwrap(); let word: Word = original.into(); - let restored = TokenMetadata::try_from(word).unwrap(); + let restored = FungibleTokenMetadata::try_from(word).unwrap(); assert_eq!(restored.symbol(), symbol); assert_eq!(restored.decimals(), decimals); diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 50824673d1..aba06ae431 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -59,7 +59,7 @@ fn test_basic_wallet_default_notes() { TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), - crate::account::faucets::TokenName::try_from("POL").unwrap(), + crate::account::faucets::TokenName::new("POL").unwrap(), None, None, None, @@ -329,7 +329,7 @@ fn test_basic_fungible_faucet_custom_notes() { TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), - crate::account::faucets::TokenName::try_from("POL").unwrap(), + crate::account::faucets::TokenName::new("POL").unwrap(), None, None, None, diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 4bb804c1fb..60a61646bf 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -1,7 +1,7 @@ //! Account / contract / faucet metadata (slots 0..23) //! //! All of the following are metadata of the account (or faucet): token_symbol, decimals, -//! max_supply, owner, name, initialized_config, mutability_config, description, logo URI, +//! max_supply, owner, name, mutability_config, description, logo URI, //! and external link. //! //! ## Storage layout @@ -12,7 +12,6 @@ //! | `ownable::owner_config` | owner account id (defined by ownable module) | //! | `metadata::name_0` | first 4 felts of name | //! | `metadata::name_1` | last 4 felts of name | -//! | `metadata::initialized_config` | `[desc_init, logo_init, extlink_init, max_supply_mutable]` | //! | `metadata::mutability_config` | `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` | //! | `metadata::description_0..5` | description (6 Words, ~192 bytes) | //! | `metadata::logo_uri_0..5` | logo URI (6 Words, ~192 bytes) | @@ -25,25 +24,23 @@ //! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. //! Any change to slot indices or names must be applied in both Rust and MASM. //! -//! ## Config Words +//! ## Config Word //! -//! Two config Words store per-field boolean flags: -//! -//! **initialized_config**: `[desc_init, logo_init, extlink_init, max_supply_mutable]` -//! - Each flag is 0 (not initialized) or 1 (initialized). +//! A single config Word stores per-field boolean flags: //! //! **mutability_config**: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` //! - Each flag is 0 (immutable) or 1 (mutable / owner can update). //! -//! `max_supply_mutable` appears in both words for convenient access. +//! Whether a field is *present* is determined by whether its storage words are all zero +//! (absent) or not (present). No separate `initialized_config` is needed. //! //! ## MASM modules //! //! All metadata procedures (getters, `get_owner`, setters) live in //! `miden::standards::metadata::fungible`, which depends on ownable. The standalone -//! Info component uses the standards library and exposes `get_name`, `get_description`, -//! `get_logo_uri`, `get_external_link`; for owner and mutable fields use a component -//! that re-exports from fungible (e.g. network fungible faucet). +//! The TokenMetadata component uses the standards library and exposes `get_name`, +//! `get_description`, `get_logo_uri`, `get_external_link`; for owner and mutable fields use a +//! component that re-exports from fungible (e.g. network fungible faucet). //! //! ## Name encoding (UTF-8) //! @@ -55,15 +52,17 @@ //! # Example //! //! ```ignore -//! use miden_standards::account::metadata::Info; +//! use miden_standards::account::metadata::TokenMetadata; +//! use miden_standards::account::faucets::{TokenName, Description, LogoURI}; //! -//! let info = Info::new() -//! .with_name([name_word_0, name_word_1]) -//! .with_description([d0, d1, d2, d3, d4, d5], true) // initialized + mutable -//! .with_logo_uri([l0, l1, l2, l3, l4, l5], false); // initialized + immutable +//! let info = TokenMetadata::new() +//! .with_name(TokenName::new("My Token").unwrap()) +//! .with_description(Description::new("A cool token").unwrap(), true) +//! .with_logo_uri(LogoURI::new("https://example.com/logo.png").unwrap(), false); //! +//! let faucet = BasicFungibleFaucet::new(/* ... */).unwrap().with_info(info); //! let account = AccountBuilder::new(seed) -//! .with_component(info) +//! .with_component(faucet) //! .build()?; //! ``` @@ -85,7 +84,8 @@ use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use thiserror::Error; -use crate::account::components::{metadata_info_component_library, storage_schema_library}; +use crate::account::components::storage_schema_library; +use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; // CONSTANTS — canonical layout: slots 0–22 // ================================================================================================ @@ -108,7 +108,7 @@ pub static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { /// Token name (2 Words = 8 felts), split across 2 slots. /// /// The encoding is not specified; the value is opaque word data. For human-readable names, -/// use UTF-8 encoding via [`name_from_utf8`] / [`name_to_utf8`] or [`Info::with_name_utf8`]. +/// use [`TokenName::new`] / [`TokenName::to_words`] / [`TokenName::try_from_words`]. pub static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::name_0").expect("valid slot name"), @@ -134,57 +134,27 @@ pub enum NameUtf8Error { /// /// Bytes are packed little-endian, 4 bytes per felt (8 felts total). The string is /// zero-padded to 32 bytes. Returns an error if the UTF-8 byte length exceeds 32. +/// +/// Prefer using [`TokenName::new`] + [`TokenName::to_words`] directly. pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { - let bytes = s.as_bytes(); - if bytes.len() > NAME_UTF8_MAX_BYTES { - return Err(NameUtf8Error::TooLong(bytes.len())); - } - let mut padded = [0u8; NAME_UTF8_MAX_BYTES]; - padded[..bytes.len()].copy_from_slice(bytes); - let felts: [Felt; 8] = padded - .chunks_exact(4) - .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap()))) - .collect::>() - .try_into() - .unwrap(); - Ok([ - Word::from([felts[0], felts[1], felts[2], felts[3]]), - Word::from([felts[4], felts[5], felts[6], felts[7]]), - ]) + use crate::account::faucets::TokenName; + Ok(TokenName::new(s)?.to_words()) } /// Decodes the 2-Word name format as UTF-8. /// /// Assumes the name was encoded with [`name_from_utf8`] (little-endian, 4 bytes per felt). /// Trailing zero bytes are trimmed before UTF-8 validation. +/// +/// Prefer using [`TokenName::try_from_words`] directly. pub fn name_to_utf8(words: &[Word; 2]) -> Result { - let mut bytes = [0u8; NAME_UTF8_MAX_BYTES]; - for (i, word) in words.iter().enumerate() { - for (j, f) in word.iter().enumerate() { - let v = f.as_int(); - if v > u32::MAX as u64 { - return Err(NameUtf8Error::InvalidUtf8); - } - bytes[i * 16 + j * 4..][..4].copy_from_slice(&(v as u32).to_le_bytes()); - } - } - let len = bytes.iter().position(|&b| b == 0).unwrap_or(NAME_UTF8_MAX_BYTES); - String::from_utf8(bytes[..len].to_vec()).map_err(|_| NameUtf8Error::InvalidUtf8) + use crate::account::faucets::TokenName; + Ok(TokenName::try_from_words(words)?.as_str().into()) } -/// Initialized config slot: `[desc_init, logo_init, extlink_init, max_supply_mutable]`. -/// -/// Each flag is 0 (not initialized) or 1 (initialized). -/// `max_supply_mutable` appears here for convenient access. -pub static INITIALIZED_CONFIG_SLOT: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::metadata::initialized_config") - .expect("storage slot name should be valid") -}); - /// Mutability config slot: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]`. /// /// Each flag is 0 (immutable) or 1 (mutable / owner can update). -/// `max_supply_mutable` appears here for convenient access. pub static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::metadata::mutability_config") .expect("storage slot name should be valid") @@ -194,40 +164,28 @@ pub static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| /// 6 Words = 24 felts × 8 bytes = 192 bytes. pub const FIELD_MAX_BYTES: usize = 192; -/// Errors when encoding metadata fields from bytes. +/// Errors when encoding or decoding metadata fields. #[derive(Debug, Clone, Error)] pub enum FieldBytesError { /// Field exceeds [`FIELD_MAX_BYTES`]. #[error("field must be at most {FIELD_MAX_BYTES} bytes, got {0}")] TooLong(usize), + /// Decoded bytes are not valid UTF-8. + #[error("field is not valid UTF-8")] + InvalidUtf8, } -/// Encodes a byte slice into 6 Words (24 felts). +/// Encodes a UTF-8 string into 6 Words (24 felts). /// -/// Bytes are packed little-endian, 8 bytes per felt (24 felts total). The slice is zero-padded +/// Bytes are packed little-endian, 8 bytes per felt (24 felts total). The string is zero-padded /// to 192 bytes. Returns an error if the length exceeds 192. +/// +/// Prefer using [`Description::new`] + [`Description::to_words`] (or `LogoURI` / `ExternalLink`) +/// directly. pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 6], FieldBytesError> { - if bytes.len() > FIELD_MAX_BYTES { - return Err(FieldBytesError::TooLong(bytes.len())); - } - let mut padded = [0u8; FIELD_MAX_BYTES]; - padded[..bytes.len()].copy_from_slice(bytes); - let felts: Vec = padded - .chunks_exact(8) - .map(|chunk| { - Felt::try_from(u64::from_le_bytes(chunk.try_into().unwrap())) - .expect("u64 values from 8-byte chunks fit in Felt") - }) - .collect(); - let felts: [Felt; 24] = felts.try_into().unwrap(); - Ok([ - Word::from([felts[0], felts[1], felts[2], felts[3]]), - Word::from([felts[4], felts[5], felts[6], felts[7]]), - Word::from([felts[8], felts[9], felts[10], felts[11]]), - Word::from([felts[12], felts[13], felts[14], felts[15]]), - Word::from([felts[16], felts[17], felts[18], felts[19]]), - Word::from([felts[20], felts[21], felts[22], felts[23]]), - ]) + use crate::account::faucets::Description; + let s = core::str::from_utf8(bytes).map_err(|_| FieldBytesError::InvalidUtf8)?; + Ok(Description::new(s)?.to_words()) } /// Description (6 Words = 24 felts), split across 6 slots. @@ -306,11 +264,6 @@ pub fn owner_config_slot() -> &'static StorageSlotName { &OWNER_CONFIG_SLOT } -/// Returns the [`StorageSlotName`] for the initialized config Word. -pub fn initialized_config_slot() -> &'static StorageSlotName { - &INITIALIZED_CONFIG_SLOT -} - /// Returns the [`StorageSlotName`] for the mutability config Word. pub fn mutability_config_slot() -> &'static StorageSlotName { &MUTABILITY_CONFIG_SLOT @@ -324,29 +277,24 @@ pub fn mutability_config_slot() -> &'static StorageSlotName { /// ## Storage Layout /// /// - Slot 2–3: name (2 Words = 8 felts) -/// - Slot 4: initialized_config `[desc_init, logo_init, extlink_init, max_supply_mutable]` -/// - Slot 5: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` -/// - Slot 6–11: description (6 Words) -/// - Slot 12–17: logo_uri (6 Words) -/// - Slot 18–23: external_link (6 Words) +/// - Slot 4: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +/// - Slot 5–10: description (6 Words) +/// - Slot 11–16: logo_uri (6 Words) +/// - Slot 17–22: external_link (6 Words) #[derive(Debug, Clone, Default)] -pub struct Info { - name: Option<[Word; 2]>, - /// Whether the description field is mutable (owner can update). +pub struct TokenMetadata { + name: Option, + description: Option, + logo_uri: Option, + external_link: Option, description_mutable: bool, - /// Whether the logo URI field is mutable (owner can update). logo_uri_mutable: bool, - /// Whether the external link field is mutable (owner can update). external_link_mutable: bool, - /// If true (1), the owner may call optional_set_max_supply. If false (0), immutable. max_supply_mutable: bool, - description: Option<[Word; 6]>, - logo_uri: Option<[Word; 6]>, - external_link: Option<[Word; 6]>, } -impl Info { - /// Creates a new empty metadata info (all fields absent by default). +impl TokenMetadata { + /// Creates a new empty token metadata (all fields absent by default). pub fn new() -> Self { Self::default() } @@ -358,84 +306,39 @@ impl Info { self } - /// Sets the name metadata (2 Words = 8 felts). - /// - /// Encoding is not specified; for human-readable UTF-8 text use - /// [`with_name_utf8`](Info::with_name_utf8). - pub fn with_name(mut self, name: [Word; 2]) -> Self { + /// Sets the token name. + pub fn with_name(mut self, name: TokenName) -> Self { self.name = Some(name); self } - /// Sets the name from a UTF-8 string (at most [`NAME_UTF8_MAX_BYTES`] bytes). - pub fn with_name_utf8(mut self, s: &str) -> Result { - self.name = Some(name_from_utf8(s)?); - Ok(self) - } - - /// Sets the description metadata (6 Words) with mutability. + /// Sets the description with mutability. /// /// When `mutable` is `true`, the owner can update the description later. - /// The field is always marked as initialized when data is provided. - pub fn with_description(mut self, description: [Word; 6], mutable: bool) -> Self { + pub fn with_description(mut self, description: Description, mutable: bool) -> Self { self.description = Some(description); self.description_mutable = mutable; self } - /// Sets the description from a byte slice (at most [`FIELD_MAX_BYTES`] bytes). - pub fn with_description_from_bytes( - mut self, - bytes: &[u8], - mutable: bool, - ) -> Result { - self.description = Some(field_from_bytes(bytes)?); - self.description_mutable = mutable; - Ok(self) - } - - /// Sets the logo URI metadata (6 Words) with mutability. + /// Sets the logo URI with mutability. /// /// When `mutable` is `true`, the owner can update the logo URI later. - /// The field is always marked as initialized when data is provided. - pub fn with_logo_uri(mut self, logo_uri: [Word; 6], mutable: bool) -> Self { + pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self { self.logo_uri = Some(logo_uri); self.logo_uri_mutable = mutable; self } - /// Sets the logo URI from a byte slice (at most [`FIELD_MAX_BYTES`] bytes). - pub fn with_logo_uri_from_bytes( - mut self, - bytes: &[u8], - mutable: bool, - ) -> Result { - self.logo_uri = Some(field_from_bytes(bytes)?); - self.logo_uri_mutable = mutable; - Ok(self) - } - - /// Sets the external link metadata (6 Words) with mutability. + /// Sets the external link with mutability. /// /// When `mutable` is `true`, the owner can update the external link later. - /// The field is always marked as initialized when data is provided. - pub fn with_external_link(mut self, external_link: [Word; 6], mutable: bool) -> Self { + pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self { self.external_link = Some(external_link); self.external_link_mutable = mutable; self } - /// Sets the external link from a byte slice (at most [`FIELD_MAX_BYTES`] bytes). - pub fn with_external_link_from_bytes( - mut self, - bytes: &[u8], - mutable: bool, - ) -> Result { - self.external_link = Some(field_from_bytes(bytes)?); - self.external_link_mutable = mutable; - Ok(self) - } - /// Returns the slot name for name chunk 0. pub fn name_chunk_0_slot() -> &'static StorageSlotName { &NAME_SLOTS[0] @@ -464,18 +367,21 @@ impl Info { /// Reads the name and optional metadata fields from account storage. /// /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if - /// at least one word is non-zero. - #[allow(clippy::type_complexity)] + /// at least one word is non-zero. Decoding errors (e.g. invalid UTF-8 in storage) cause the + /// field to be returned as `None`. pub fn read_metadata_from_storage( storage: &AccountStorage, - ) -> (Option<[Word; 2]>, Option<[Word; 6]>, Option<[Word; 6]>, Option<[Word; 6]>) { - // Read name + ) -> (Option, Option, Option, Option) { let name = if let (Ok(chunk_0), Ok(chunk_1)) = ( - storage.get_item(Info::name_chunk_0_slot()), - storage.get_item(Info::name_chunk_1_slot()), + storage.get_item(TokenMetadata::name_chunk_0_slot()), + storage.get_item(TokenMetadata::name_chunk_1_slot()), ) { - let name: [Word; 2] = [chunk_0, chunk_1]; - if name != [Word::default(); 2] { Some(name) } else { None } + let words: [Word; 2] = [chunk_0, chunk_1]; + if words != [Word::default(); 2] { + TokenName::try_from_words(&words).ok() + } else { + None + } } else { None }; @@ -494,82 +400,65 @@ impl Info { if any_set { Some(field) } else { None } }; - let description = read_field(&DESCRIPTION_SLOTS); - let logo_uri = read_field(&LOGO_URI_SLOTS); - let external_link = read_field(&EXTERNAL_LINK_SLOTS); + let description = + read_field(&DESCRIPTION_SLOTS).and_then(|w| Description::try_from_words(&w).ok()); + let logo_uri = read_field(&LOGO_URI_SLOTS).and_then(|w| LogoURI::try_from_words(&w).ok()); + let external_link = + read_field(&EXTERNAL_LINK_SLOTS).and_then(|w| ExternalLink::try_from_words(&w).ok()); (name, description, logo_uri, external_link) } } -impl From for AccountComponent { - fn from(extension: Info) -> Self { - let mut storage_slots: Vec = Vec::new(); - - if let Some(name) = extension.name { - storage_slots.push(StorageSlot::with_value(Info::name_chunk_0_slot().clone(), name[0])); - storage_slots.push(StorageSlot::with_value(Info::name_chunk_1_slot().clone(), name[1])); - } - - let desc_initialized = extension.description.is_some(); - let logo_initialized = extension.logo_uri.is_some(); - let extlink_initialized = extension.external_link.is_some(); - - // Initialized config word: [desc_init, logo_init, extlink_init, max_supply_mutable] - let initialized_config_word = Word::from([ - Felt::from(desc_initialized as u32), - Felt::from(logo_initialized as u32), - Felt::from(extlink_initialized as u32), - Felt::from(extension.max_supply_mutable as u32), - ]); - storage_slots.push(StorageSlot::with_value( - initialized_config_slot().clone(), - initialized_config_word, +impl TokenMetadata { + /// Returns the storage slots for this metadata (without creating an `AccountComponent`). + /// + /// These slots are meant to be included directly in a faucet component rather than + /// added as a separate `AccountComponent`. + pub fn storage_slots(&self) -> Vec { + let mut slots: Vec = Vec::new(); + + let name_words = self.name.as_ref().map(|n| n.to_words()).unwrap_or_default(); + slots.push(StorageSlot::with_value( + TokenMetadata::name_chunk_0_slot().clone(), + name_words[0], + )); + slots.push(StorageSlot::with_value( + TokenMetadata::name_chunk_1_slot().clone(), + name_words[1], )); - // Mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable] let mutability_config_word = Word::from([ - Felt::from(extension.description_mutable as u32), - Felt::from(extension.logo_uri_mutable as u32), - Felt::from(extension.external_link_mutable as u32), - Felt::from(extension.max_supply_mutable as u32), + Felt::from(self.description_mutable as u32), + Felt::from(self.logo_uri_mutable as u32), + Felt::from(self.external_link_mutable as u32), + Felt::from(self.max_supply_mutable as u32), ]); - storage_slots.push(StorageSlot::with_value( + slots.push(StorageSlot::with_value( mutability_config_slot().clone(), mutability_config_word, )); - // Description slots (always write 6 slots if initialized) - if let Some(description) = extension.description { - for (i, word) in description.iter().enumerate() { - storage_slots - .push(StorageSlot::with_value(Info::description_slot(i).clone(), *word)); - } + let desc_words: [Word; 6] = + self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); + for (i, word) in desc_words.iter().enumerate() { + slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word)); } - // Logo URI slots - if let Some(logo_uri) = extension.logo_uri { - for (i, word) in logo_uri.iter().enumerate() { - storage_slots.push(StorageSlot::with_value(Info::logo_uri_slot(i).clone(), *word)); - } + let logo_words: [Word; 6] = + self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); + for (i, word) in logo_words.iter().enumerate() { + slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word)); } - // External link slots - if let Some(external_link) = extension.external_link { - for (i, word) in external_link.iter().enumerate() { - storage_slots - .push(StorageSlot::with_value(Info::external_link_slot(i).clone(), *word)); - } + let link_words: [Word; 6] = + self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); + for (i, word) in link_words.iter().enumerate() { + slots + .push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word)); } - let metadata = AccountComponentMetadata::new("miden::standards::metadata::info") - .with_description( - "Metadata info (name, config, description, logo URI, external link) in fixed value slots", - ) - .with_supports_all_types(); - - AccountComponent::new(metadata_info_component_library(), storage_slots, metadata) - .expect("Info component should satisfy the requirements") + slots } } @@ -714,177 +603,139 @@ fn compute_schema_commitment<'a>( #[cfg(test)] mod tests { use miden_protocol::Word; - use miden_protocol::account::AccountBuilder; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; + use miden_protocol::account::{Account, AccountBuilder}; use super::{ AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment, - FIELD_MAX_BYTES, - FieldBytesError, - Info, NAME_UTF8_MAX_BYTES, - NameUtf8Error, - field_from_bytes, - initialized_config_slot, + TokenMetadata as InfoType, mutability_config_slot, - name_from_utf8, - name_to_utf8, }; use crate::account::auth::{AuthSingleSig, NoAuth}; + use crate::account::faucets::{BasicFungibleFaucet, Description, TokenName}; + + fn build_account_with_info(info: InfoType) -> Account { + let name = TokenName::new("T").unwrap(); + let faucet = BasicFungibleFaucet::new( + miden_protocol::asset::TokenSymbol::new("TST").unwrap(), + 2, + miden_protocol::Felt::new(1_000), + name, + None, + None, + None, + ) + .unwrap() + .with_info(info); + AccountBuilder::new([1u8; 32]) + .account_type(miden_protocol::account::AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(faucet) + .build() + .unwrap() + } #[test] fn metadata_info_can_store_name_and_description() { - let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; - let description = [ - Word::from([10u32, 11, 12, 13]), - Word::from([14u32, 15, 16, 17]), - Word::from([18u32, 19, 20, 21]), - Word::from([22u32, 23, 24, 25]), - Word::from([26u32, 27, 28, 29]), - Word::from([30u32, 31, 32, 33]), - ]; - - let extension = Info::new().with_name(name).with_description(description, false); + let name = TokenName::new("test_name").unwrap(); + let description = Description::new("test description").unwrap(); - let account = AccountBuilder::new([1u8; 32]) - .with_auth_component(NoAuth) - .with_component(extension) - .build() - .unwrap(); + let name_words = name.to_words(); + let desc_words = description.to_words(); + + let info = InfoType::new().with_name(name).with_description(description, false); + let account = build_account_with_info(info); - // Verify name chunks - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); - assert_eq!(name_0, name[0]); - assert_eq!(name_1, name[1]); + let name_0 = account.storage().get_item(InfoType::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(InfoType::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); - // Verify description chunks - for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(InfoType::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } } #[test] fn metadata_info_empty_works() { - let extension = Info::new(); - - let _account = AccountBuilder::new([1u8; 32]) - .with_auth_component(NoAuth) - .with_component(extension) - .build() - .unwrap(); + let _account = build_account_with_info(InfoType::new()); } #[test] fn config_slots_set_correctly() { use miden_protocol::Felt; - // Info with description mutable, max_supply_mutable = true - let info = Info::new() - .with_description([Word::default(); 6], true) + let info = InfoType::new() + .with_description(Description::new("test").unwrap(), true) .with_max_supply_mutable(true); - let account = AccountBuilder::new([2u8; 32]) - .with_auth_component(NoAuth) - .with_component(info) - .build() - .unwrap(); - - // Check initialized config - let init_word = account.storage().get_item(initialized_config_slot()).unwrap(); - assert_eq!(init_word[0], Felt::from(1u32), "desc_init should be 1"); - assert_eq!(init_word[1], Felt::from(0u32), "logo_init should be 0"); - assert_eq!(init_word[2], Felt::from(0u32), "extlink_init should be 0"); - assert_eq!(init_word[3], Felt::from(1u32), "max_supply_mutable should be 1"); + let account = build_account_with_info(info); - // Check mutability config let mut_word = account.storage().get_item(mutability_config_slot()).unwrap(); assert_eq!(mut_word[0], Felt::from(1u32), "desc_mutable should be 1"); assert_eq!(mut_word[1], Felt::from(0u32), "logo_mutable should be 0"); assert_eq!(mut_word[2], Felt::from(0u32), "extlink_mutable should be 0"); assert_eq!(mut_word[3], Felt::from(1u32), "max_supply_mutable should be 1"); - // Info with defaults (all flags 0) - let account_default = AccountBuilder::new([3u8; 32]) - .with_auth_component(NoAuth) - .with_component(Info::new()) - .build() - .unwrap(); - let init_default = account_default.storage().get_item(initialized_config_slot()).unwrap(); - assert_eq!(init_default[0], Felt::from(0u32), "desc_init should be 0 by default"); - assert_eq!(init_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); + let account_default = build_account_with_info(InfoType::new()); let mut_default = account_default.storage().get_item(mutability_config_slot()).unwrap(); assert_eq!(mut_default[0], Felt::from(0u32), "desc_mutable should be 0 by default"); assert_eq!(mut_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); } #[test] - fn name_utf8_roundtrip() { + fn name_roundtrip() { let s = "POL Faucet"; - let words = name_from_utf8(s).unwrap(); - let decoded = name_to_utf8(&words).unwrap(); - assert_eq!(decoded, s); + let name = TokenName::new(s).unwrap(); + let words = name.to_words(); + let decoded = TokenName::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), s); } #[test] - fn name_utf8_max_32_bytes_accepted() { + fn name_max_32_bytes_accepted() { let s = "a".repeat(NAME_UTF8_MAX_BYTES); assert_eq!(s.len(), 32); - let words = name_from_utf8(&s).unwrap(); - let decoded = name_to_utf8(&words).unwrap(); - assert_eq!(decoded, s); + let name = TokenName::new(&s).unwrap(); + let words = name.to_words(); + let decoded = TokenName::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), s); } #[test] - fn name_utf8_too_long_errors() { + fn name_too_long_errors() { let s = "a".repeat(33); - assert!(matches!(name_from_utf8(&s), Err(NameUtf8Error::TooLong(33)))); + assert!(TokenName::new(&s).is_err()); } #[test] - fn field_192_bytes_accepted() { - let bytes = [0u8; FIELD_MAX_BYTES]; - let words = field_from_bytes(&bytes).unwrap(); - assert_eq!(words.len(), 6); + fn description_max_bytes_accepted() { + let s = "a".repeat(Description::MAX_BYTES); + let desc = Description::new(&s).unwrap(); + assert_eq!(desc.to_words().len(), 6); } #[test] - fn field_193_bytes_rejected() { - let bytes = [0u8; 193]; - assert!(matches!(field_from_bytes(&bytes), Err(FieldBytesError::TooLong(193)))); - } - - #[test] - fn metadata_info_with_name_utf8() { - let extension = Info::new().with_name_utf8("My Token").unwrap(); - let account = AccountBuilder::new([1u8; 32]) - .with_auth_component(NoAuth) - .with_component(extension) - .build() - .unwrap(); - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); - let decoded = name_to_utf8(&[name_0, name_1]).unwrap(); - assert_eq!(decoded, "My Token"); + fn description_too_long_rejected() { + let s = "a".repeat(193); + assert!(Description::new(&s).is_err()); } #[test] - fn metadata_info_name_only_works() { - let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; - let extension = Info::new().with_name(name); - - let account = AccountBuilder::new([1u8; 32]) - .with_auth_component(NoAuth) - .with_component(extension) - .build() - .unwrap(); - - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); - assert_eq!(name_0, name[0]); - assert_eq!(name_1, name[1]); + fn metadata_info_with_name() { + let name = TokenName::new("My Token").unwrap(); + let name_words = name.to_words(); + let info = InfoType::new().with_name(name); + let account = build_account_with_info(info); + let name_0 = account.storage().get_item(InfoType::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(InfoType::name_chunk_1_slot()).unwrap(); + let decoded = TokenName::try_from_words(&[name_0, name_1]).unwrap(); + assert_eq!(decoded.as_str(), "My Token"); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); } #[test] diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index ac30e6201c..9cb5207e20 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -46,8 +46,15 @@ use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, OutputNote, TransactionKernel}; use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word}; -use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet, TokenName}; -use miden_standards::account::metadata::Info; +use miden_standards::account::faucets::{ + BasicFungibleFaucet, + Description, + ExternalLink, + LogoURI, + NetworkFungibleFaucet, + TokenName, +}; +use miden_standards::account::metadata::TokenMetadata as TokenMetadataInfo; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote}; use miden_standards::testing::account_component::MockAccountComponent; @@ -306,8 +313,8 @@ impl MockChainBuilder { token_symbol: &str, max_supply: u64, ) -> anyhow::Result { - let name = TokenName::try_from(token_symbol) - .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); + let name = TokenName::new(token_symbol) + .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; @@ -336,8 +343,8 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; - let name = TokenName::try_from(token_symbol) - .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); + let name = TokenName::new(token_symbol) + .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; let basic_faucet = BasicFungibleFaucet::new( @@ -372,11 +379,13 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; - let name = TokenName::try_from(token_symbol) - .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); + let name = TokenName::new(token_symbol) + .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; + let info = TokenMetadataInfo::new().with_name(name.clone()); + let network_faucet = NetworkFungibleFaucet::new( token_symbol, DEFAULT_FAUCET_DECIMALS, @@ -388,14 +397,12 @@ impl MockChainBuilder { None, ) .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")?; - - let info = Info::new().with_name(name.as_words()); + .context("failed to create network fungible faucet")? + .with_info(info); let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) .with_component(network_faucet) - .with_component(info) .account_type(AccountType::FungibleFaucet); // Network faucets always use IncrNonce auth (no authentication) @@ -421,11 +428,33 @@ impl MockChainBuilder { .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?; let token_supply = Felt::try_from(token_supply.unwrap_or(0)) .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?; - let name = TokenName::try_from(token_symbol) - .unwrap_or_else(|_| TokenName::try_from("").expect("empty name should be valid")); + let name = TokenName::new(token_symbol) + .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; + let mut info = TokenMetadataInfo::new() + .with_name(name.clone()) + .with_max_supply_mutable(max_supply_mutable); + if let Some((words, mutable)) = description { + info = info.with_description( + Description::try_from_words(&words).expect("valid description words"), + mutable, + ); + } + if let Some((words, mutable)) = logo_uri { + info = info.with_logo_uri( + LogoURI::try_from_words(&words).expect("valid logo_uri words"), + mutable, + ); + } + if let Some((words, mutable)) = external_link { + info = info.with_external_link( + ExternalLink::try_from_words(&words).expect("valid external_link words"), + mutable, + ); + } + let network_faucet = NetworkFungibleFaucet::new( token_symbol, DEFAULT_FAUCET_DECIMALS, @@ -437,25 +466,12 @@ impl MockChainBuilder { None, ) .and_then(|f| f.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")?; - - let mut info = Info::new() - .with_name(name.as_words()) - .with_max_supply_mutable(max_supply_mutable); - if let Some((words, mutable)) = description { - info = info.with_description(words, mutable); - } - if let Some((words, mutable)) = logo_uri { - info = info.with_logo_uri(words, mutable); - } - if let Some((words, mutable)) = external_link { - info = info.with_external_link(words, mutable); - } + .context("failed to create network fungible faucet")? + .with_info(info); let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) .with_component(network_faucet) - .with_component(info) .account_type(AccountType::FungibleFaucet); self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 8b40e17584..ce2b417b2a 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -23,7 +23,7 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::note::{NoteAssets, NoteScript, NoteType}; use miden_protocol::transaction::OutputNote; -use miden_standards::account::faucets::TokenMetadata; +use miden_standards::account::faucets::FungibleTokenMetadata; use miden_standards::note::StandardNote; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use miden_tx::utils::hex_to_bytes; @@ -262,7 +262,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { // STEP 3: CONSUME ALL BURN NOTES WITH THE AGGLAYER FAUCET // -------------------------------------------------------------------------------------------- - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let initial_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( initial_token_supply, Felt::new(total_burned), @@ -286,7 +286,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { mock_chain.prove_next_block()?; } - let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let final_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( final_token_supply, Felt::new(initial_token_supply.as_int() - total_burned), diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index d95856800e..04d9908a99 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -4,6 +4,7 @@ extern crate alloc; use alloc::sync::Arc; +use miden_crypto::hash::rpo::Rpo256 as Hasher; use miden_processor::crypto::RpoRandomCoin; use miden_protocol::account::{ AccountBuilder, @@ -16,34 +17,60 @@ use miden_protocol::assembly::DefaultSourceManager; use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::{Felt, Word}; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::TokenName; +use miden_standards::account::faucets::{ + BasicFungibleFaucet, + Description, + ExternalLink, + LogoURI, + TokenName, +}; use miden_standards::account::metadata::{ DESCRIPTION_DATA_KEY, + EXTERNAL_LINK_DATA_KEY, FieldBytesError, - Info, + LOGO_URI_DATA_KEY, NAME_UTF8_MAX_BYTES, + TokenMetadata as Info, field_from_bytes, mutability_config_slot, }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ ERR_DESCRIPTION_NOT_MUTABLE, + ERR_EXTERNAL_LINK_NOT_MUTABLE, + ERR_LOGO_URI_NOT_MUTABLE, ERR_MAX_SUPPLY_IMMUTABLE, ERR_SENDER_NOT_OWNER, }; use miden_standards::testing::note::NoteBuilder; use miden_testing::{MockChain, TransactionContextBuilder, assert_transaction_executor_error}; +fn build_faucet_with_info(info: Info) -> BasicFungibleFaucet { + BasicFungibleFaucet::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + None, + ) + .unwrap() + .with_info(info) +} + /// Tests that the metadata extension can store and retrieve name via MASM. #[tokio::test] async fn metadata_info_get_name_from_masm() -> anyhow::Result<()> { - let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; + let token_name = TokenName::new("test name").unwrap(); + let name = token_name.to_words(); - let extension = Info::new().with_name(name); + let extension = Info::new().with_name(token_name); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build()?; // MASM script to read name and verify values @@ -86,13 +113,12 @@ async fn metadata_info_get_name_from_masm() -> anyhow::Result<()> { /// Tests that reading zero-valued name returns empty words. #[tokio::test] async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { - // Create extension with zero-valued name (slots exist, but contain zeros) - let name = [Word::default(), Word::default()]; - let extension = Info::new().with_name(name); + let extension = Info::new(); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build()?; let tx_script = r#" @@ -119,36 +145,35 @@ async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { Ok(()) } -/// Tests that get_description (first word of 6) can be read via MASM. +/// Tests that get_description_commitment returns the RPO256 hash of the 6 description words. #[tokio::test] async fn metadata_info_get_description_from_masm() -> anyhow::Result<()> { - let description = [ - Word::from([10u32, 11, 12, 13]), - Word::from([14u32, 15, 16, 17]), - Word::from([18u32, 19, 20, 21]), - Word::from([22u32, 23, 24, 25]), - Word::from([26u32, 27, 28, 29]), - Word::from([30u32, 31, 32, 33]), - ]; + let desc_text = "some test description text"; + let description_typed = Description::new(desc_text).unwrap(); + let description = description_typed.to_words(); - let extension = Info::new().with_description(description, false); + let extension = Info::new().with_description(description_typed, false); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build()?; + let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); + let expected_commitment = Hasher::hash_elements(&desc_felts); + let tx_script = format!( r#" begin - call.::miden::standards::metadata::fungible::get_description - # => [DESCRIPTION_0, pad(12)] + call.::miden::standards::metadata::fungible::get_description_commitment + # => [COMMITMENT, pad(12)] - push.{expected_0} - assert_eqw.err="description_0 does not match" + push.{expected} + assert_eqw.err="description commitment does not match" end "#, - expected_0 = description[0], + expected = expected_commitment, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -170,37 +195,32 @@ async fn metadata_info_get_description_from_masm() -> anyhow::Result<()> { fn metadata_info_with_faucet_storage() { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; - use miden_standards::account::faucets::BasicFungibleFaucet; - let name = [Word::from([111u32, 222, 333, 444]), Word::from([555u32, 666, 777, 888])]; - let description = [ - Word::from([10u32, 20, 30, 40]), - Word::from([50u32, 60, 70, 80]), - Word::from([90u32, 100, 110, 120]), - Word::from([130u32, 140, 150, 160]), - Word::from([170u32, 180, 190, 200]), - Word::from([210u32, 220, 230, 240]), - ]; + let token_name = TokenName::new("test faucet name").unwrap(); + let name = token_name.to_words(); + let desc_text = "faucet description text for testing"; + let description_typed = Description::new(desc_text).unwrap(); + let description = description_typed.to_words(); + + let extension = Info::new().with_name(token_name).with_description(description_typed, false); let faucet = BasicFungibleFaucet::new( "TST".try_into().unwrap(), 8, // decimals Felt::new(1_000_000), // max_supply - TokenName::try_from("TST").unwrap(), + TokenName::new("TST").unwrap(), None, None, None, ) - .unwrap(); - - let extension = Info::new().with_name(name).with_description(description, false); + .unwrap() + .with_info(extension); let account = AccountBuilder::new([1u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Public) .with_auth_component(NoAuth) .with_component(faucet) - .with_component(extension) .build() .unwrap(); @@ -227,10 +247,12 @@ fn metadata_info_with_faucet_storage() { #[test] fn name_32_bytes_accepted() { let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); - let extension = Info::new().with_name_utf8(&max_name).unwrap(); + let token_name = TokenName::new(&max_name).unwrap(); + let extension = Info::new().with_name(token_name); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build() .unwrap(); let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); @@ -243,7 +265,7 @@ fn name_32_bytes_accepted() { #[test] fn name_33_bytes_rejected() { let too_long = "a".repeat(33); - let result = Info::new().with_name_utf8(&too_long); + let result = TokenName::new(&too_long); assert!(result.is_err()); assert!(matches!( result, @@ -254,18 +276,14 @@ fn name_33_bytes_rejected() { /// Tests that description at full capacity (6 Words) is supported. #[test] fn description_6_words_full_capacity() { - let description = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - ]; - let extension = Info::new().with_description(description, false); + let desc_text = "a".repeat(Description::MAX_BYTES); + let description_typed = Description::new(&desc_text).unwrap(); + let description = description_typed.to_words(); + let extension = Info::new().with_description(description_typed, false); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build() .unwrap(); for (i, expected) in description.iter().enumerate() { @@ -277,7 +295,8 @@ fn description_6_words_full_capacity() { /// Tests that field longer than 192 bytes (193 bytes) is rejected. #[test] fn field_193_bytes_rejected() { - let result = field_from_bytes(&[0u8; 193]); + let long_string = "a".repeat(193); + let result = field_from_bytes(long_string.as_bytes()); assert!(result.is_err()); assert!(matches!(result, Err(FieldBytesError::TooLong(193)))); } @@ -287,36 +306,30 @@ fn field_193_bytes_rejected() { fn faucet_with_integrated_metadata() { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; - use miden_standards::account::faucets::BasicFungibleFaucet; - let name = [Word::from([11u32, 22, 33, 44]), Word::from([55u32, 66, 77, 88])]; - let description = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - ]; + let token_name = TokenName::new("integrated name").unwrap(); + let name = token_name.to_words(); + let desc_text = "integrated description text"; + let description_typed = Description::new(desc_text).unwrap(); + let description = description_typed.to_words(); let faucet = BasicFungibleFaucet::new( "INT".try_into().unwrap(), 6, // decimals Felt::new(500_000), // max_supply - TokenName::try_from("INT").unwrap(), + TokenName::new("INT").unwrap(), None, None, None, ) .unwrap(); - let extension = Info::new().with_name(name).with_description(description, false); + let extension = Info::new().with_name(token_name).with_description(description_typed, false); let account = AccountBuilder::new([2u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Public) .with_auth_component(NoAuth) - .with_component(faucet) - .with_component(extension) + .with_component(faucet.with_info(extension)) .build() .unwrap(); @@ -348,39 +361,31 @@ fn faucet_with_integrated_metadata() { #[test] fn faucet_initialized_with_max_name_and_full_description() { use miden_protocol::account::AccountStorageMode; - use miden_standards::account::faucets::BasicFungibleFaucet; let max_name = "0".repeat(NAME_UTF8_MAX_BYTES); - let description = [ - Word::from([101u32, 102, 103, 104]), - Word::from([105u32, 106, 107, 108]), - Word::from([109u32, 110, 111, 112]), - Word::from([113u32, 114, 115, 116]), - Word::from([117u32, 118, 119, 120]), - Word::from([121u32, 122, 123, 124]), - ]; + let desc_text = "a".repeat(Description::MAX_BYTES); + let description_typed = Description::new(&desc_text).unwrap(); + let description = description_typed.to_words(); let faucet = BasicFungibleFaucet::new( "MAX".try_into().unwrap(), 6, Felt::new(1_000_000), - TokenName::try_from("MAX").unwrap(), + TokenName::new("MAX").unwrap(), None, None, None, ) .unwrap(); let extension = Info::new() - .with_name_utf8(&max_name) - .unwrap() - .with_description(description, false); + .with_name(TokenName::new(&max_name).unwrap()) + .with_description(description_typed, false); let account = AccountBuilder::new([5u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Public) .with_auth_component(NoAuth) - .with_component(faucet) - .with_component(extension) + .with_component(faucet.with_info(extension)) .build() .unwrap(); @@ -411,21 +416,16 @@ fn network_faucet_initialized_with_max_name_and_full_description() { ); let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); - let description = [ - Word::from([201u32, 202, 203, 204]), - Word::from([205u32, 206, 207, 208]), - Word::from([209u32, 210, 211, 212]), - Word::from([213u32, 214, 215, 216]), - Word::from([217u32, 218, 219, 220]), - Word::from([221u32, 222, 223, 224]), - ]; + let desc_text = "a".repeat(Description::MAX_BYTES); + let description_typed = Description::new(&desc_text).unwrap(); + let description = description_typed.to_words(); let network_faucet = NetworkFungibleFaucet::new( "NET".try_into().unwrap(), 6, Felt::new(2_000_000), owner_account_id, - TokenName::try_from("NET").unwrap(), + TokenName::new("NET").unwrap(), None, None, None, @@ -435,16 +435,14 @@ fn network_faucet_initialized_with_max_name_and_full_description() { .unwrap(); let extension = Info::new() - .with_name_utf8(&max_name) - .unwrap() - .with_description(description, false); + .with_name(TokenName::new(&max_name).unwrap()) + .with_description(description_typed, false); let account = AccountBuilder::new([6u8; 32]) .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(NoAuth) - .with_component(network_faucet) - .with_component(extension) + .with_component(network_faucet.with_info(extension)) .build() .unwrap(); @@ -477,21 +475,16 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( let max_name = "b".repeat(NAME_UTF8_MAX_BYTES); let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); - let description = [ - Word::from([301u32, 302, 303, 304]), - Word::from([305u32, 306, 307, 308]), - Word::from([309u32, 310, 311, 312]), - Word::from([313u32, 314, 315, 316]), - Word::from([317u32, 318, 319, 320]), - Word::from([321u32, 322, 323, 324]), - ]; + let desc_text = "network faucet description"; + let description_typed = Description::new(desc_text).unwrap(); + let description = description_typed.to_words(); let network_faucet = NetworkFungibleFaucet::new( "MAS".try_into().unwrap(), 6, Felt::new(1_000_000), owner_account_id, - TokenName::try_from("MAS").unwrap(), + TokenName::new("MAS").unwrap(), None, None, None, @@ -501,18 +494,19 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( .unwrap(); let extension = Info::new() - .with_name_utf8(&max_name) - .unwrap() - .with_description(description, false); + .with_name(TokenName::new(&max_name).unwrap()) + .with_description(description_typed, false); let account = AccountBuilder::new([7u8; 32]) .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(NoAuth) - .with_component(network_faucet) - .with_component(extension) + .with_component(network_faucet.with_info(extension)) .build()?; + let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); + let expected_desc_commitment = Hasher::hash_elements(&desc_felts); + let tx_script = format!( r#" begin @@ -522,15 +516,15 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( push.{expected_name_1} assert_eqw.err="network faucet name chunk 1 does not match" - call.::miden::standards::metadata::fungible::get_description - # => [DESCRIPTION_0, pad(12)] - push.{expected_desc_0} - assert_eqw.err="network faucet description_0 does not match" + call.::miden::standards::metadata::fungible::get_description_commitment + # => [COMMITMENT, pad(12)] + push.{expected_desc_commitment} + assert_eqw.err="network faucet description commitment does not match" end "#, expected_name_0 = name_words[0], expected_name_1 = name_words[1], - expected_desc_0 = description[0], + expected_desc_commitment = expected_desc_commitment, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -553,7 +547,6 @@ async fn faucet_get_decimals_only() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::BasicFungibleFaucet; let token_symbol = TokenSymbol::new("POL").unwrap(); let decimals: u8 = 8; @@ -563,7 +556,7 @@ async fn faucet_get_decimals_only() -> anyhow::Result<()> { token_symbol, decimals, max_supply, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -611,7 +604,6 @@ async fn faucet_get_token_symbol_only() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::BasicFungibleFaucet; let token_symbol = TokenSymbol::new("POL").unwrap(); let decimals: u8 = 8; @@ -621,7 +613,7 @@ async fn faucet_get_token_symbol_only() -> anyhow::Result<()> { token_symbol, decimals, max_supply, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -669,7 +661,6 @@ async fn faucet_get_token_supply_only() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::BasicFungibleFaucet; let token_symbol = TokenSymbol::new("POL").unwrap(); let decimals: u8 = 8; @@ -680,7 +671,7 @@ async fn faucet_get_token_supply_only() -> anyhow::Result<()> { token_symbol, decimals, max_supply, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -728,7 +719,6 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::BasicFungibleFaucet; let token_symbol = TokenSymbol::new("POL").unwrap(); let decimals: u8 = 8; @@ -738,7 +728,7 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { token_symbol, decimals, max_supply, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -787,12 +777,14 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { /// Isolated test: only get_name. #[tokio::test] async fn metadata_get_name_only() -> anyhow::Result<()> { - let name = [Word::from([1u32, 2, 3, 4]), Word::from([5u32, 6, 7, 8])]; - let extension = Info::new().with_name(name); + let token_name = TokenName::new("test name").unwrap(); + let name = token_name.to_words(); + let extension = Info::new().with_name(token_name); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build()?; let tx_script = format!( @@ -823,34 +815,33 @@ async fn metadata_get_name_only() -> anyhow::Result<()> { Ok(()) } -/// Isolated test: only get_description. +/// Isolated test: get_description_commitment returns the RPO256 hash of the 6 description words. #[tokio::test] async fn metadata_get_description_only() -> anyhow::Result<()> { - let description = [ - Word::from([10u32, 11, 12, 13]), - Word::from([14u32, 15, 16, 17]), - Word::from([18u32, 19, 20, 21]), - Word::from([22u32, 23, 24, 25]), - Word::from([26u32, 27, 28, 29]), - Word::from([30u32, 31, 32, 33]), - ]; - let extension = Info::new().with_description(description, false); + let desc_text = "some test description text"; + let description_typed = Description::new(desc_text).unwrap(); + let description = description_typed.to_words(); + let extension = Info::new().with_description(description_typed, false); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build()?; + let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); + let expected_commitment = Hasher::hash_elements(&desc_felts); + let tx_script = format!( r#" begin - call.::miden::standards::metadata::fungible::get_description - # => [DESCRIPTION_0, pad(12)] - push.{expected_0} - assert_eqw.err="description_0 does not match" + call.::miden::standards::metadata::fungible::get_description_commitment + # => [COMMITMENT, pad(12)] + push.{expected} + assert_eqw.err="description commitment does not match" end "#, - expected_0 = description[0], + expected = expected_commitment, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -867,32 +858,21 @@ async fn metadata_get_description_only() -> anyhow::Result<()> { Ok(()) } -/// Isolated test: get_initialized_config and get_mutability_config. +/// Isolated test: get_mutability_config. #[tokio::test] async fn metadata_get_config_only() -> anyhow::Result<()> { let extension = Info::new() - .with_description([Word::default(); 6], true) // mutable + .with_description(Description::new("test").unwrap(), true) // mutable .with_max_supply_mutable(true); let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(extension) + .with_component(build_faucet_with_info(extension)) .build()?; let tx_script = r#" begin - # Check initialized config - call.::miden::standards::metadata::fungible::get_initialized_config - # => [max_supply_mutable, extlink_init, logo_init, desc_init, pad(12)] - push.1 - assert_eq.err="max_supply_mutable should be 1" - push.0 - assert_eq.err="extlink_init should be 0" - push.0 - assert_eq.err="logo_init should be 0" - push.1 - assert_eq.err="desc_init should be 1" - # Check mutability config call.::miden::standards::metadata::fungible::get_mutability_config # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] @@ -944,7 +924,7 @@ async fn metadata_get_owner_only() -> anyhow::Result<()> { decimals, max_supply, owner_account_id, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -996,7 +976,6 @@ async fn faucet_get_max_supply_only() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::BasicFungibleFaucet; let token_symbol = TokenSymbol::new("POL").unwrap(); let decimals: u8 = 8; @@ -1006,7 +985,7 @@ async fn faucet_get_max_supply_only() -> anyhow::Result<()> { token_symbol, decimals, max_supply, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -1054,7 +1033,6 @@ async fn faucet_get_decimals_and_symbol_from_masm() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::BasicFungibleFaucet; let token_symbol = TokenSymbol::new("POL").unwrap(); let decimals: u8 = 8; @@ -1064,7 +1042,7 @@ async fn faucet_get_decimals_and_symbol_from_masm() -> anyhow::Result<()> { token_symbol, decimals, max_supply, - TokenName::try_from("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -1130,39 +1108,36 @@ async fn faucet_get_decimals_and_symbol_from_masm() -> anyhow::Result<()> { async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { use miden_protocol::Felt; use miden_protocol::account::AccountStorageMode; - use miden_standards::account::faucets::BasicFungibleFaucet; - let name = [Word::from([100u32, 200, 300, 400]), Word::from([500u32, 600, 700, 800])]; - let description = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - ]; + let token_name = TokenName::new("readable name").unwrap(); + let name = token_name.to_words(); + let desc_text = "readable description"; + let description_typed = Description::new(desc_text).unwrap(); + let description = description_typed.to_words(); let faucet = BasicFungibleFaucet::new( "MAS".try_into().unwrap(), 10, // decimals Felt::new(999_999), // max_supply - TokenName::try_from("MAS").unwrap(), + TokenName::new("MAS").unwrap(), None, None, None, ) .unwrap(); - let extension = Info::new().with_name(name).with_description(description, false); + let extension = Info::new().with_name(token_name).with_description(description_typed, false); let account = AccountBuilder::new([3u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Public) .with_auth_component(NoAuth) - .with_component(faucet) - .with_component(extension) + .with_component(faucet.with_info(extension)) .build()?; - // MASM script to read name and description via the metadata procedures and verify + let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); + let expected_desc_commitment = Hasher::hash_elements(&desc_felts); + + // MASM script to read name and description commitment via the metadata procedures and verify let tx_script = format!( r#" begin @@ -1174,16 +1149,16 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { push.{expected_name_1} assert_eqw.err="faucet name chunk 1 does not match" - # Get description and verify first chunk - call.::miden::standards::metadata::fungible::get_description - # => [DESCRIPTION_0, pad(12)] - push.{expected_desc_0} - assert_eqw.err="faucet description_0 does not match" + # Get description commitment and verify + call.::miden::standards::metadata::fungible::get_description_commitment + # => [COMMITMENT, pad(12)] + push.{expected_desc_commitment} + assert_eqw.err="faucet description commitment does not match" end "#, expected_name_0 = name[0], expected_name_1 = name[1], - expected_desc_0 = description[0], + expected_desc_commitment = expected_desc_commitment, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -1275,7 +1250,7 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When description flag is 2 and note sender is the owner, optional_set_description succeeds. +/// When description mutable flag is 1 and note sender is the owner, optional_set_description succeeds. #[tokio::test] async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -1357,7 +1332,7 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> Ok(()) } -/// When description flag is 2 but note sender is not the owner, optional_set_description panics. +/// When description mutable flag is 1 but note sender is not the owner, optional_set_description panics. #[tokio::test] async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -1635,22 +1610,29 @@ async fn optional_set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> /// Also tests is_max_supply_mutable with true (expects 1). #[tokio::test] async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { - let data = field_from_bytes(b"test").unwrap(); + let desc = Description::new("test").unwrap(); + let logo = LogoURI::new("https://example.com/logo").unwrap(); + let link = ExternalLink::new("https://example.com").unwrap(); let cases: Vec<(Info, &str, u8)> = vec![ (Info::new().with_max_supply_mutable(true), "is_max_supply_mutable", 1), - (Info::new().with_description(data, true), "is_description_mutable", 1), - (Info::new().with_description(data, false), "is_description_mutable", 0), - (Info::new().with_logo_uri(data, true), "is_logo_uri_mutable", 1), - (Info::new().with_logo_uri(data, false), "is_logo_uri_mutable", 0), - (Info::new().with_external_link(data, true), "is_external_link_mutable", 1), - (Info::new().with_external_link(data, false), "is_external_link_mutable", 0), + (Info::new().with_description(desc.clone(), true), "is_description_mutable", 1), + (Info::new().with_description(desc, false), "is_description_mutable", 0), + (Info::new().with_logo_uri(logo.clone(), true), "is_logo_uri_mutable", 1), + (Info::new().with_logo_uri(logo, false), "is_logo_uri_mutable", 0), + ( + Info::new().with_external_link(link.clone(), true), + "is_external_link_mutable", + 1, + ), + (Info::new().with_external_link(link, false), "is_external_link_mutable", 0), ]; for (info, proc_name, expected) in cases { let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(info) + .with_component(build_faucet_with_info(info)) .build()?; let tx_script = format!( @@ -1675,3 +1657,585 @@ async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { Ok(()) } + +// ================================================================================================= +// get_logo_uri_commitment: commitment test +// ================================================================================================= + +/// Tests that get_logo_uri_commitment returns the RPO256 hash of the 6 logo URI words. +#[tokio::test] +async fn metadata_get_logo_uri_commitment() -> anyhow::Result<()> { + let logo_text = "https://example.com/logo.png"; + let logo_typed = LogoURI::new(logo_text).unwrap(); + let logo_words = logo_typed.to_words(); + + let extension = Info::new().with_logo_uri(logo_typed, false); + + let account = AccountBuilder::new([10u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(build_faucet_with_info(extension)) + .build()?; + + let logo_felts: Vec = logo_words.iter().flat_map(|w| w.iter().copied()).collect(); + let expected_commitment = Hasher::hash_elements(&logo_felts); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_logo_uri_commitment + # => [COMMITMENT, pad(12)] + push.{expected} + assert_eqw.err="logo URI commitment does not match" + end + "#, + expected = expected_commitment, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +// ================================================================================================= +// get_external_link_commitment: commitment test +// ================================================================================================= + +/// Tests that get_external_link_commitment returns the RPO256 hash of the 6 external link words. +#[tokio::test] +async fn metadata_get_external_link_commitment() -> anyhow::Result<()> { + let link_text = "https://example.com"; + let link_typed = ExternalLink::new(link_text).unwrap(); + let link_words = link_typed.to_words(); + + let extension = Info::new().with_external_link(link_typed, false); + + let account = AccountBuilder::new([11u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(build_faucet_with_info(extension)) + .build()?; + + let link_felts: Vec = link_words.iter().flat_map(|w| w.iter().copied()).collect(); + let expected_commitment = Hasher::hash_elements(&link_felts); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_external_link_commitment + # => [COMMITMENT, pad(12)] + push.{expected} + assert_eqw.err="external link commitment does not match" + end + "#, + expected = expected_commitment, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +/// Tests that commitment of an empty (all-zero) description is deterministic. +#[tokio::test] +async fn metadata_get_description_commitment_zero_field() -> anyhow::Result<()> { + // No description set — all storage slots will be zero words + let extension = Info::new(); + + let account = AccountBuilder::new([12u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(build_faucet_with_info(extension)) + .build()?; + + // Expected: RPO256 hash of 24 zero felts + let zero_felts = vec![Felt::new(0); 24]; + let expected_commitment = Hasher::hash_elements(&zero_felts); + + let tx_script = format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_description_commitment + # => [COMMITMENT, pad(12)] + push.{expected} + assert_eqw.err="zero description commitment does not match" + end + "#, + expected = expected_commitment, + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + tx_context.execute().await?; + + Ok(()) +} + +// ================================================================================================= +// optional_set_logo_uri: mutable flag and verify_owner +// ================================================================================================= + +/// When logo URI flag is 0 (immutable), optional_set_logo_uri panics. +#[tokio::test] +async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let logo = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "LGO", + 1000, + owner_account_id, + Some(0), + false, + None, + Some((logo, false)), // immutable + None, + )?; + let mock_chain = builder.build()?; + + let new_logo = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let tx_script = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_logo_uri + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .extend_advice_map([(LOGO_URI_DATA_KEY, field_advice_map_value(&new_logo))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_LOGO_URI_NOT_MUTABLE); + + Ok(()) +} + +/// When logo URI mutable flag is 1 and note sender is the owner, optional_set_logo_uri succeeds. +#[tokio::test] +async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_logo = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let new_logo = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "LGO", + 1000, + owner_account_id, + Some(0), + false, + None, + Some((initial_logo, true)), // mutable + None, + )?; + let mock_chain = builder.build()?; + + let set_logo_note_script_code = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_logo_uri + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_logo_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(set_logo_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(50u32); 4].into()); + let set_logo_note = NoteBuilder::new(owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([40, 41, 42, 43u32])) + .code(set_logo_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_logo_note])? + .add_note_script(set_logo_note_script) + .extend_advice_map([(LOGO_URI_DATA_KEY, field_advice_map_value(&new_logo))]) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + for (i, expected) in new_logo.iter().enumerate() { + let chunk = updated_faucet.storage().get_item(Info::logo_uri_slot(i))?; + assert_eq!(chunk, *expected, "logo_uri_{i} should be updated"); + } + + Ok(()) +} + +/// When logo URI mutable flag is 1 but note sender is not the owner, optional_set_logo_uri panics. +#[tokio::test] +async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let non_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_logo = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let new_logo = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "LGO", + 1000, + owner_account_id, + Some(0), + false, + None, + Some((initial_logo, true)), + None, + )?; + let mock_chain = builder.build()?; + + let set_logo_note_script_code = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_logo_uri + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_logo_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(set_logo_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let set_logo_note = NoteBuilder::new(non_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([50, 51, 52, 53u32])) + .code(set_logo_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_logo_note])? + .add_note_script(set_logo_note_script) + .extend_advice_map([(LOGO_URI_DATA_KEY, field_advice_map_value(&new_logo))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} + +// ================================================================================================= +// optional_set_external_link: mutable flag and verify_owner +// ================================================================================================= + +/// When external link flag is 0 (immutable), optional_set_external_link panics. +#[tokio::test] +async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let link = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "EXL", + 1000, + owner_account_id, + Some(0), + false, + None, + None, + Some((link, false)), // immutable + )?; + let mock_chain = builder.build()?; + + let new_link = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let tx_script = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_external_link + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = + CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .extend_advice_map([(EXTERNAL_LINK_DATA_KEY, field_advice_map_value(&new_link))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_EXTERNAL_LINK_NOT_MUTABLE); + + Ok(()) +} + +/// When external link mutable flag is 1 and note sender is the owner, optional_set_external_link succeeds. +#[tokio::test] +async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_link = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let new_link = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "EXL", + 1000, + owner_account_id, + Some(0), + false, + None, + None, + Some((initial_link, true)), // mutable + )?; + let mock_chain = builder.build()?; + + let set_link_note_script_code = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_external_link + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_link_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(set_link_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(60u32); 4].into()); + let set_link_note = NoteBuilder::new(owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([60, 61, 62, 63u32])) + .code(set_link_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_link_note])? + .add_note_script(set_link_note_script) + .extend_advice_map([(EXTERNAL_LINK_DATA_KEY, field_advice_map_value(&new_link))]) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + for (i, expected) in new_link.iter().enumerate() { + let chunk = updated_faucet.storage().get_item(Info::external_link_slot(i))?; + assert_eq!(chunk, *expected, "external_link_{i} should be updated"); + } + + Ok(()) +} + +/// When external link mutable flag is 1 but note sender is not the owner, optional_set_external_link panics. +#[tokio::test] +async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + let owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let non_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let initial_link = [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + ]; + let new_link = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + ]; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "EXL", + 1000, + owner_account_id, + Some(0), + false, + None, + None, + Some((initial_link, true)), + )?; + let mock_chain = builder.build()?; + + let set_link_note_script_code = r#" + begin + call.::miden::standards::metadata::fungible::optional_set_external_link + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let set_link_note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(set_link_note_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let set_link_note = NoteBuilder::new(non_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([70, 71, 72, 73u32])) + .code(set_link_note_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[set_link_note])? + .add_note_script(set_link_note_script) + .extend_advice_map([(EXTERNAL_LINK_DATA_KEY, field_advice_map_value(&new_link))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 99a061474d..fb6bddf6d8 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -30,8 +30,8 @@ use miden_protocol::transaction::{ExecutedTransaction, OutputNote}; use miden_protocol::{Felt, Word}; use miden_standards::account::faucets::{ BasicFungibleFaucet, + FungibleTokenMetadata, NetworkFungibleFaucet, - TokenMetadata, }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ @@ -284,7 +284,7 @@ async fn prove_burning_fungible_asset_on_existing_faucet_succeeds() -> anyhow::R builder.add_output_note(OutputNote::Full(note.clone())); let mock_chain = builder.build()?; - let token_metadata = TokenMetadata::try_from(faucet.storage())?; + let token_metadata = FungibleTokenMetadata::try_from(faucet.storage())?; // Check that max_supply at the word's index 0 is 200. The remainder of the word is initialized // with the metadata of the faucet which we don't need to check. @@ -565,7 +565,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { let mut target_account = builder.add_existing_wallet(Auth::IncrNonce)?; // Check the Network Fungible Faucet's max supply. - let actual_max_supply = TokenMetadata::try_from(faucet.storage())?.max_supply(); + let actual_max_supply = FungibleTokenMetadata::try_from(faucet.storage())?.max_supply(); assert_eq!(actual_max_supply.as_canonical_u64(), max_supply); // Check that the creator account ID is stored in slot 2 (second storage slot of the component) @@ -580,7 +580,7 @@ async fn network_faucet_mint() -> anyhow::Result<()> { // Check that the faucet's token supply has been correctly initialized. // The already issued amount should be 50. - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let initial_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!(initial_token_supply.as_canonical_u64(), token_supply); // CREATE MINT NOTE USING STANDARD NOTE @@ -1188,7 +1188,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { mock_chain.prove_next_block()?; // Check the initial token issuance before burning - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let initial_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!(initial_token_supply, Felt::new(100)); // EXECUTE BURN NOTE AGAINST NETWORK FAUCET @@ -1205,7 +1205,7 @@ async fn network_faucet_burn() -> anyhow::Result<()> { // Apply the delta to the faucet account and verify the token issuance decreased faucet.apply_delta(executed_transaction.account_delta())?; - let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + let final_token_supply = FungibleTokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( final_token_supply, Felt::new(initial_token_supply.as_canonical_u64() - burn_amount) From 0f7766c6d8112f957e639b812ef573c87d048787 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Sat, 28 Feb 2026 19:16:56 -0300 Subject: [PATCH 06/56] chore: lint --- crates/miden-testing/tests/metadata.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 04d9908a99..bf6c9ff888 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -1250,7 +1250,8 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When description mutable flag is 1 and note sender is the owner, optional_set_description succeeds. +/// When description mutable flag is 1 and note sender is the owner, optional_set_description +/// succeeds. #[tokio::test] async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -1332,7 +1333,8 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> Ok(()) } -/// When description mutable flag is 1 but note sender is not the owner, optional_set_description panics. +/// When description mutable flag is 1 but note sender is not the owner, optional_set_description +/// panics. #[tokio::test] async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -2084,7 +2086,8 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When external link mutable flag is 1 and note sender is the owner, optional_set_external_link succeeds. +/// When external link mutable flag is 1 and note sender is the owner, optional_set_external_link +/// succeeds. #[tokio::test] async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -2162,7 +2165,8 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( Ok(()) } -/// When external link mutable flag is 1 but note sender is not the owner, optional_set_external_link panics. +/// When external link mutable flag is 1 but note sender is not the owner, +/// optional_set_external_link panics. #[tokio::test] async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); From 6fd309ed4459fe04a8fa1e2974c3960e17a548f6 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 4 Mar 2026 12:21:42 -0300 Subject: [PATCH 07/56] Refactor token metadata encoding to use 7-bytes-per-felt, length-prefixed format - Updated `TokenName`, `Description`, `LogoURI`, and `ExternalLink` structs to support new encoding scheme. - Adjusted maximum byte lengths and encoding/decoding methods to accommodate the new format. - Modified metadata module to reflect changes in field sizes and encoding conventions. - Updated tests to validate new field sizes and encoding behavior. - Changed mock chain builder and associated tests to handle updated metadata structure. --- .../asm/standards/metadata/fungible.masm | 128 +++++++---- .../src/account/faucets/token_metadata.rs | 200 +++++++++++------- .../src/account/metadata/mod.rs | 93 ++++---- .../src/mock_chain/chain_builder.rs | 6 +- crates/miden-testing/tests/metadata.rs | 40 +++- 5 files changed, 290 insertions(+), 177 deletions(-) diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index 653ac9fc4d..d03c135532 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -25,6 +25,7 @@ const DESCRIPTION_2_SLOT = word("miden::standards::metadata::description_2") const DESCRIPTION_3_SLOT = word("miden::standards::metadata::description_3") const DESCRIPTION_4_SLOT = word("miden::standards::metadata::description_4") const DESCRIPTION_5_SLOT = word("miden::standards::metadata::description_5") +const DESCRIPTION_6_SLOT = word("miden::standards::metadata::description_6") const LOGO_URI_0_SLOT = word("miden::standards::metadata::logo_uri_0") const LOGO_URI_1_SLOT = word("miden::standards::metadata::logo_uri_1") @@ -32,6 +33,7 @@ const LOGO_URI_2_SLOT = word("miden::standards::metadata::logo_uri_2") const LOGO_URI_3_SLOT = word("miden::standards::metadata::logo_uri_3") const LOGO_URI_4_SLOT = word("miden::standards::metadata::logo_uri_4") const LOGO_URI_5_SLOT = word("miden::standards::metadata::logo_uri_5") +const LOGO_URI_6_SLOT = word("miden::standards::metadata::logo_uri_6") const EXTERNAL_LINK_0_SLOT = word("miden::standards::metadata::external_link_0") const EXTERNAL_LINK_1_SLOT = word("miden::standards::metadata::external_link_1") @@ -39,6 +41,7 @@ const EXTERNAL_LINK_2_SLOT = word("miden::standards::metadata::external_link_2") const EXTERNAL_LINK_3_SLOT = word("miden::standards::metadata::external_link_3") const EXTERNAL_LINK_4_SLOT = word("miden::standards::metadata::external_link_4") const EXTERNAL_LINK_5_SLOT = word("miden::standards::metadata::external_link_5") +const EXTERNAL_LINK_6_SLOT = word("miden::standards::metadata::external_link_6") const CHUNK_1_LOC = 0 const CHUNK_0_LOC = 4 @@ -48,6 +51,7 @@ const FIELD_2_LOC = 8 const FIELD_3_LOC = 12 const FIELD_4_LOC = 16 const FIELD_5_LOC = 20 +const FIELD_6_LOC = 24 const ERR_DESCRIPTION_NOT_MUTABLE = "description is not mutable" const ERR_LOGO_URI_NOT_MUTABLE = "logo URI is not mutable" @@ -55,7 +59,7 @@ const ERR_EXTERNAL_LINK_NOT_MUTABLE = "external link is not mutable" const ERR_MAX_SUPPLY_IMMUTABLE = "max supply is immutable" const ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY = "new max supply is less than current token supply" -# Advice map keys for the 6-word field data. +# Advice map keys for the 7-word field data. const DESCRIPTION_DATA_KEY = [0, 0, 0, 1] const LOGO_URI_DATA_KEY = [0, 0, 0, 2] const EXTERNAL_LINK_DATA_KEY = [0, 0, 0, 3] @@ -115,6 +119,10 @@ proc get_description_chunk_5 push.DESCRIPTION_5_SLOT[0..2] exec.active_account::get_item end +proc get_description_chunk_6 + push.DESCRIPTION_6_SLOT[0..2] + exec.active_account::get_item +end # --- Logo URI chunk loaders --- proc get_logo_uri_chunk_0 @@ -141,6 +149,10 @@ proc get_logo_uri_chunk_5 push.LOGO_URI_5_SLOT[0..2] exec.active_account::get_item end +proc get_logo_uri_chunk_6 + push.LOGO_URI_6_SLOT[0..2] + exec.active_account::get_item +end # --- External link chunk loaders --- proc get_external_link_chunk_0 @@ -167,6 +179,10 @@ proc get_external_link_chunk_5 push.EXTERNAL_LINK_5_SLOT[0..2] exec.active_account::get_item end +proc get_external_link_chunk_6 + push.EXTERNAL_LINK_6_SLOT[0..2] + exec.active_account::get_item +end # ================================================================================================= # TOKEN METADATA (token_supply, max_supply, decimals, token_symbol) @@ -345,12 +361,12 @@ end pub use ownable::get_owner # ================================================================================================= -# DESCRIPTION (6 words) — commitment/unhashing pattern +# DESCRIPTION (7 words) — commitment/unhashing pattern # ================================================================================================= -#! Returns an RPO256 commitment to the description and inserts the 6 words into the advice map. +#! Returns an RPO256 commitment to the description and inserts the 7 words into the advice map. #! -#! The commitment is computed over the 24 felts of the description. The preimage (6 words) +#! The commitment is computed over the 28 felts of the description. The preimage (7 words) #! is placed in the advice map keyed by the commitment so that a subsequent #! `get_description` call can retrieve and verify it. #! @@ -358,7 +374,7 @@ pub use ownable::get_owner #! Outputs: [COMMITMENT, pad(12)] #! #! Invocation: call -@locals(24) +@locals(28) pub proc get_description_commitment exec.get_description_chunk_0 loc_storew_be.FIELD_0_LOC dropw @@ -372,17 +388,19 @@ pub proc get_description_commitment loc_storew_be.FIELD_4_LOC dropw exec.get_description_chunk_5 loc_storew_be.FIELD_5_LOC dropw + exec.get_description_chunk_6 + loc_storew_be.FIELD_6_LOC dropw # => [pad(16)] - # Hash the 6 words (24 felts) in local memory via RPO256. + # Hash the 7 words (28 felts) in local memory via RPO256. # hash_elements expects [ptr, num_felts]. - push.24 locaddr.FIELD_0_LOC + push.28 locaddr.FIELD_0_LOC exec.rpo256::hash_elements # => [COMMITMENT, pad(16)] depth=20 - # Insert the 6-word preimage into the advice map keyed by COMMITMENT. + # Insert the 7-word preimage into the advice map keyed by COMMITMENT. # adv.insert_mem expects [COMMITMENT, start_addr, end_addr] on stack. - locaddr.FIELD_0_LOC dup add.24 + locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 adv.insert_mem movup.4 drop movup.4 drop @@ -392,7 +410,7 @@ pub proc get_description_commitment # => [COMMITMENT, pad(12)] depth=16 end -#! Writes the description (6 Words) to memory at the given pointer. +#! Writes the description (7 Words) to memory at the given pointer. #! #! Loads the description from storage, computes its RPO256 commitment, inserts the preimage #! into the advice map, then pipes it into destination memory and verifies the commitment. @@ -401,7 +419,7 @@ end #! Outputs: [dest_ptr, pad(15)] #! #! Invocation: exec -@locals(24) +@locals(28) pub proc get_description exec.get_description_chunk_0 loc_storew_be.FIELD_0_LOC dropw @@ -415,16 +433,18 @@ pub proc get_description loc_storew_be.FIELD_4_LOC dropw exec.get_description_chunk_5 loc_storew_be.FIELD_5_LOC dropw + exec.get_description_chunk_6 + loc_storew_be.FIELD_6_LOC dropw # => [dest_ptr, pad(15)] depth=16 - # Hash the 6 words (24 felts) in local memory. - push.24 locaddr.FIELD_0_LOC - # => [start_ptr, 24, dest_ptr, pad(15)] depth=18 + # Hash the 7 words (28 felts) in local memory. + push.28 locaddr.FIELD_0_LOC + # => [start_ptr, 28, dest_ptr, pad(15)] depth=18 exec.rpo256::hash_elements # => [COMMITMENT, dest_ptr, pad(15)] depth=20 # Insert preimage into advice map keyed by COMMITMENT. - locaddr.FIELD_0_LOC dup add.24 + locaddr.FIELD_0_LOC dup add.28 # => [end_ptr, start_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 movdn.5 movdn.4 # => [COMMITMENT, start_ptr, end_ptr, dest_ptr, pad(15)] @@ -438,24 +458,24 @@ pub proc get_description # Set up pipe_preimage_to_memory: [num_words, dest_ptr, COMMITMENT, ...] # dest_ptr is at position 4 (right after COMMITMENT) - dup.4 push.6 - # => [6, dest_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 + dup.4 push.7 + # => [7, dest_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 exec.mem::pipe_preimage_to_memory drop # => [dest_ptr, pad(15)] depth=16 (consumed 6 [6,dest_ptr,COMMITMENT], pushed 1 [updated_ptr], dropped it) end # ================================================================================================= -# LOGO URI (6 words) — commitment/unhashing pattern +# LOGO URI (7 words) — commitment/unhashing pattern # ================================================================================================= -#! Returns an RPO256 commitment to the logo URI and inserts the 6 words into the advice map. +#! Returns an RPO256 commitment to the logo URI and inserts the 7 words into the advice map. #! #! Inputs: [pad(16)] #! Outputs: [COMMITMENT, pad(12)] #! #! Invocation: call -@locals(24) +@locals(28) pub proc get_logo_uri_commitment exec.get_logo_uri_chunk_0 loc_storew_be.FIELD_0_LOC dropw @@ -469,11 +489,13 @@ pub proc get_logo_uri_commitment loc_storew_be.FIELD_4_LOC dropw exec.get_logo_uri_chunk_5 loc_storew_be.FIELD_5_LOC dropw + exec.get_logo_uri_chunk_6 + loc_storew_be.FIELD_6_LOC dropw - push.24 locaddr.FIELD_0_LOC + push.28 locaddr.FIELD_0_LOC exec.rpo256::hash_elements - locaddr.FIELD_0_LOC dup add.24 + locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 adv.insert_mem movup.4 drop movup.4 drop @@ -481,13 +503,13 @@ pub proc get_logo_uri_commitment swapw dropw end -#! Writes the logo URI (6 Words) to memory at the given pointer. +#! Writes the logo URI (7 Words) to memory at the given pointer. #! #! Inputs: [dest_ptr, pad(15)] #! Outputs: [dest_ptr, pad(15)] #! #! Invocation: exec -@locals(24) +@locals(28) pub proc get_logo_uri exec.get_logo_uri_chunk_0 loc_storew_be.FIELD_0_LOC dropw @@ -501,31 +523,33 @@ pub proc get_logo_uri loc_storew_be.FIELD_4_LOC dropw exec.get_logo_uri_chunk_5 loc_storew_be.FIELD_5_LOC dropw + exec.get_logo_uri_chunk_6 + loc_storew_be.FIELD_6_LOC dropw - push.24 locaddr.FIELD_0_LOC + push.28 locaddr.FIELD_0_LOC exec.rpo256::hash_elements - locaddr.FIELD_0_LOC dup add.24 + locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 adv.insert_mem movup.4 drop movup.4 drop adv.push_mapval - dup.4 push.6 + dup.4 push.7 exec.mem::pipe_preimage_to_memory drop end # ================================================================================================= -# EXTERNAL LINK (6 words) — commitment/unhashing pattern +# EXTERNAL LINK (7 words) — commitment/unhashing pattern # ================================================================================================= -#! Returns an RPO256 commitment to the external link and inserts the 6 words into the advice map. +#! Returns an RPO256 commitment to the external link and inserts the 7 words into the advice map. #! #! Inputs: [pad(16)] #! Outputs: [COMMITMENT, pad(12)] #! #! Invocation: call -@locals(24) +@locals(28) pub proc get_external_link_commitment exec.get_external_link_chunk_0 loc_storew_be.FIELD_0_LOC dropw @@ -539,11 +563,13 @@ pub proc get_external_link_commitment loc_storew_be.FIELD_4_LOC dropw exec.get_external_link_chunk_5 loc_storew_be.FIELD_5_LOC dropw + exec.get_external_link_chunk_6 + loc_storew_be.FIELD_6_LOC dropw - push.24 locaddr.FIELD_0_LOC + push.28 locaddr.FIELD_0_LOC exec.rpo256::hash_elements - locaddr.FIELD_0_LOC dup add.24 + locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 adv.insert_mem movup.4 drop movup.4 drop @@ -551,13 +577,13 @@ pub proc get_external_link_commitment swapw dropw end -#! Writes the external link (6 Words) to memory at the given pointer. +#! Writes the external link (7 Words) to memory at the given pointer. #! #! Inputs: [dest_ptr, pad(15)] #! Outputs: [dest_ptr, pad(15)] #! #! Invocation: exec -@locals(24) +@locals(28) pub proc get_external_link exec.get_external_link_chunk_0 loc_storew_be.FIELD_0_LOC dropw @@ -571,17 +597,19 @@ pub proc get_external_link loc_storew_be.FIELD_4_LOC dropw exec.get_external_link_chunk_5 loc_storew_be.FIELD_5_LOC dropw + exec.get_external_link_chunk_6 + loc_storew_be.FIELD_6_LOC dropw - push.24 locaddr.FIELD_0_LOC + push.28 locaddr.FIELD_0_LOC exec.rpo256::hash_elements - locaddr.FIELD_0_LOC dup add.24 + locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 adv.insert_mem movup.4 drop movup.4 drop adv.push_mapval - dup.4 push.6 + dup.4 push.7 exec.mem::pipe_preimage_to_memory drop end @@ -589,12 +617,12 @@ end # OPTIONAL SET DESCRIPTION (owner-only when desc_mutable == 1) # ================================================================================================= -#! Updates the description (6 Words) if the description mutability flag is 1 +#! Updates the description (7 Words) if the description mutability flag is 1 #! and the note sender is the owner. #! #! Before executing the transaction, populate the advice map: #! key: DESCRIPTION_DATA_KEY ([0, 0, 0, 1]) -#! value: [D0, D1, D2, D3, D4, D5] (24 felts in natural order) +#! value: [D0, D1, D2, D3, D4, D5, D6] (28 felts in natural order) #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] @@ -620,7 +648,7 @@ pub proc optional_set_description exec.ownable::verify_owner # => [pad(16)] - # Load the 6 description words from the advice map + # Load the 7 description words from the advice map push.DESCRIPTION_DATA_KEY adv.push_mapval dropw @@ -649,18 +677,22 @@ pub proc optional_set_description padw adv_loadw push.DESCRIPTION_5_SLOT[0..2] exec.native_account::set_item dropw + + padw adv_loadw + push.DESCRIPTION_6_SLOT[0..2] + exec.native_account::set_item dropw end # ================================================================================================= # OPTIONAL SET LOGO URI (owner-only when logo_mutable == 1) # ================================================================================================= -#! Updates the logo URI (6 Words) if the logo URI mutability flag is 1 +#! Updates the logo URI (7 Words) if the logo URI mutability flag is 1 #! and the note sender is the owner. #! #! Before executing the transaction, populate the advice map: #! key: LOGO_URI_DATA_KEY ([0, 0, 0, 2]) -#! value: [L0, L1, L2, L3, L4, L5] (24 felts in natural order) +#! value: [L0, L1, L2, L3, L4, L5, L6] (28 felts in natural order) #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] @@ -713,18 +745,22 @@ pub proc optional_set_logo_uri padw adv_loadw push.LOGO_URI_5_SLOT[0..2] exec.native_account::set_item dropw + + padw adv_loadw + push.LOGO_URI_6_SLOT[0..2] + exec.native_account::set_item dropw end # ================================================================================================= # OPTIONAL SET EXTERNAL LINK (owner-only when extlink_mutable == 1) # ================================================================================================= -#! Updates the external link (6 Words) if the external link mutability flag is 1 +#! Updates the external link (7 Words) if the external link mutability flag is 1 #! and the note sender is the owner. #! #! Before executing the transaction, populate the advice map: #! key: EXTERNAL_LINK_DATA_KEY ([0, 0, 0, 3]) -#! value: [E0, E1, E2, E3, E4, E5] (24 felts in natural order) +#! value: [E0, E1, E2, E3, E4, E5, E6] (28 felts in natural order) #! #! Inputs: [pad(16)] #! Outputs: [pad(16)] @@ -777,6 +813,10 @@ pub proc optional_set_external_link padw adv_loadw push.EXTERNAL_LINK_5_SLOT[0..2] exec.native_account::set_item dropw + + padw adv_loadw + push.EXTERNAL_LINK_6_SLOT[0..2] + exec.native_account::set_item dropw end # ================================================================================================= diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index aca959ad3c..bacd676068 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -1,6 +1,5 @@ use alloc::boxed::Box; use alloc::string::String; -use alloc::vec::Vec; use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; use miden_protocol::asset::{FungibleAsset, TokenSymbol}; @@ -9,19 +8,26 @@ use miden_protocol::{Felt, Word}; use super::FungibleFaucetError; use crate::account::metadata::{self, FieldBytesError, NameUtf8Error}; +// ENCODING CONSTANTS +// ================================================================================================ + +/// Number of data bytes packed into each felt (7 bytes = 56 bits, always < Goldilocks prime). +const BYTES_PER_FELT: usize = 7; + // TOKEN NAME // ================================================================================================ /// Token display name (max 32 bytes UTF-8). /// /// Internally stores the un-encoded string for cheap access via [`as_str`](Self::as_str). -/// The invariant that the string can be encoded into 2 Words (8 felts, 4 bytes/felt) is +/// The invariant that the string can be encoded into 2 Words (8 felts × 7 bytes/felt) is /// enforced at construction time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TokenName(Box); impl TokenName { - /// Maximum byte length for a token name (2 Words = 8 felts x 4 bytes). + /// Maximum byte length for a token name (2 Words = 8 felts × 7 bytes, capacity 55, + /// capped at 32). pub const MAX_BYTES: usize = metadata::NAME_UTF8_MAX_BYTES; /// Creates a token name from a UTF-8 string (at most 32 bytes). @@ -37,37 +43,32 @@ impl TokenName { &self.0 } - /// Encodes the name into 2 Words for storage (4 bytes/felt, little-endian, zero-padded). + /// Encodes the name into 2 Words for storage (7 bytes/felt, length-prefixed, + /// zero-padded). pub fn to_words(&self) -> [Word; 2] { - let bytes = self.0.as_bytes(); - let mut padded = [0u8; Self::MAX_BYTES]; - padded[..bytes.len()].copy_from_slice(bytes); - let felts: [Felt; 8] = padded - .chunks_exact(4) - .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap()))) - .collect::>() - .try_into() - .unwrap(); + let felts = encode_utf8_to_felts::<8>(self.0.as_bytes()); [ Word::from([felts[0], felts[1], felts[2], felts[3]]), Word::from([felts[4], felts[5], felts[6], felts[7]]), ] } - /// Decodes a token name from 2 Words (4 bytes/felt, little-endian). + /// Decodes a token name from 2 Words (7 bytes/felt, length-prefixed). pub fn try_from_words(words: &[Word; 2]) -> Result { - let mut bytes = [0u8; Self::MAX_BYTES]; - for (i, word) in words.iter().enumerate() { - for (j, f) in word.iter().enumerate() { - let v = f.as_int(); - if v > u32::MAX as u64 { - return Err(NameUtf8Error::InvalidUtf8); - } - bytes[i * 16 + j * 4..][..4].copy_from_slice(&(v as u32).to_le_bytes()); - } + let felts: [Felt; 8] = [ + words[0][0], + words[0][1], + words[0][2], + words[0][3], + words[1][0], + words[1][1], + words[1][2], + words[1][3], + ]; + let s = decode_felts_to_utf8::<8>(&felts).map_err(|_| NameUtf8Error::InvalidUtf8)?; + if s.len() > Self::MAX_BYTES { + return Err(NameUtf8Error::TooLong(s.len())); } - let len = bytes.iter().position(|&b| b == 0).unwrap_or(Self::MAX_BYTES); - let s = String::from_utf8(bytes[..len].to_vec()).map_err(|_| NameUtf8Error::InvalidUtf8)?; Ok(Self(s.into())) } } @@ -75,18 +76,18 @@ impl TokenName { // DESCRIPTION // ================================================================================================ -/// Token description (max 192 bytes UTF-8). +/// Token description (max [`FIELD_MAX_BYTES`](metadata::FIELD_MAX_BYTES) bytes UTF-8). /// -/// Internally stores the un-encoded string. The invariant that it can be encoded into 6 Words -/// (24 felts, 8 bytes/felt) is enforced at construction time. +/// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words +/// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Description(Box); impl Description { - /// Maximum byte length for a description (6 Words = 24 felts x 8 bytes). + /// Maximum byte length for a description (7 Words = 28 felts × 7 bytes − 1 length byte). pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; - /// Creates a description from a UTF-8 string (at most 192 bytes). + /// Creates a description from a UTF-8 string. pub fn new(s: &str) -> Result { if s.len() > Self::MAX_BYTES { return Err(FieldBytesError::TooLong(s.len())); @@ -99,13 +100,13 @@ impl Description { &self.0 } - /// Encodes the description into 6 Words for storage (8 bytes/felt, little-endian). - pub fn to_words(&self) -> [Word; 6] { + /// Encodes the description into 7 Words for storage (7 bytes/felt, length-prefixed). + pub fn to_words(&self) -> [Word; 7] { encode_field_to_words(self.0.as_bytes()) } - /// Decodes a description from 6 Words (8 bytes/felt, little-endian). - pub fn try_from_words(words: &[Word; 6]) -> Result { + /// Decodes a description from 7 Words (7 bytes/felt, length-prefixed). + pub fn try_from_words(words: &[Word; 7]) -> Result { let s = decode_field_from_words(words)?; Ok(Self(s.into())) } @@ -114,18 +115,18 @@ impl Description { // LOGO URI // ================================================================================================ -/// Token logo URI (max 192 bytes UTF-8). +/// Token logo URI (max [`FIELD_MAX_BYTES`](metadata::FIELD_MAX_BYTES) bytes UTF-8). /// -/// Internally stores the un-encoded string. The invariant that it can be encoded into 6 Words -/// (24 felts, 8 bytes/felt) is enforced at construction time. +/// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words +/// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LogoURI(Box); impl LogoURI { - /// Maximum byte length for a logo URI (6 Words = 24 felts x 8 bytes). + /// Maximum byte length for a logo URI (7 Words = 28 felts × 7 bytes − 1 length byte). pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; - /// Creates a logo URI from a UTF-8 string (at most 192 bytes). + /// Creates a logo URI from a UTF-8 string. pub fn new(s: &str) -> Result { if s.len() > Self::MAX_BYTES { return Err(FieldBytesError::TooLong(s.len())); @@ -138,13 +139,13 @@ impl LogoURI { &self.0 } - /// Encodes the logo URI into 6 Words for storage (8 bytes/felt, little-endian). - pub fn to_words(&self) -> [Word; 6] { + /// Encodes the logo URI into 7 Words for storage (7 bytes/felt, length-prefixed). + pub fn to_words(&self) -> [Word; 7] { encode_field_to_words(self.0.as_bytes()) } - /// Decodes a logo URI from 6 Words (8 bytes/felt, little-endian). - pub fn try_from_words(words: &[Word; 6]) -> Result { + /// Decodes a logo URI from 7 Words (7 bytes/felt, length-prefixed). + pub fn try_from_words(words: &[Word; 7]) -> Result { let s = decode_field_from_words(words)?; Ok(Self(s.into())) } @@ -153,18 +154,18 @@ impl LogoURI { // EXTERNAL LINK // ================================================================================================ -/// Token external link (max 192 bytes UTF-8). +/// Token external link (max [`FIELD_MAX_BYTES`](metadata::FIELD_MAX_BYTES) bytes UTF-8). /// -/// Internally stores the un-encoded string. The invariant that it can be encoded into 6 Words -/// (24 felts, 8 bytes/felt) is enforced at construction time. +/// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words +/// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExternalLink(Box); impl ExternalLink { - /// Maximum byte length for an external link (6 Words = 24 felts x 8 bytes). + /// Maximum byte length for an external link (7 Words = 28 felts × 7 bytes − 1 length byte). pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; - /// Creates an external link from a UTF-8 string (at most 192 bytes). + /// Creates an external link from a UTF-8 string. pub fn new(s: &str) -> Result { if s.len() > Self::MAX_BYTES { return Err(FieldBytesError::TooLong(s.len())); @@ -177,13 +178,13 @@ impl ExternalLink { &self.0 } - /// Encodes the external link into 6 Words for storage (8 bytes/felt, little-endian). - pub fn to_words(&self) -> [Word; 6] { + /// Encodes the external link into 7 Words for storage (7 bytes/felt, length-prefixed). + pub fn to_words(&self) -> [Word; 7] { encode_field_to_words(self.0.as_bytes()) } - /// Decodes an external link from 6 Words (8 bytes/felt, little-endian). - pub fn try_from_words(words: &[Word; 6]) -> Result { + /// Decodes an external link from 7 Words (7 bytes/felt, length-prefixed). + pub fn try_from_words(words: &[Word; 7]) -> Result { let s = decode_field_from_words(words)?; Ok(Self(s.into())) } @@ -192,26 +193,74 @@ impl ExternalLink { // ENCODING HELPERS // ================================================================================================ -/// Encodes a byte slice into 6 Words (24 felts, 8 bytes/felt, little-endian, zero-padded). +/// Encodes a UTF-8 byte slice into `N` felts using 7-bytes-per-felt, length-prefixed encoding. +/// +/// ## Buffer layout (`N × 7` bytes) +/// +/// ```text +/// Byte 0: string length (u8) +/// Bytes 1..1+len: UTF-8 content +/// Remaining: zero-padded +/// +/// Felt 0: buffer[0..7] (length byte + first 6 data bytes) +/// Felt 1: buffer[7..14] (next 7 data bytes) +/// ... +/// ``` +/// +/// Each 7-byte chunk is stored as a little-endian `u64` with the high byte always zero, +/// so the value is always < 2^56 and fits safely in a Goldilocks field element. +/// +/// # Panics +/// +/// Panics (debug-only) if `bytes.len() + 1 > N * BYTES_PER_FELT` (content + length byte +/// exceeds buffer capacity). +fn encode_utf8_to_felts(bytes: &[u8]) -> [Felt; N] { + let buf_len = N * BYTES_PER_FELT; + debug_assert!(bytes.len() < buf_len); + + let mut buf = [0u8; 256]; // large enough for any field (max 28 * 7 = 196) + buf[0] = bytes.len() as u8; + buf[1..1 + bytes.len()].copy_from_slice(bytes); + + let mut felts = [Felt::ZERO; N]; + for (i, felt) in felts.iter_mut().enumerate() { + let start = i * BYTES_PER_FELT; + let mut le_bytes = [0u8; 8]; + le_bytes[..BYTES_PER_FELT].copy_from_slice(&buf[start..start + BYTES_PER_FELT]); + // High byte is always 0 ⇒ value < 2^56, safe for Goldilocks. + *felt = Felt::try_from(u64::from_le_bytes(le_bytes)).expect("7 bytes always fit in a Felt"); + } + felts +} + +/// Decodes `N` felts (7-bytes-per-felt, length-prefixed) back to a UTF-8 string. +fn decode_felts_to_utf8(felts: &[Felt; N]) -> Result { + let buf_len = N * BYTES_PER_FELT; + let mut buf = [0u8; 256]; + for (i, felt) in felts.iter().enumerate() { + let v = felt.as_int(); + let le = v.to_le_bytes(); + // Reject values that use the high byte (> 7 bytes of data). + if le[BYTES_PER_FELT] != 0 { + return Err(FieldBytesError::InvalidUtf8); + } + buf[i * BYTES_PER_FELT..][..BYTES_PER_FELT].copy_from_slice(&le[..BYTES_PER_FELT]); + } + let len = buf[0] as usize; + if len + 1 > buf_len { + return Err(FieldBytesError::InvalidUtf8); + } + String::from_utf8(buf[1..1 + len].to_vec()).map_err(|_| FieldBytesError::InvalidUtf8) +} + +/// Encodes a byte slice into 7 Words (28 felts, 7 bytes/felt, length-prefixed, zero-padded). /// /// # Panics /// /// Panics (debug-only) if `bytes.len() > FIELD_MAX_BYTES`. Callers must validate length first. -fn encode_field_to_words(bytes: &[u8]) -> [Word; 6] { +fn encode_field_to_words(bytes: &[u8]) -> [Word; 7] { debug_assert!(bytes.len() <= metadata::FIELD_MAX_BYTES); - let mut padded = [0u8; metadata::FIELD_MAX_BYTES]; - padded[..bytes.len()].copy_from_slice(bytes); - let felts: Vec = padded - .chunks_exact(8) - .map(|chunk| { - // SAFETY: Valid UTF-8 bytes have values in 0x00..=0xF4. A u64 formed from 8 such - // bytes can never reach the Goldilocks prime (2^64 - 2^32 + 1) because that would - // require all 8 bytes to be >= 0xFF, which is impossible in valid UTF-8. - Felt::try_from(u64::from_le_bytes(chunk.try_into().unwrap())) - .expect("UTF-8 bytes cannot overflow Felt") - }) - .collect(); - let felts: [Felt; 24] = felts.try_into().unwrap(); + let felts = encode_utf8_to_felts::<28>(bytes); [ Word::from([felts[0], felts[1], felts[2], felts[3]]), Word::from([felts[4], felts[5], felts[6], felts[7]]), @@ -219,20 +268,17 @@ fn encode_field_to_words(bytes: &[u8]) -> [Word; 6] { Word::from([felts[12], felts[13], felts[14], felts[15]]), Word::from([felts[16], felts[17], felts[18], felts[19]]), Word::from([felts[20], felts[21], felts[22], felts[23]]), + Word::from([felts[24], felts[25], felts[26], felts[27]]), ] } -/// Decodes 6 Words (8 bytes/felt, little-endian) back to a UTF-8 string. -fn decode_field_from_words(words: &[Word; 6]) -> Result { - let mut bytes = [0u8; metadata::FIELD_MAX_BYTES]; +/// Decodes 7 Words (28 felts, 7 bytes/felt, length-prefixed) back to a UTF-8 string. +fn decode_field_from_words(words: &[Word; 7]) -> Result { + let mut felts = [Felt::ZERO; 28]; for (i, word) in words.iter().enumerate() { - for (j, f) in word.iter().enumerate() { - let v = f.as_int(); - bytes[i * 32 + j * 8..][..8].copy_from_slice(&v.to_le_bytes()); - } + felts[i * 4..i * 4 + 4].copy_from_slice(word.as_slice()); } - let len = bytes.iter().position(|&b| b == 0).unwrap_or(metadata::FIELD_MAX_BYTES); - String::from_utf8(bytes[..len].to_vec()).map_err(|_| FieldBytesError::InvalidUtf8) + decode_felts_to_utf8::<28>(&felts) } // TOKEN METADATA @@ -635,7 +681,7 @@ mod tests { #[test] fn description_too_long() { - let s = "a".repeat(193); + let s = "a".repeat(Description::MAX_BYTES + 1); assert!(Description::new(&s).is_err()); } diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 60a61646bf..4d6629eada 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -1,4 +1,4 @@ -//! Account / contract / faucet metadata (slots 0..23) +//! Account / contract / faucet metadata (slots 0..25) //! //! All of the following are metadata of the account (or faucet): token_symbol, decimals, //! max_supply, owner, name, mutability_config, description, logo URI, @@ -13,9 +13,9 @@ //! | `metadata::name_0` | first 4 felts of name | //! | `metadata::name_1` | last 4 felts of name | //! | `metadata::mutability_config` | `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` | -//! | `metadata::description_0..5` | description (6 Words, ~192 bytes) | -//! | `metadata::logo_uri_0..5` | logo URI (6 Words, ~192 bytes) | -//! | `metadata::external_link_0..5` | external link (6 Words, ~192 bytes) | +//! | `metadata::description_0..6` | description (7 Words, max 195 bytes) | +//! | `metadata::logo_uri_0..6` | logo URI (7 Words, max 195 bytes) | +//! | `metadata::external_link_0..6` | external link (7 Words, max 195 bytes) | //! //! Slot names use the `miden::standards::metadata::*` namespace, except for the //! owner which is defined by the ownable module @@ -42,12 +42,15 @@ //! `get_description`, `get_logo_uri`, `get_external_link`; for owner and mutable fields use a //! component that re-exports from fungible (e.g. network fungible faucet). //! -//! ## Name encoding (UTF-8) +//! ## String encoding (UTF-8) //! -//! The name slots hold opaque words. This crate defines a **convention** for human-readable -//! names: UTF-8 bytes, 4 bytes per felt, little-endian, up to 32 bytes (see [`name_from_utf8`], -//! [`name_to_utf8`]). There is no Miden-wide standard for string→felt encoding; this convention -//! ensures Rust and MASM (or other consumers) can interoperate when they all use these helpers. +//! All string fields use **7-bytes-per-felt, length-prefixed** encoding. The N felts are +//! serialized into a flat buffer of N × 7 bytes; byte 0 is the string length, followed by UTF-8 +//! content, zero-padded. Each 7-byte chunk is stored as a LE u64 with the high byte always zero, +//! so it always fits in a Goldilocks field element. +//! +//! The name slots hold 2 Words (8 felts, capacity 55 bytes, capped at 32). See +//! [`name_from_utf8`], [`name_to_utf8`] for convenience helpers. //! //! # Example //! @@ -116,7 +119,8 @@ pub static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { ] }); -/// Maximum length of a name in bytes when using the UTF-8 encoding (2 Words = 8 felts × 4 bytes). +/// Maximum length of a name in bytes when using the UTF-8 encoding (2 Words = 8 felts × 7 bytes +/// = 56 byte buffer − 1 length byte = 55 capacity, capped at 32). pub const NAME_UTF8_MAX_BYTES: usize = 32; /// Errors when encoding or decoding the metadata name as UTF-8. @@ -132,8 +136,8 @@ pub enum NameUtf8Error { /// Encodes a UTF-8 string into the 2-Word name format. /// -/// Bytes are packed little-endian, 4 bytes per felt (8 felts total). The string is -/// zero-padded to 32 bytes. Returns an error if the UTF-8 byte length exceeds 32. +/// Bytes are packed 7-bytes-per-felt, length-prefixed, into 8 felts (2 Words). +/// Returns an error if the UTF-8 byte length exceeds 32. /// /// Prefer using [`TokenName::new`] + [`TokenName::to_words`] directly. pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { @@ -143,8 +147,7 @@ pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { /// Decodes the 2-Word name format as UTF-8. /// -/// Assumes the name was encoded with [`name_from_utf8`] (little-endian, 4 bytes per felt). -/// Trailing zero bytes are trimmed before UTF-8 validation. +/// Assumes the name was encoded with [`name_from_utf8`] (7-bytes-per-felt, length-prefixed). /// /// Prefer using [`TokenName::try_from_words`] directly. pub fn name_to_utf8(words: &[Word; 2]) -> Result { @@ -161,8 +164,8 @@ pub static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| }); /// Maximum length of a metadata field (description, logo_uri, external_link) in bytes. -/// 6 Words = 24 felts × 8 bytes = 192 bytes. -pub const FIELD_MAX_BYTES: usize = 192; +/// 7 Words = 28 felts × 7 bytes = 196 byte buffer − 1 length byte = 195 bytes. +pub const FIELD_MAX_BYTES: usize = 195; /// Errors when encoding or decoding metadata fields. #[derive(Debug, Clone, Error)] @@ -175,21 +178,21 @@ pub enum FieldBytesError { InvalidUtf8, } -/// Encodes a UTF-8 string into 6 Words (24 felts). +/// Encodes a UTF-8 string into 7 Words (28 felts). /// -/// Bytes are packed little-endian, 8 bytes per felt (24 felts total). The string is zero-padded -/// to 192 bytes. Returns an error if the length exceeds 192. +/// Bytes are packed 7-bytes-per-felt, length-prefixed, into 28 felts (7 Words). +/// Returns an error if the length exceeds [`FIELD_MAX_BYTES`]. /// /// Prefer using [`Description::new`] + [`Description::to_words`] (or `LogoURI` / `ExternalLink`) /// directly. -pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 6], FieldBytesError> { +pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 7], FieldBytesError> { use crate::account::faucets::Description; let s = core::str::from_utf8(bytes).map_err(|_| FieldBytesError::InvalidUtf8)?; Ok(Description::new(s)?.to_words()) } -/// Description (6 Words = 24 felts), split across 6 slots. -pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { +/// Description (7 Words = 28 felts), split across 7 slots. +pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::description_0").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::description_1").expect("valid slot name"), @@ -197,11 +200,12 @@ pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| StorageSlotName::new("miden::standards::metadata::description_3").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::description_4").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::description_5").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::description_6").expect("valid slot name"), ] }); -/// Logo URI (6 Words = 24 felts), split across 6 slots. -pub static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { +/// Logo URI (7 Words = 28 felts), split across 7 slots. +pub static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::logo_uri_0").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::logo_uri_1").expect("valid slot name"), @@ -209,11 +213,12 @@ pub static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { StorageSlotName::new("miden::standards::metadata::logo_uri_3").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::logo_uri_4").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::logo_uri_5").expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::logo_uri_6").expect("valid slot name"), ] }); -/// External link (6 Words = 24 felts), split across 6 slots. -pub static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(|| { +/// External link (7 Words = 28 felts), split across 7 slots. +pub static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::external_link_0") .expect("valid slot name"), @@ -227,6 +232,8 @@ pub static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 6]> = LazyLock::new(| .expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::external_link_5") .expect("valid slot name"), + StorageSlotName::new("miden::standards::metadata::external_link_6") + .expect("valid slot name"), ] }); @@ -236,18 +243,18 @@ pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::ne .expect("storage slot name should be valid") }); -/// The advice map key used by `optional_set_description` to read the 6 field words. +/// The advice map key used by `optional_set_description` to read the 7 field words. /// /// Must match `DESCRIPTION_DATA_KEY` in `fungible.masm`. The value stored under this key -/// should be 24 felts: `[FIELD_0, FIELD_1, FIELD_2, FIELD_3, FIELD_4, FIELD_5]`. +/// should be 28 felts: `[FIELD_0, FIELD_1, FIELD_2, FIELD_3, FIELD_4, FIELD_5, FIELD_6]`. pub const DESCRIPTION_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]); -/// The advice map key used by `optional_set_logo_uri` to read the 6 field words. +/// The advice map key used by `optional_set_logo_uri` to read the 7 field words. pub const LOGO_URI_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]); -/// The advice map key used by `optional_set_external_link` to read the 6 field words. +/// The advice map key used by `optional_set_external_link` to read the 7 field words. pub const EXTERNAL_LINK_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); @@ -278,9 +285,9 @@ pub fn mutability_config_slot() -> &'static StorageSlotName { /// /// - Slot 2–3: name (2 Words = 8 felts) /// - Slot 4: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` -/// - Slot 5–10: description (6 Words) -/// - Slot 11–16: logo_uri (6 Words) -/// - Slot 17–22: external_link (6 Words) +/// - Slot 5–11: description (7 Words) +/// - Slot 12–18: logo_uri (7 Words) +/// - Slot 19–25: external_link (7 Words) #[derive(Debug, Clone, Default)] pub struct TokenMetadata { name: Option, @@ -349,17 +356,17 @@ impl TokenMetadata { &NAME_SLOTS[1] } - /// Returns the slot name for a description chunk by index (0..6). + /// Returns the slot name for a description chunk by index (0..7). pub fn description_slot(index: usize) -> &'static StorageSlotName { &DESCRIPTION_SLOTS[index] } - /// Returns the slot name for a logo URI chunk by index (0..6). + /// Returns the slot name for a logo URI chunk by index (0..7). pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName { &LOGO_URI_SLOTS[index] } - /// Returns the slot name for an external link chunk by index (0..6). + /// Returns the slot name for an external link chunk by index (0..7). pub fn external_link_slot(index: usize) -> &'static StorageSlotName { &EXTERNAL_LINK_SLOTS[index] } @@ -386,8 +393,8 @@ impl TokenMetadata { None }; - let read_field = |slots: &[StorageSlotName; 6]| -> Option<[Word; 6]> { - let mut field = [Word::default(); 6]; + let read_field = |slots: &[StorageSlotName; 7]| -> Option<[Word; 7]> { + let mut field = [Word::default(); 7]; let mut any_set = false; for (i, slot) in field.iter_mut().enumerate() { if let Ok(chunk) = storage.get_item(&slots[i]) { @@ -439,19 +446,19 @@ impl TokenMetadata { mutability_config_word, )); - let desc_words: [Word; 6] = + let desc_words: [Word; 7] = self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); for (i, word) in desc_words.iter().enumerate() { slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word)); } - let logo_words: [Word; 6] = + let logo_words: [Word; 7] = self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); for (i, word) in logo_words.iter().enumerate() { slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word)); } - let link_words: [Word; 6] = + let link_words: [Word; 7] = self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); for (i, word) in link_words.iter().enumerate() { slots @@ -715,12 +722,12 @@ mod tests { fn description_max_bytes_accepted() { let s = "a".repeat(Description::MAX_BYTES); let desc = Description::new(&s).unwrap(); - assert_eq!(desc.to_words().len(), 6); + assert_eq!(desc.to_words().len(), 7); } #[test] fn description_too_long_rejected() { - let s = "a".repeat(193); + let s = "a".repeat(super::FIELD_MAX_BYTES + 1); assert!(Description::new(&s).is_err()); } diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 9cb5207e20..f8cdc6b225 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -420,9 +420,9 @@ impl MockChainBuilder { owner_account_id: AccountId, token_supply: Option, max_supply_mutable: bool, - description: Option<([Word; 6], bool)>, - logo_uri: Option<([Word; 6], bool)>, - external_link: Option<([Word; 6], bool)>, + description: Option<([Word; 7], bool)>, + logo_uri: Option<([Word; 7], bool)>, + external_link: Option<([Word; 7], bool)>, ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply) .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?; diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index bf6c9ff888..2a362ddaa8 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -273,9 +273,9 @@ fn name_33_bytes_rejected() { )); } -/// Tests that description at full capacity (6 Words) is supported. +/// Tests that description at full capacity (7 Words) is supported. #[test] -fn description_6_words_full_capacity() { +fn description_7_words_full_capacity() { let desc_text = "a".repeat(Description::MAX_BYTES); let description_typed = Description::new(&desc_text).unwrap(); let description = description_typed.to_words(); @@ -292,13 +292,15 @@ fn description_6_words_full_capacity() { } } -/// Tests that field longer than 192 bytes (193 bytes) is rejected. +/// Tests that a field exceeding [`FIELD_MAX_BYTES`] is rejected. #[test] -fn field_193_bytes_rejected() { - let long_string = "a".repeat(193); +fn field_over_max_bytes_rejected() { + use miden_standards::account::metadata::FIELD_MAX_BYTES; + let over = FIELD_MAX_BYTES + 1; + let long_string = "a".repeat(over); let result = field_from_bytes(long_string.as_bytes()); assert!(result.is_err()); - assert!(matches!(result, Err(FieldBytesError::TooLong(193)))); + assert!(matches!(result, Err(FieldBytesError::TooLong(n)) if n == over)); } /// Tests that BasicFungibleFaucet with Info component (name/description) works correctly. @@ -1180,8 +1182,8 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { // ================================================================================================= /// Builds the advice map value for field setters. -fn field_advice_map_value(field: &[Word; 6]) -> Vec { - let mut value = Vec::with_capacity(24); +fn field_advice_map_value(field: &[Word; 7]) -> Vec { + let mut value = Vec::with_capacity(28); for word in field.iter() { value.extend(word.iter()); } @@ -1205,6 +1207,7 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "DSC", @@ -1225,6 +1228,7 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let tx_script = r#" @@ -1270,6 +1274,7 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let new_desc = [ Word::from([100u32, 101, 102, 103]), @@ -1278,6 +1283,7 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( @@ -1359,6 +1365,7 @@ async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<() Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let new_desc = [ Word::from([100u32, 101, 102, 103]), @@ -1367,6 +1374,7 @@ async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<() Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( @@ -1768,8 +1776,8 @@ async fn metadata_get_description_commitment_zero_field() -> anyhow::Result<()> .with_component(build_faucet_with_info(extension)) .build()?; - // Expected: RPO256 hash of 24 zero felts - let zero_felts = vec![Felt::new(0); 24]; + // Expected: RPO256 hash of 28 zero felts (7 Words) + let zero_felts = vec![Felt::new(0); 28]; let expected_commitment = Hasher::hash_elements(&zero_felts); let tx_script = format!( @@ -1819,6 +1827,7 @@ async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "LGO", @@ -1839,6 +1848,7 @@ async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let tx_script = r#" @@ -1883,6 +1893,7 @@ async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let new_logo = [ Word::from([100u32, 101, 102, 103]), @@ -1891,6 +1902,7 @@ async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( @@ -1967,6 +1979,7 @@ async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let new_logo = [ Word::from([100u32, 101, 102, 103]), @@ -1975,6 +1988,7 @@ async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( @@ -2041,6 +2055,7 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "EXL", @@ -2061,6 +2076,7 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let tx_script = r#" @@ -2106,6 +2122,7 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let new_link = [ Word::from([100u32, 101, 102, 103]), @@ -2114,6 +2131,7 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( @@ -2191,6 +2209,7 @@ async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result< Word::from([13u32, 14, 15, 16]), Word::from([17u32, 18, 19, 20]), Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), ]; let new_link = [ Word::from([100u32, 101, 102, 103]), @@ -2199,6 +2218,7 @@ async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result< Word::from([112u32, 113, 114, 115]), Word::from([116u32, 117, 118, 119]), Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( From 8eb49492f4fa5ed63c336d72a9a1fae7af6c628a Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 4 Mar 2026 17:30:20 -0300 Subject: [PATCH 08/56] refactor: replace RPO256 with Poseidon2 for hashing in fungible metadata and update related tests --- .../asm/standards/metadata/fungible.masm | 14 +++++----- .../src/account/faucets/token_metadata.rs | 2 +- crates/miden-testing/tests/metadata.rs | 26 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index d03c135532..83091885d2 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -7,7 +7,7 @@ use miden::protocol::active_account use miden::protocol::native_account use miden::standards::access::ownable -use miden::core::crypto::hashes::rpo256 +use miden::core::crypto::hashes::poseidon2 use miden::core::mem # ================================================================================================= @@ -395,7 +395,7 @@ pub proc get_description_commitment # Hash the 7 words (28 felts) in local memory via RPO256. # hash_elements expects [ptr, num_felts]. push.28 locaddr.FIELD_0_LOC - exec.rpo256::hash_elements + exec.poseidon2::hash_elements # => [COMMITMENT, pad(16)] depth=20 # Insert the 7-word preimage into the advice map keyed by COMMITMENT. @@ -440,7 +440,7 @@ pub proc get_description # Hash the 7 words (28 felts) in local memory. push.28 locaddr.FIELD_0_LOC # => [start_ptr, 28, dest_ptr, pad(15)] depth=18 - exec.rpo256::hash_elements + exec.poseidon2::hash_elements # => [COMMITMENT, dest_ptr, pad(15)] depth=20 # Insert preimage into advice map keyed by COMMITMENT. @@ -493,7 +493,7 @@ pub proc get_logo_uri_commitment loc_storew_be.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC - exec.rpo256::hash_elements + exec.poseidon2::hash_elements locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 @@ -527,7 +527,7 @@ pub proc get_logo_uri loc_storew_be.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC - exec.rpo256::hash_elements + exec.poseidon2::hash_elements locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 @@ -567,7 +567,7 @@ pub proc get_external_link_commitment loc_storew_be.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC - exec.rpo256::hash_elements + exec.poseidon2::hash_elements locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 @@ -601,7 +601,7 @@ pub proc get_external_link loc_storew_be.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC - exec.rpo256::hash_elements + exec.poseidon2::hash_elements locaddr.FIELD_0_LOC dup add.28 movdn.5 movdn.4 diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index bacd676068..b2a0be1a13 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -238,7 +238,7 @@ fn decode_felts_to_utf8(felts: &[Felt; N]) -> Result 7 bytes of data). if le[BYTES_PER_FELT] != 0 { diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 2a362ddaa8..627ab1ca25 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -5,7 +5,7 @@ extern crate alloc; use alloc::sync::Arc; use miden_crypto::hash::rpo::Rpo256 as Hasher; -use miden_processor::crypto::RpoRandomCoin; +use miden_crypto::rand::RpoRandomCoin; use miden_protocol::account::{ AccountBuilder, AccountId, @@ -572,7 +572,7 @@ async fn faucet_get_decimals_only() -> anyhow::Result<()> { .with_component(faucet) .build()?; - let expected_decimals = Felt::from(decimals).as_int(); + let expected_decimals = Felt::from(decimals).as_canonical_u64(); let tx_script = format!( r#" @@ -629,7 +629,7 @@ async fn faucet_get_token_symbol_only() -> anyhow::Result<()> { .with_component(faucet) .build()?; - let expected_symbol = Felt::from(token_symbol).as_int(); + let expected_symbol = Felt::from(token_symbol).as_canonical_u64(); let tx_script = format!( r#" @@ -687,7 +687,7 @@ async fn faucet_get_token_supply_only() -> anyhow::Result<()> { .with_component(faucet) .build()?; - let expected_token_supply = token_supply.as_int(); + let expected_token_supply = token_supply.as_canonical_u64(); let tx_script = format!( r#" @@ -744,9 +744,9 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { .with_component(faucet) .build()?; - let expected_symbol = Felt::from(token_symbol).as_int(); - let expected_decimals = Felt::from(decimals).as_int(); - let expected_max_supply = max_supply.as_int(); + let expected_symbol = Felt::from(token_symbol).as_canonical_u64(); + let expected_decimals = Felt::from(decimals).as_canonical_u64(); + let expected_max_supply = max_supply.as_canonical_u64(); let expected_token_supply = 0u64; let tx_script = format!( @@ -940,8 +940,8 @@ async fn metadata_get_owner_only() -> anyhow::Result<()> { .with_component(faucet) .build()?; - let expected_prefix = owner_account_id.prefix().as_felt().as_int(); - let expected_suffix = owner_account_id.suffix().as_int(); + let expected_prefix = owner_account_id.prefix().as_felt().as_canonical_u64(); + let expected_suffix = owner_account_id.suffix().as_canonical_u64(); let tx_script = format!( r#" @@ -1001,7 +1001,7 @@ async fn faucet_get_max_supply_only() -> anyhow::Result<()> { .with_component(faucet) .build()?; - let expected_max_supply = max_supply.as_int(); + let expected_max_supply = max_supply.as_canonical_u64(); let tx_script = format!( r#" @@ -1059,9 +1059,9 @@ async fn faucet_get_decimals_and_symbol_from_masm() -> anyhow::Result<()> { .build()?; // Compute expected felt values - let expected_decimals = Felt::from(decimals).as_int(); - let expected_symbol = Felt::from(token_symbol).as_int(); - let expected_max_supply = max_supply.as_int(); + let expected_decimals = Felt::from(decimals).as_canonical_u64(); + let expected_symbol = Felt::from(token_symbol).as_canonical_u64(); + let expected_max_supply = max_supply.as_canonical_u64(); let tx_script = format!( r#" From 441accb1fc542265db39fb4de45dfd6ec286a409 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 4 Mar 2026 17:36:20 -0300 Subject: [PATCH 09/56] refactor: format BasicFungibleFaucet initialization for improved readability --- .../miden-testing/src/mock_chain/chain_builder.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index f8cdc6b225..3829e38fcd 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -318,9 +318,16 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; - let basic_faucet = - BasicFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt, name, None, None, None) - .context("failed to create BasicFungibleFaucet")?; + let basic_faucet = BasicFungibleFaucet::new( + token_symbol, + DEFAULT_FAUCET_DECIMALS, + max_supply_felt, + name, + None, + None, + None, + ) + .context("failed to create BasicFungibleFaucet")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) From 02c6af18c2498af02afa56bb55383945c0ecd565 Mon Sep 17 00:00:00 2001 From: mayor Date: Wed, 4 Mar 2026 18:30:49 -0300 Subject: [PATCH 10/56] fix: adapt metadata MASM to little-endian word ordering after upstream migration The upstream LE migration (PR #2512) changed get_item to return [word[0], ..., word[3]] (word[0] on top) instead of [word[3], ..., word[0]]. This commit: - Reverses element extraction logic in all getters (get_token_metadata, get_max_supply, get_decimals, get_token_symbol, get_token_supply, mutability checks) - Fixes mutability flag extraction in all optional setters - Fixes max_supply replacement logic in optional_set_max_supply - Changes loc_storew_be/loc_loadw_be to loc_storew_le/loc_loadw_le to preserve natural word element ordering in memory for hash computations - Updates test hasher from Rpo256 to Poseidon2 to match MASM poseidon2 module - Fixes get_owner stack depth (get_item returns +2 net elements with 2-felt slot IDs) - Updates all inline MASM test assertions to match LE stack ordering --- .../asm/standards/access/ownable.masm | 4 + .../asm/standards/metadata/fungible.masm | 219 +++++++++--------- crates/miden-testing/tests/metadata.rs | 26 +-- 3 files changed, 127 insertions(+), 122 deletions(-) diff --git a/crates/miden-standards/asm/standards/access/ownable.masm b/crates/miden-standards/asm/standards/access/ownable.masm index b0591e71a5..bf9e2bef72 100644 --- a/crates/miden-standards/asm/standards/access/ownable.masm +++ b/crates/miden-standards/asm/standards/access/ownable.masm @@ -89,6 +89,10 @@ end #! Invocation: call pub proc get_owner exec.owner + # => [owner_suffix, owner_prefix, pad(16)] depth=18 + # get_item returns 4 elements from 2 inputs (net +2); owner drops 2, leaving +2 excess. + # Remove 2 excess pad elements to restore call-convention depth of 16. + movup.2 drop movup.2 drop # => [owner_suffix, owner_prefix, pad(14)] end diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index 83091885d2..f28a2cad98 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -68,7 +68,8 @@ const EXTERNAL_LINK_DATA_KEY = [0, 0, 0, 3] # PRIVATE HELPERS — single source of truth for slot access # ================================================================================================= -#! Loads token metadata word from storage. Output: [token_symbol, decimals, max_supply, token_supply]. +#! Loads token metadata word from storage. +#! Output: [token_supply, max_supply, decimals, token_symbol] (word[0] on top, little-endian). proc get_token_metadata_word push.TOKEN_METADATA_SLOT[0..2] exec.active_account::get_item @@ -87,8 +88,8 @@ proc get_name_chunk_1 end #! Loads the mutability config word. -#! Output: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)]. -#! (word[3] on top after get_item) +#! Output: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)]. +#! (word[0] on top after get_item, little-endian) proc get_mutability_config_word push.MUTABILITY_CONFIG_SLOT[0..2] exec.active_account::get_item @@ -191,24 +192,24 @@ end #! Returns the full token metadata word. #! #! The word is stored as [token_supply, max_supply, decimals, token_symbol] and -#! loaded onto the stack with word[3] on top. +#! loaded onto the stack with word[0] on top (little-endian). #! #! Inputs: [] -#! Outputs: [token_symbol, decimals, max_supply, token_supply, pad(12)] (word[3] on top) +#! Outputs: [token_supply, max_supply, decimals, token_symbol, pad(12)] (word[0] on top) #! #! Invocation: call pub proc get_token_metadata exec.get_token_metadata_word swapw dropw - # => [token_symbol, decimals, max_supply, token_supply, pad(12)] + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] end #! Returns the maximum supply. #! Inputs: [pad(16)] Outputs: [max_supply, pad(15)] Invocation: call pub proc get_max_supply exec.get_token_metadata - # => [token_symbol, decimals, max_supply, token_supply, pad(12)] - drop drop swap drop + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + drop movdn.2 drop drop # => [max_supply, pad(15)] end @@ -216,8 +217,8 @@ end #! Inputs: [pad(16)] Outputs: [decimals, pad(15)] Invocation: call pub proc get_decimals exec.get_token_metadata - # => [token_symbol, decimals, max_supply, token_supply, pad(12)] - drop movdn.2 drop drop + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + drop drop swap drop # => [decimals, pad(15)] end @@ -225,8 +226,8 @@ end #! Inputs: [pad(16)] Outputs: [token_symbol, pad(15)] Invocation: call pub proc get_token_symbol exec.get_token_metadata - # => [token_symbol, decimals, max_supply, token_supply, pad(12)] - movdn.3 drop drop drop + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + drop drop drop # => [token_symbol, pad(15)] end @@ -234,8 +235,8 @@ end #! Inputs: [pad(16)] Outputs: [token_supply, pad(15)] Invocation: call pub proc get_token_supply exec.get_token_metadata - # => [token_symbol, decimals, max_supply, token_supply, pad(12)] - drop drop drop + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] + movdn.3 drop drop drop # => [token_supply, pad(15)] end @@ -252,14 +253,14 @@ end @locals(8) pub proc get_name exec.get_name_chunk_1 - loc_storew_be.CHUNK_1_LOC dropw + loc_storew_le.CHUNK_1_LOC dropw exec.get_name_chunk_0 - loc_storew_be.CHUNK_0_LOC dropw + loc_storew_le.CHUNK_0_LOC dropw - loc_loadw_be.CHUNK_1_LOC + loc_loadw_le.CHUNK_1_LOC swapw - loc_loadw_be.CHUNK_0_LOC + loc_loadw_le.CHUNK_0_LOC # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] end @@ -269,17 +270,17 @@ end #! Returns the mutability config word. #! -#! After get_item, stack has word[3] on top: -#! [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] +#! After get_item, stack has word[0] on top (little-endian): +#! [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] #! #! Inputs: [pad(16)] -#! Outputs: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] +#! Outputs: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] #! #! Invocation: call pub proc get_mutability_config exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] end # ================================================================================================= @@ -288,15 +289,15 @@ end #! Returns whether max supply is mutable (single felt: 0 or 1). #! -#! Reads mutability_config word felt[3] (max_supply_mutable), which is on top after get_item. +#! Reads mutability_config word felt[3] (max_supply_mutable). #! #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_max_supply_mutable exec.get_mutability_config_word - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - movdn.3 drop drop drop + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop drop drop # => [max_supply_mutable, pad(15)] end @@ -307,13 +308,13 @@ end #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_description_mutable exec.get_mutability_config_word - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - drop drop drop - # => [desc_mutable, pad(12)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + movdn.3 drop drop drop + # => [desc_mutable, pad(15)] push.1 eq - # => [is_mutable, pad(12)] + # => [is_mutable, pad(15)] end #! Returns whether logo URI is mutable (flag == 1). @@ -323,13 +324,13 @@ end #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_logo_uri_mutable exec.get_mutability_config_word - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - drop drop swap drop - # => [logo_mutable, pad(12)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop movdn.2 drop drop + # => [logo_mutable, pad(15)] push.1 eq - # => [is_mutable, pad(12)] + # => [is_mutable, pad(15)] end #! Returns whether external link is mutable (flag == 1). @@ -339,13 +340,13 @@ end #! Inputs: [pad(16)] Outputs: [flag, pad(15)] Invocation: call pub proc is_external_link_mutable exec.get_mutability_config_word - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - drop movdn.2 drop drop - # => [extlink_mutable, pad(12)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + drop drop swap drop + # => [extlink_mutable, pad(15)] push.1 eq - # => [is_mutable, pad(12)] + # => [is_mutable, pad(15)] end # ================================================================================================= @@ -377,19 +378,19 @@ pub use ownable::get_owner @locals(28) pub proc get_description_commitment exec.get_description_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + loc_storew_le.FIELD_0_LOC dropw exec.get_description_chunk_1 - loc_storew_be.FIELD_1_LOC dropw + loc_storew_le.FIELD_1_LOC dropw exec.get_description_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + loc_storew_le.FIELD_2_LOC dropw exec.get_description_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + loc_storew_le.FIELD_3_LOC dropw exec.get_description_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + loc_storew_le.FIELD_4_LOC dropw exec.get_description_chunk_5 - loc_storew_be.FIELD_5_LOC dropw + loc_storew_le.FIELD_5_LOC dropw exec.get_description_chunk_6 - loc_storew_be.FIELD_6_LOC dropw + loc_storew_le.FIELD_6_LOC dropw # => [pad(16)] # Hash the 7 words (28 felts) in local memory via RPO256. @@ -422,19 +423,19 @@ end @locals(28) pub proc get_description exec.get_description_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + loc_storew_le.FIELD_0_LOC dropw exec.get_description_chunk_1 - loc_storew_be.FIELD_1_LOC dropw + loc_storew_le.FIELD_1_LOC dropw exec.get_description_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + loc_storew_le.FIELD_2_LOC dropw exec.get_description_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + loc_storew_le.FIELD_3_LOC dropw exec.get_description_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + loc_storew_le.FIELD_4_LOC dropw exec.get_description_chunk_5 - loc_storew_be.FIELD_5_LOC dropw + loc_storew_le.FIELD_5_LOC dropw exec.get_description_chunk_6 - loc_storew_be.FIELD_6_LOC dropw + loc_storew_le.FIELD_6_LOC dropw # => [dest_ptr, pad(15)] depth=16 # Hash the 7 words (28 felts) in local memory. @@ -478,19 +479,19 @@ end @locals(28) pub proc get_logo_uri_commitment exec.get_logo_uri_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + loc_storew_le.FIELD_0_LOC dropw exec.get_logo_uri_chunk_1 - loc_storew_be.FIELD_1_LOC dropw + loc_storew_le.FIELD_1_LOC dropw exec.get_logo_uri_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + loc_storew_le.FIELD_2_LOC dropw exec.get_logo_uri_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + loc_storew_le.FIELD_3_LOC dropw exec.get_logo_uri_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + loc_storew_le.FIELD_4_LOC dropw exec.get_logo_uri_chunk_5 - loc_storew_be.FIELD_5_LOC dropw + loc_storew_le.FIELD_5_LOC dropw exec.get_logo_uri_chunk_6 - loc_storew_be.FIELD_6_LOC dropw + loc_storew_le.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC exec.poseidon2::hash_elements @@ -512,19 +513,19 @@ end @locals(28) pub proc get_logo_uri exec.get_logo_uri_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + loc_storew_le.FIELD_0_LOC dropw exec.get_logo_uri_chunk_1 - loc_storew_be.FIELD_1_LOC dropw + loc_storew_le.FIELD_1_LOC dropw exec.get_logo_uri_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + loc_storew_le.FIELD_2_LOC dropw exec.get_logo_uri_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + loc_storew_le.FIELD_3_LOC dropw exec.get_logo_uri_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + loc_storew_le.FIELD_4_LOC dropw exec.get_logo_uri_chunk_5 - loc_storew_be.FIELD_5_LOC dropw + loc_storew_le.FIELD_5_LOC dropw exec.get_logo_uri_chunk_6 - loc_storew_be.FIELD_6_LOC dropw + loc_storew_le.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC exec.poseidon2::hash_elements @@ -552,19 +553,19 @@ end @locals(28) pub proc get_external_link_commitment exec.get_external_link_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + loc_storew_le.FIELD_0_LOC dropw exec.get_external_link_chunk_1 - loc_storew_be.FIELD_1_LOC dropw + loc_storew_le.FIELD_1_LOC dropw exec.get_external_link_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + loc_storew_le.FIELD_2_LOC dropw exec.get_external_link_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + loc_storew_le.FIELD_3_LOC dropw exec.get_external_link_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + loc_storew_le.FIELD_4_LOC dropw exec.get_external_link_chunk_5 - loc_storew_be.FIELD_5_LOC dropw + loc_storew_le.FIELD_5_LOC dropw exec.get_external_link_chunk_6 - loc_storew_be.FIELD_6_LOC dropw + loc_storew_le.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC exec.poseidon2::hash_elements @@ -586,19 +587,19 @@ end @locals(28) pub proc get_external_link exec.get_external_link_chunk_0 - loc_storew_be.FIELD_0_LOC dropw + loc_storew_le.FIELD_0_LOC dropw exec.get_external_link_chunk_1 - loc_storew_be.FIELD_1_LOC dropw + loc_storew_le.FIELD_1_LOC dropw exec.get_external_link_chunk_2 - loc_storew_be.FIELD_2_LOC dropw + loc_storew_le.FIELD_2_LOC dropw exec.get_external_link_chunk_3 - loc_storew_be.FIELD_3_LOC dropw + loc_storew_le.FIELD_3_LOC dropw exec.get_external_link_chunk_4 - loc_storew_be.FIELD_4_LOC dropw + loc_storew_le.FIELD_4_LOC dropw exec.get_external_link_chunk_5 - loc_storew_be.FIELD_5_LOC dropw + loc_storew_le.FIELD_5_LOC dropw exec.get_external_link_chunk_6 - loc_storew_be.FIELD_6_LOC dropw + loc_storew_le.FIELD_6_LOC dropw push.28 locaddr.FIELD_0_LOC exec.poseidon2::hash_elements @@ -633,12 +634,12 @@ end #! #! Invocation: call (from note script context) pub proc optional_set_description - # Read mutability config word: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - # desc_mutable is at bottom of the 4 elements; drop the top 3 - drop drop drop + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + # desc_mutable is on top (word[0]); drop the other 3 + movdn.3 drop drop drop # => [desc_mutable, pad(15)] push.1 eq assert.err=ERR_DESCRIPTION_NOT_MUTABLE @@ -703,14 +704,12 @@ end #! #! Invocation: call (from note script context) pub proc optional_set_logo_uri - # Read mutability config word: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - # logo_mutable is word[1]; drop top 2 (max_supply_mutable, extlink_mutable), then swap+drop - drop drop - # => [logo_mutable, desc_mutable, pad(14)] - swap drop + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + # logo_mutable is word[1]; drop desc_mutable, then keep logo and drop rest + drop movdn.2 drop drop # => [logo_mutable, pad(15)] push.1 eq assert.err=ERR_LOGO_URI_NOT_MUTABLE @@ -771,14 +770,12 @@ end #! #! Invocation: call (from note script context) pub proc optional_set_external_link - # Read mutability config word: [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(16)] + # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] exec.get_mutability_config_word swapw dropw - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] - # extlink_mutable is word[2]; drop top 1 (max_supply_mutable), then move down and drop - drop - # => [extlink_mutable, logo_mutable, desc_mutable, pad(13)] - movdn.2 drop drop + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] + # extlink_mutable is word[2]; drop top 2, swap+drop to get it + drop drop swap drop # => [extlink_mutable, pad(15)] push.1 eq assert.err=ERR_EXTERNAL_LINK_NOT_MUTABLE @@ -838,10 +835,13 @@ end pub proc optional_set_max_supply # 1. Check mutable flag from mutability config word exec.get_mutability_config_word - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, new_max_supply, pad(15)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, new_max_supply, pad(15)] + # max_supply_mutable is word[3]; bring it to top + movup.3 + # => [max_supply_mutable, desc_mutable, logo_mutable, extlink_mutable, new_max_supply, pad(15)] push.1 eq assert.err=ERR_MAX_SUPPLY_IMMUTABLE - # => [extlink_mutable, logo_mutable, desc_mutable, new_max_supply, pad(15)] + # => [desc_mutable, logo_mutable, extlink_mutable, new_max_supply, pad(15)] drop drop drop # => [new_max_supply, pad(15)] @@ -851,22 +851,23 @@ pub proc optional_set_max_supply # 3. Read current metadata word exec.get_token_metadata_word - # => [token_symbol, decimals, max_supply, token_supply, new_max_supply, pad(15)] + # => [token_supply, max_supply, decimals, token_symbol, new_max_supply, pad(15)] # 4. Validate: new_max_supply >= token_supply (i.e. token_supply <= new_max_supply) - dup.3 # copy token_supply to top + dup # copy token_supply (index 0) to top dup.5 # copy new_max_supply to top - # => [new_max_supply, token_supply, token_symbol, decimals, max_supply, token_supply, new_max_supply, ...] + # => [new_max_supply, token_supply, token_supply, max_supply, decimals, token_symbol, new_max_supply, ...] lte # token_supply <= new_max_supply assert.err=ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY - # => [token_symbol, decimals, max_supply, token_supply, new_max_supply, pad(15)] + # => [token_supply, max_supply, decimals, token_symbol, new_max_supply, pad(15)] - # 5. Replace max_supply with new_max_supply in the metadata word + # 5. Replace max_supply (word[1]) with new_max_supply in the metadata word movup.4 # bring new_max_supply to top - movdn.2 # => [token_symbol, decimals, new_max_supply, max_supply, token_supply, pad(15)] - movup.3 # bring old max_supply to top + # => [new_max_supply, token_supply, max_supply, decimals, token_symbol, pad(15)] + swap # => [token_supply, new_max_supply, max_supply, decimals, token_symbol, pad(15)] + movup.2 # => [max_supply, token_supply, new_max_supply, decimals, token_symbol, pad(15)] drop # discard old max_supply - # => [token_symbol, decimals, new_max_supply, token_supply, pad(15)] + # => [token_supply, new_max_supply, decimals, token_symbol, pad(15)] # 6. Write updated metadata word back to storage push.TOKEN_METADATA_SLOT[0..2] diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 627ab1ca25..7d2622b152 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -4,7 +4,7 @@ extern crate alloc; use alloc::sync::Arc; -use miden_crypto::hash::rpo::Rpo256 as Hasher; +use miden_crypto::hash::poseidon2::Poseidon2 as Hasher; use miden_crypto::rand::RpoRandomCoin; use miden_protocol::account::{ AccountBuilder, @@ -753,11 +753,11 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { r#" begin call.::miden::standards::metadata::fungible::get_token_metadata - # => [token_symbol, decimals, max_supply, token_supply, pad(12)] - push.{expected_symbol} assert_eq.err="token_symbol does not match" - push.{expected_decimals} assert_eq.err="decimals does not match" - push.{expected_max_supply} assert_eq.err="max_supply does not match" + # => [token_supply, max_supply, decimals, token_symbol, pad(12)] push.{expected_token_supply} assert_eq.err="token_supply does not match" + push.{expected_max_supply} assert_eq.err="max_supply does not match" + push.{expected_decimals} assert_eq.err="decimals does not match" + push.{expected_symbol} assert_eq.err="token_symbol does not match" end "#, ); @@ -877,15 +877,15 @@ async fn metadata_get_config_only() -> anyhow::Result<()> { begin # Check mutability config call.::miden::standards::metadata::fungible::get_mutability_config - # => [max_supply_mutable, extlink_mutable, logo_mutable, desc_mutable, pad(12)] + # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] push.1 - assert_eq.err="max_supply_mutable should be 1" - push.0 - assert_eq.err="extlink_mutable should be 0" + assert_eq.err="desc_mutable should be 1" push.0 assert_eq.err="logo_mutable should be 0" + push.0 + assert_eq.err="extlink_mutable should be 0" push.1 - assert_eq.err="desc_mutable should be 1" + assert_eq.err="max_supply_mutable should be 1" end "#; @@ -947,11 +947,11 @@ async fn metadata_get_owner_only() -> anyhow::Result<()> { r#" begin call.::miden::standards::metadata::fungible::get_owner - # => [owner_prefix, owner_suffix, pad(14)] - push.{expected_prefix} - assert_eq.err="owner prefix does not match" + # => [owner_suffix, owner_prefix, pad(14)] push.{expected_suffix} assert_eq.err="owner suffix does not match" + push.{expected_prefix} + assert_eq.err="owner prefix does not match" push.0 assert_eq.err="clean stack: pad must be 0" end From 9a9a4dc9c77abc0a7eb92a45efd79df519b47c8d Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 5 Mar 2026 11:16:57 -0300 Subject: [PATCH 11/56] chore: adding type as entry instead of separate variables --- .../src/account/faucets/basic_fungible.rs | 40 +++++-------------- .../src/account/faucets/network_fungible.rs | 36 ++++------------- 2 files changed, 19 insertions(+), 57 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 929ad843a5..0ddbe2c1c5 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -294,9 +294,7 @@ impl TryFrom<&Account> for BasicFungibleFaucet { } /// Creates a new faucet account with basic fungible faucet interface, -/// account storage type, specified authentication scheme, and provided meta data (token symbol, -/// decimals, max supply). Optional `name`, `description`, `logo_uri`, and `external_link` are -/// stored in the faucet's metadata storage slots when provided. +/// account storage type, specified authentication scheme, and provided metadata. /// /// The basic faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -311,18 +309,11 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// - [`BasicFungibleFaucet`] /// - [`AuthSingleSigAcl`] /// - Token metadata (name, description, etc.) when provided via [`BasicFungibleFaucet::with_info`] -#[allow(clippy::too_many_arguments)] pub fn create_basic_fungible_faucet( init_seed: [u8; 32], - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, + metadata: FungibleTokenMetadata, account_storage_mode: AccountStorageMode, auth_method: AuthMethod, - name: TokenName, - description: Option, - logo_uri: Option, - external_link: Option, ) -> Result { let distribute_proc_root = BasicFungibleFaucet::distribute_digest(); @@ -354,27 +345,18 @@ pub fn create_basic_fungible_faucet( }, }; - let mut info = TokenMetadataInfo::new().with_name(name.clone()); - if let Some(d) = &description { + let mut info = TokenMetadataInfo::new().with_name(metadata.name().clone()); + if let Some(d) = metadata.description() { info = info.with_description(d.clone(), false); } - if let Some(l) = &logo_uri { + if let Some(l) = metadata.logo_uri() { info = info.with_logo_uri(l.clone(), false); } - if let Some(e) = &external_link { + if let Some(e) = metadata.external_link() { info = info.with_external_link(e.clone(), false); } - let faucet = BasicFungibleFaucet::new( - symbol, - decimals, - max_supply, - name, - description, - logo_uri, - external_link, - )? - .with_info(info); + let faucet = BasicFungibleFaucet::from_metadata(metadata).with_info(info); let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) @@ -405,6 +387,7 @@ mod tests { Description, Felt, FungibleFaucetError, + FungibleTokenMetadata, TokenName, TokenSymbol, create_basic_fungible_faucet, @@ -436,19 +419,18 @@ mod tests { let token_name = TokenName::new(token_name_string).unwrap(); let description = Description::new(description_string).unwrap(); - let faucet_account = create_basic_fungible_faucet( - init_seed, + let metadata = FungibleTokenMetadata::new( token_symbol, decimals, max_supply, - storage_mode, - auth_method, token_name, Some(description), None, None, ) .unwrap(); + let faucet_account = + create_basic_fungible_faucet(init_seed, metadata, storage_mode, auth_method).unwrap(); // The falcon auth component's public key should be present. assert_eq!( diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 03982d561a..7d9d1aec3d 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -352,10 +352,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { } } -/// Creates a new faucet account with network fungible faucet interface and provided metadata -/// (token symbol, decimals, max supply, owner account ID). Optional `name`, `description`, -/// `logo_uri`, and `external_link` are stored in the faucet's metadata storage slots when -/// provided. +/// Creates a new faucet account with network fungible faucet interface and provided metadata. /// /// The network faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -371,42 +368,25 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// /// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] type and /// contains no additional storage slots for its auth ([`NoAuth`]). -#[allow(clippy::too_many_arguments)] pub fn create_network_fungible_faucet( init_seed: [u8; 32], - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - owner_account_id: AccountId, - name: TokenName, - description: Option, - logo_uri: Option, - external_link: Option, + metadata: FungibleTokenMetadata, + owner: AccountId, ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let mut info = TokenMetadataInfo::new().with_name(name.clone()); - if let Some(d) = &description { + let mut info = TokenMetadataInfo::new().with_name(metadata.name().clone()); + if let Some(d) = metadata.description() { info = info.with_description(d.clone(), false); } - if let Some(l) = &logo_uri { + if let Some(l) = metadata.logo_uri() { info = info.with_logo_uri(l.clone(), false); } - if let Some(e) = &external_link { + if let Some(e) = metadata.external_link() { info = info.with_external_link(e.clone(), false); } - let faucet = NetworkFungibleFaucet::new( - symbol, - decimals, - max_supply, - owner_account_id, - name, - description, - logo_uri, - external_link, - )? - .with_info(info); + let faucet = NetworkFungibleFaucet::from_metadata(metadata, owner).with_info(info); let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) From e750a7504de38840bf761781e52d049baae62d01 Mon Sep 17 00:00:00 2001 From: igamigo Date: Thu, 5 Mar 2026 20:51:57 -0300 Subject: [PATCH 12/56] refactor: enforce defining supported types on metadata (#2554) --- CHANGELOG.md | 1 + crates/miden-agglayer/src/lib.rs | 10 ++-- .../src/account/account_id/account_type.rs | 18 ++++++++ .../miden-protocol/src/account/builder/mod.rs | 4 +- crates/miden-protocol/src/account/code/mod.rs | 5 +- .../src/account/component/metadata/mod.rs | 46 ++++--------------- .../src/account/component/mod.rs | 15 +++--- .../src/account/component/storage/toml/mod.rs | 3 +- crates/miden-protocol/src/account/mod.rs | 12 +++-- .../src/testing/account_code.rs | 3 +- .../src/testing/add_component.rs | 7 ++- .../src/testing/component_metadata.rs | 3 +- .../src/testing/noop_auth_component.rs | 8 ++-- .../src/account/auth/multisig.rs | 4 +- .../src/account/auth/no_auth.rs | 7 ++- .../src/account/auth/singlesig.rs | 5 +- .../src/account/auth/singlesig_acl.rs | 4 +- .../src/account/faucets/basic_fungible.rs | 8 ++-- .../src/account/faucets/network_fungible.rs | 10 ++-- .../src/account/interface/test.rs | 8 ++-- .../src/account/metadata/mod.rs | 7 +-- .../src/account/wallets/mod.rs | 5 +- .../account_component/conditional_auth.rs | 8 ++-- .../testing/account_component/incr_nonce.rs | 8 ++-- .../mock_account_component.rs | 14 ++++-- .../mock_faucet_component.rs | 9 ++-- .../src/kernel_tests/tx/test_faucet.rs | 6 ++- 27 files changed, 117 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 386f110b59..81937eb05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### Changes +- [BREAKING] Made `supported_types` a required parameter of `AccountComponentMetadata::new()`; removed `with_supported_type`, `with_supported_types`, `with_supports_all_types`, and `with_supports_regular_types` builder methods; added `AccountType::all()` and `AccountType::regular()` helpers ([#2554](https://github.com/0xMiden/protocol/pull/2554)). - [BREAKING] Migrated to miden-vm 0.21 and miden-crypto 0.22 ([#2508](https://github.com/0xMiden/miden-base/pull/2508)). - [BREAKING] The stack orientation changed from big-endian to little-endian - see PR description ([#2508](https://github.com/0xMiden/miden-base/pull/2508)). - [BREAKING] The native hash function changed from RPO256 to Poseidon2 - see PR description ([#2508](https://github.com/0xMiden/miden-base/pull/2508)). diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 5215142e58..47f5b776f2 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -97,9 +97,8 @@ fn agglayer_faucet_component_library() -> Library { /// Creates an AggLayer Bridge component with the specified storage slots. fn bridge_component(storage_slots: Vec) -> AccountComponent { let library = agglayer_bridge_component_library(); - let metadata = AccountComponentMetadata::new("agglayer::bridge") - .with_description("Bridge component for AggLayer") - .with_supports_all_types(); + let metadata = AccountComponentMetadata::new("agglayer::bridge", AccountType::all()) + .with_description("Bridge component for AggLayer"); AccountComponent::new(library, storage_slots, metadata) .expect("bridge component should satisfy the requirements of a valid account component") @@ -254,9 +253,8 @@ impl From for AccountComponent { /// validates CLAIM notes against a bridge MMR account before minting assets. fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { let library = agglayer_faucet_component_library(); - let metadata = AccountComponentMetadata::new("agglayer::faucet") - .with_description("AggLayer faucet component with bridge validation") - .with_supported_type(AccountType::FungibleFaucet); + let metadata = AccountComponentMetadata::new("agglayer::faucet", [AccountType::FungibleFaucet]) + .with_description("AggLayer faucet component with bridge validation"); AccountComponent::new(library, storage_slots, metadata).expect( "agglayer_faucet component should satisfy the requirements of a valid account component", diff --git a/crates/miden-protocol/src/account/account_id/account_type.rs b/crates/miden-protocol/src/account/account_id/account_type.rs index e81f1d1370..1ea4c02f98 100644 --- a/crates/miden-protocol/src/account/account_id/account_type.rs +++ b/crates/miden-protocol/src/account/account_id/account_type.rs @@ -29,6 +29,24 @@ pub enum AccountType { } impl AccountType { + /// Returns all account types. + pub fn all() -> [AccountType; 4] { + [ + AccountType::FungibleFaucet, + AccountType::NonFungibleFaucet, + AccountType::RegularAccountImmutableCode, + AccountType::RegularAccountUpdatableCode, + ] + } + + /// Returns the regular account types (immutable and updatable code). + pub fn regular() -> [AccountType; 2] { + [ + AccountType::RegularAccountImmutableCode, + AccountType::RegularAccountUpdatableCode, + ] + } + /// Returns `true` if the account is a faucet. pub fn is_faucet(&self) -> bool { matches!(self, Self::FungibleFaucet | Self::NonFungibleFaucet) diff --git a/crates/miden-protocol/src/account/builder/mod.rs b/crates/miden-protocol/src/account/builder/mod.rs index bd5994324c..7a0e7bec6e 100644 --- a/crates/miden-protocol/src/account/builder/mod.rs +++ b/crates/miden-protocol/src/account/builder/mod.rs @@ -344,7 +344,7 @@ mod tests { value[0] = Felt::new(custom.slot0); let metadata = - AccountComponentMetadata::new("test::custom_component1").with_supports_all_types(); + AccountComponentMetadata::new("test::custom_component1", AccountType::all()); AccountComponent::new( CUSTOM_LIBRARY1.clone(), vec![StorageSlot::with_value(CUSTOM_COMPONENT1_SLOT_NAME.clone(), value)], @@ -366,7 +366,7 @@ mod tests { value1[3] = Felt::new(custom.slot1); let metadata = - AccountComponentMetadata::new("test::custom_component2").with_supports_all_types(); + AccountComponentMetadata::new("test::custom_component2", AccountType::all()); AccountComponent::new( CUSTOM_LIBRARY2.clone(), vec![ diff --git a/crates/miden-protocol/src/account/code/mod.rs b/crates/miden-protocol/src/account/code/mod.rs index b934ebe23f..c8825b9f3c 100644 --- a/crates/miden-protocol/src/account/code/mod.rs +++ b/crates/miden-protocol/src/account/code/mod.rs @@ -448,7 +448,7 @@ mod tests { #[test] fn test_account_code_no_auth_component() { let library = Assembler::default().assemble_library([CODE]).unwrap(); - let metadata = AccountComponentMetadata::new("test::no_auth").with_supports_all_types(); + let metadata = AccountComponentMetadata::new("test::no_auth", AccountType::all()); let component = AccountComponent::new(library, vec![], metadata).unwrap(); let err = @@ -486,8 +486,7 @@ mod tests { "; let library = Assembler::default().assemble_library([code_with_multiple_auth]).unwrap(); - let metadata = - AccountComponentMetadata::new("test::multiple_auth").with_supports_all_types(); + let metadata = AccountComponentMetadata::new("test::multiple_auth", AccountType::all()); let component = AccountComponent::new(library, vec![], metadata).unwrap(); let err = diff --git a/crates/miden-protocol/src/account/component/metadata/mod.rs b/crates/miden-protocol/src/account/component/metadata/mod.rs index 6061203dc6..b02c007014 100644 --- a/crates/miden-protocol/src/account/component/metadata/mod.rs +++ b/crates/miden-protocol/src/account/component/metadata/mod.rs @@ -41,7 +41,6 @@ use crate::utils::serde::{ /// ``` /// use std::collections::BTreeMap; /// -/// use miden_protocol::account::StorageSlotName; /// use miden_protocol::account::component::{ /// AccountComponentMetadata, /// FeltSchema, @@ -54,6 +53,7 @@ use crate::utils::serde::{ /// WordSchema, /// WordValue, /// }; +/// use miden_protocol::account::{AccountType, StorageSlotName}; /// /// let slot_name = StorageSlotName::new("demo::test_value")?; /// @@ -69,7 +69,7 @@ use crate::utils::serde::{ /// StorageSlotSchema::Value(ValueSlotSchema::new(Some("demo slot".into()), word)), /// )])?; /// -/// let metadata = AccountComponentMetadata::new("test name") +/// let metadata = AccountComponentMetadata::new("test name", AccountType::all()) /// .with_description("description of the component") /// .with_storage_schema(storage_schema); /// @@ -105,21 +105,23 @@ pub struct AccountComponentMetadata { } impl AccountComponentMetadata { - /// Create a new [AccountComponentMetadata] with the given name. + /// Create a new [AccountComponentMetadata] with the given name and supported account types. /// /// Other fields are initialized to sensible defaults: /// - `description`: empty string /// - `version`: 1.0.0 - /// - `supported_types`: empty set /// - `storage_schema`: default (empty) /// /// Use the `with_*` mutator methods to customize these fields. - pub fn new(name: impl Into) -> Self { + pub fn new( + name: impl Into, + supported_types: impl IntoIterator, + ) -> Self { Self { name: name.into(), description: String::new(), version: Version::new(1, 0, 0), - supported_types: BTreeSet::new(), + supported_types: supported_types.into_iter().collect(), storage_schema: StorageSchema::default(), } } @@ -136,38 +138,6 @@ impl AccountComponentMetadata { self } - /// Adds a supported account type to the component. - pub fn with_supported_type(mut self, account_type: AccountType) -> Self { - self.supported_types.insert(account_type); - self - } - - /// Sets the supported account types of the component. - pub fn with_supported_types(mut self, supported_types: BTreeSet) -> Self { - self.supported_types = supported_types; - self - } - - /// Sets the component to support all account types. - pub fn with_supports_all_types(mut self) -> Self { - self.supported_types.extend([ - AccountType::FungibleFaucet, - AccountType::NonFungibleFaucet, - AccountType::RegularAccountImmutableCode, - AccountType::RegularAccountUpdatableCode, - ]); - self - } - - /// Sets the component to support regular account types (immutable and updatable code). - pub fn with_supports_regular_types(mut self) -> Self { - self.supported_types.extend([ - AccountType::RegularAccountImmutableCode, - AccountType::RegularAccountUpdatableCode, - ]); - self - } - /// Sets the storage schema of the component. pub fn with_storage_schema(mut self, schema: StorageSchema) -> Self { self.storage_schema = schema; diff --git a/crates/miden-protocol/src/account/component/mod.rs b/crates/miden-protocol/src/account/component/mod.rs index b102087f3a..b9da72c1f5 100644 --- a/crates/miden-protocol/src/account/component/mod.rs +++ b/crates/miden-protocol/src/account/component/mod.rs @@ -256,10 +256,12 @@ mod tests { let library = Assembler::default().assemble_library([CODE]).unwrap(); // Test with metadata - let metadata = AccountComponentMetadata::new("test_component") - .with_description("A test component") - .with_version(Version::new(1, 0, 0)) - .with_supported_type(AccountType::RegularAccountImmutableCode); + let metadata = AccountComponentMetadata::new( + "test_component", + [AccountType::RegularAccountImmutableCode], + ) + .with_description("A test component") + .with_version(Version::new(1, 0, 0)); let metadata_bytes = metadata.to_bytes(); let package_with_metadata = Package { @@ -308,10 +310,9 @@ mod tests { let component_code = AccountComponentCode::from(library.clone()); // Create metadata for the component - let metadata = AccountComponentMetadata::new("test_component") + let metadata = AccountComponentMetadata::new("test_component", AccountType::regular()) .with_description("A test component") - .with_version(Version::new(1, 0, 0)) - .with_supports_regular_types(); + .with_version(Version::new(1, 0, 0)); // Test with empty init data - this tests the complete workflow: // Library + Metadata -> AccountComponent diff --git a/crates/miden-protocol/src/account/component/storage/toml/mod.rs b/crates/miden-protocol/src/account/component/storage/toml/mod.rs index d9e9450dd1..a5850d1afb 100644 --- a/crates/miden-protocol/src/account/component/storage/toml/mod.rs +++ b/crates/miden-protocol/src/account/component/storage/toml/mod.rs @@ -69,10 +69,9 @@ impl AccountComponentMetadata { } let storage_schema = StorageSchema::new(fields)?; - Ok(Self::new(raw.name) + Ok(Self::new(raw.name, raw.supported_types) .with_description(raw.description) .with_version(raw.version) - .with_supported_types(raw.supported_types) .with_storage_schema(storage_schema)) } diff --git a/crates/miden-protocol/src/account/mod.rs b/crates/miden-protocol/src/account/mod.rs index 2653c37149..0396dbac43 100644 --- a/crates/miden-protocol/src/account/mod.rs +++ b/crates/miden-protocol/src/account/mod.rs @@ -803,10 +803,14 @@ mod tests { let library1 = Assembler::default().assemble_library([code1]).unwrap(); // This component support all account types except the regular account with updatable code. - let metadata = AccountComponentMetadata::new("test::component1") - .with_supported_type(AccountType::FungibleFaucet) - .with_supported_type(AccountType::NonFungibleFaucet) - .with_supported_type(AccountType::RegularAccountImmutableCode); + let metadata = AccountComponentMetadata::new( + "test::component1", + [ + AccountType::FungibleFaucet, + AccountType::NonFungibleFaucet, + AccountType::RegularAccountImmutableCode, + ], + ); let component1 = AccountComponent::new(library1, vec![], metadata).unwrap(); let err = Account::initialize_from_components( diff --git a/crates/miden-protocol/src/testing/account_code.rs b/crates/miden-protocol/src/testing/account_code.rs index 6fecd9fe48..ea98aeb1cf 100644 --- a/crates/miden-protocol/src/testing/account_code.rs +++ b/crates/miden-protocol/src/testing/account_code.rs @@ -23,8 +23,7 @@ impl AccountCode { let library = Assembler::default() .assemble_library([CODE]) .expect("mock account component should assemble"); - let metadata = - AccountComponentMetadata::new("miden::testing::mock").with_supports_all_types(); + let metadata = AccountComponentMetadata::new("miden::testing::mock", AccountType::all()); let component = AccountComponent::new(library, vec![], metadata).unwrap(); Self::from_components( diff --git a/crates/miden-protocol/src/testing/add_component.rs b/crates/miden-protocol/src/testing/add_component.rs index 6d8712a231..98dd2b8629 100644 --- a/crates/miden-protocol/src/testing/add_component.rs +++ b/crates/miden-protocol/src/testing/add_component.rs @@ -1,5 +1,5 @@ -use crate::account::AccountComponent; use crate::account::component::AccountComponentMetadata; +use crate::account::{AccountComponent, AccountType}; use crate::assembly::{Assembler, Library}; use crate::utils::sync::LazyLock; @@ -25,9 +25,8 @@ pub struct AddComponent; impl From for AccountComponent { fn from(_: AddComponent) -> Self { - let metadata = AccountComponentMetadata::new("miden::testing::add") - .with_description("Add component for testing") - .with_supports_all_types(); + let metadata = AccountComponentMetadata::new("miden::testing::add", AccountType::all()) + .with_description("Add component for testing"); AccountComponent::new(ADD_LIBRARY.clone(), vec![], metadata) .expect("component should be valid") diff --git a/crates/miden-protocol/src/testing/component_metadata.rs b/crates/miden-protocol/src/testing/component_metadata.rs index f86e077421..2586d5e1dd 100644 --- a/crates/miden-protocol/src/testing/component_metadata.rs +++ b/crates/miden-protocol/src/testing/component_metadata.rs @@ -1,9 +1,10 @@ +use crate::account::AccountType; use crate::account::component::AccountComponentMetadata; impl AccountComponentMetadata { /// Creates a mock [`AccountComponentMetadata`] with the given name that supports all account /// types. pub fn mock(name: &str) -> Self { - AccountComponentMetadata::new(name).with_supports_all_types() + AccountComponentMetadata::new(name, AccountType::all()) } } diff --git a/crates/miden-protocol/src/testing/noop_auth_component.rs b/crates/miden-protocol/src/testing/noop_auth_component.rs index 1bd6f6a202..5a7880e7f8 100644 --- a/crates/miden-protocol/src/testing/noop_auth_component.rs +++ b/crates/miden-protocol/src/testing/noop_auth_component.rs @@ -1,5 +1,5 @@ -use crate::account::AccountComponent; use crate::account::component::AccountComponentMetadata; +use crate::account::{AccountComponent, AccountType}; use crate::assembly::{Assembler, Library}; use crate::utils::sync::LazyLock; @@ -26,9 +26,9 @@ pub struct NoopAuthComponent; impl From for AccountComponent { fn from(_: NoopAuthComponent) -> Self { - let metadata = AccountComponentMetadata::new("miden::testing::noop_auth") - .with_description("No-op auth component for testing") - .with_supports_all_types(); + let metadata = + AccountComponentMetadata::new("miden::testing::noop_auth", AccountType::all()) + .with_description("No-op auth component for testing"); AccountComponent::new(NOOP_AUTH_LIBRARY.clone(), vec![], metadata) .expect("component should be valid") diff --git a/crates/miden-standards/src/account/auth/multisig.rs b/crates/miden-standards/src/account/auth/multisig.rs index c463e89ddb..ef84292a47 100644 --- a/crates/miden-standards/src/account/auth/multisig.rs +++ b/crates/miden-standards/src/account/auth/multisig.rs @@ -12,6 +12,7 @@ use miden_protocol::account::component::{ }; use miden_protocol::account::{ AccountComponent, + AccountType, StorageMap, StorageMapKey, StorageSlot, @@ -303,9 +304,8 @@ impl From for AccountComponent { ]) .expect("storage schema should be valid"); - let metadata = AccountComponentMetadata::new(AuthMultisig::NAME) + let metadata = AccountComponentMetadata::new(AuthMultisig::NAME, AccountType::all()) .with_description("Multisig authentication component using hybrid signature schemes") - .with_supports_all_types() .with_storage_schema(storage_schema); AccountComponent::new(multisig_library(), storage_slots, metadata).expect( diff --git a/crates/miden-standards/src/account/auth/no_auth.rs b/crates/miden-standards/src/account/auth/no_auth.rs index 51da556554..f19b884e5c 100644 --- a/crates/miden-standards/src/account/auth/no_auth.rs +++ b/crates/miden-standards/src/account/auth/no_auth.rs @@ -1,5 +1,5 @@ -use miden_protocol::account::AccountComponent; use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{AccountComponent, AccountType}; use crate::account::components::no_auth_library; @@ -37,9 +37,8 @@ impl Default for NoAuth { impl From for AccountComponent { fn from(_: NoAuth) -> Self { - let metadata = AccountComponentMetadata::new(NoAuth::NAME) - .with_description("No authentication component") - .with_supports_all_types(); + let metadata = AccountComponentMetadata::new(NoAuth::NAME, AccountType::all()) + .with_description("No authentication component"); AccountComponent::new(no_auth_library(), vec![], metadata) .expect("NoAuth component should satisfy the requirements of a valid account component") diff --git a/crates/miden-standards/src/account/auth/singlesig.rs b/crates/miden-standards/src/account/auth/singlesig.rs index b8716f08dc..9581bfbf40 100644 --- a/crates/miden-standards/src/account/auth/singlesig.rs +++ b/crates/miden-standards/src/account/auth/singlesig.rs @@ -6,7 +6,7 @@ use miden_protocol::account::component::{ StorageSchema, StorageSlotSchema, }; -use miden_protocol::account::{AccountComponent, StorageSlot, StorageSlotName}; +use miden_protocol::account::{AccountComponent, AccountType, StorageSlot, StorageSlotName}; use miden_protocol::utils::sync::LazyLock; use crate::account::components::singlesig_library; @@ -83,9 +83,8 @@ impl From for AccountComponent { ]) .expect("storage schema should be valid"); - let metadata = AccountComponentMetadata::new(AuthSingleSig::NAME) + let metadata = AccountComponentMetadata::new(AuthSingleSig::NAME, AccountType::all()) .with_description("Authentication component using ECDSA K256 Keccak or Rpo Falcon 512 signature scheme") - .with_supports_all_types() .with_storage_schema(storage_schema); let storage_slots = vec![ diff --git a/crates/miden-standards/src/account/auth/singlesig_acl.rs b/crates/miden-standards/src/account/auth/singlesig_acl.rs index b3ca1c4ec7..2da3f212c5 100644 --- a/crates/miden-standards/src/account/auth/singlesig_acl.rs +++ b/crates/miden-standards/src/account/auth/singlesig_acl.rs @@ -11,6 +11,7 @@ use miden_protocol::account::component::{ use miden_protocol::account::{ AccountCode, AccountComponent, + AccountType, StorageMap, StorageMapKey, StorageSlot, @@ -291,9 +292,8 @@ impl From for AccountComponent { ]) .expect("storage schema should be valid"); - let metadata = AccountComponentMetadata::new(AuthSingleSigAcl::NAME) + let metadata = AccountComponentMetadata::new(AuthSingleSigAcl::NAME, AccountType::all()) .with_description("Authentication component with procedure-based ACL using ECDSA K256 Keccak or Rpo Falcon 512 signature scheme") - .with_supports_all_types() .with_storage_schema(storage_schema); AccountComponent::new(singlesig_acl_library(), storage_slots, metadata).expect( diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 0ddbe2c1c5..ab69622e7a 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -263,10 +263,10 @@ impl From for AccountComponent { let storage_schema = StorageSchema::new([BasicFungibleFaucet::metadata_slot_schema()]) .expect("storage schema should be valid"); - let metadata = AccountComponentMetadata::new(BasicFungibleFaucet::NAME) - .with_description("Basic fungible faucet component for minting and burning tokens") - .with_supported_type(AccountType::FungibleFaucet) - .with_storage_schema(storage_schema); + let metadata = + AccountComponentMetadata::new(BasicFungibleFaucet::NAME, [AccountType::FungibleFaucet]) + .with_description("Basic fungible faucet component for minting and burning tokens") + .with_storage_schema(storage_schema); AccountComponent::new(basic_fungible_faucet_library(), slots, metadata) .expect("basic fungible faucet component should satisfy the requirements of a valid account component") diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 7d9d1aec3d..880b97a25d 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -322,10 +322,12 @@ impl From for AccountComponent { ]) .expect("storage schema should be valid"); - let metadata = AccountComponentMetadata::new(NetworkFungibleFaucet::NAME) - .with_description("Network fungible faucet component for minting and burning tokens") - .with_supported_type(AccountType::FungibleFaucet) - .with_storage_schema(storage_schema); + let metadata = AccountComponentMetadata::new( + NetworkFungibleFaucet::NAME, + [AccountType::FungibleFaucet], + ) + .with_description("Network fungible faucet component for minting and burning tokens") + .with_storage_schema(storage_schema); AccountComponent::new(network_fungible_faucet_library(), slots, metadata) .expect("network fungible faucet component should satisfy the requirements of a valid account component") diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index aba06ae431..3ddb633540 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -154,7 +154,7 @@ fn test_custom_account_default_note() { let account_code = CodeBuilder::default() .compile_component_code("test::account_custom", account_custom_code_source) .unwrap(); - let metadata = AccountComponentMetadata::new("test::account_custom").with_supports_all_types(); + let metadata = AccountComponentMetadata::new("test::account_custom", AccountType::all()); let account_component = AccountComponent::new(account_code, vec![], metadata).unwrap(); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); @@ -425,8 +425,7 @@ fn test_custom_account_custom_notes() { let account_code = CodeBuilder::default() .compile_component_code("test::account::component_1", account_custom_code_source) .unwrap(); - let metadata = - AccountComponentMetadata::new("test::account::component_1").with_supports_all_types(); + let metadata = AccountComponentMetadata::new("test::account::component_1", AccountType::all()); let account_component = AccountComponent::new(account_code, vec![], metadata).unwrap(); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); @@ -529,8 +528,7 @@ fn test_custom_account_multiple_components_custom_notes() { let custom_code = CodeBuilder::default() .compile_component_code("test::account::component_1", account_custom_code_source) .unwrap(); - let metadata = - AccountComponentMetadata::new("test::account::component_1").with_supports_all_types(); + let metadata = AccountComponentMetadata::new("test::account::component_1", AccountType::all()); let custom_component = AccountComponent::new(custom_code, vec![], metadata).unwrap(); let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 4d6629eada..4113264b80 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -79,6 +79,7 @@ use miden_protocol::account::{ AccountBuilder, AccountComponent, AccountStorage, + AccountType, StorageSlot, StorageSlotName, }; @@ -517,9 +518,9 @@ impl AccountSchemaCommitment { impl From for AccountComponent { fn from(schema_commitment: AccountSchemaCommitment) -> Self { - let metadata = AccountComponentMetadata::new("miden::metadata::schema_commitment") - .with_description("Component exposing the account storage schema commitment") - .with_supports_all_types(); + let metadata = + AccountComponentMetadata::new("miden::metadata::schema_commitment", AccountType::all()) + .with_description("Component exposing the account storage schema commitment"); AccountComponent::new( storage_schema_library(), diff --git a/crates/miden-standards/src/account/wallets/mod.rs b/crates/miden-standards/src/account/wallets/mod.rs index c617289b55..292ca19600 100644 --- a/crates/miden-standards/src/account/wallets/mod.rs +++ b/crates/miden-standards/src/account/wallets/mod.rs @@ -78,9 +78,8 @@ impl BasicWallet { impl From for AccountComponent { fn from(_: BasicWallet) -> Self { - let metadata = AccountComponentMetadata::new(BasicWallet::NAME) - .with_description("Basic wallet component for receiving and sending assets") - .with_supports_all_types(); + let metadata = AccountComponentMetadata::new(BasicWallet::NAME, AccountType::all()) + .with_description("Basic wallet component for receiving and sending assets"); AccountComponent::new(basic_wallet_library(), vec![], metadata).expect( "basic wallet component should satisfy the requirements of a valid account component", diff --git a/crates/miden-standards/src/testing/account_component/conditional_auth.rs b/crates/miden-standards/src/testing/account_component/conditional_auth.rs index 8fa50cdadd..64a5f4ce46 100644 --- a/crates/miden-standards/src/testing/account_component/conditional_auth.rs +++ b/crates/miden-standards/src/testing/account_component/conditional_auth.rs @@ -1,7 +1,7 @@ use alloc::string::String; use miden_protocol::account::component::AccountComponentMetadata; -use miden_protocol::account::{AccountComponent, AccountComponentCode}; +use miden_protocol::account::{AccountComponent, AccountComponentCode, AccountType}; use miden_protocol::utils::sync::LazyLock; use crate::code_builder::CodeBuilder; @@ -52,9 +52,9 @@ pub struct ConditionalAuthComponent; impl From for AccountComponent { fn from(_: ConditionalAuthComponent) -> Self { - let metadata = AccountComponentMetadata::new("miden::testing::conditional_auth") - .with_description("Testing auth component with conditional behavior") - .with_supports_all_types(); + let metadata = + AccountComponentMetadata::new("miden::testing::conditional_auth", AccountType::all()) + .with_description("Testing auth component with conditional behavior"); AccountComponent::new(CONDITIONAL_AUTH_LIBRARY.clone(), vec![], metadata) .expect("component should be valid") diff --git a/crates/miden-standards/src/testing/account_component/incr_nonce.rs b/crates/miden-standards/src/testing/account_component/incr_nonce.rs index eda4ad0c17..96dc055ba2 100644 --- a/crates/miden-standards/src/testing/account_component/incr_nonce.rs +++ b/crates/miden-standards/src/testing/account_component/incr_nonce.rs @@ -1,5 +1,5 @@ -use miden_protocol::account::AccountComponent; use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{AccountComponent, AccountType}; use miden_protocol::assembly::Library; use miden_protocol::utils::sync::LazyLock; @@ -29,9 +29,9 @@ pub struct IncrNonceAuthComponent; impl From for AccountComponent { fn from(_: IncrNonceAuthComponent) -> Self { - let metadata = AccountComponentMetadata::new("miden::testing::incr_nonce_auth") - .with_description("Testing auth component that always increments nonce") - .with_supports_all_types(); + let metadata = + AccountComponentMetadata::new("miden::testing::incr_nonce_auth", AccountType::all()) + .with_description("Testing auth component that always increments nonce"); AccountComponent::new(INCR_NONCE_AUTH_LIBRARY.clone(), vec![], metadata) .expect("component should be valid") diff --git a/crates/miden-standards/src/testing/account_component/mock_account_component.rs b/crates/miden-standards/src/testing/account_component/mock_account_component.rs index 72e024a48a..e3e089e2cb 100644 --- a/crates/miden-standards/src/testing/account_component/mock_account_component.rs +++ b/crates/miden-standards/src/testing/account_component/mock_account_component.rs @@ -1,7 +1,13 @@ use alloc::vec::Vec; use miden_protocol::account::component::AccountComponentMetadata; -use miden_protocol::account::{AccountCode, AccountComponent, AccountStorage, StorageSlot}; +use miden_protocol::account::{ + AccountCode, + AccountComponent, + AccountStorage, + AccountType, + StorageSlot, +}; use crate::testing::mock_account_code::MockAccountCodeExt; @@ -55,9 +61,9 @@ impl MockAccountComponent { impl From for AccountComponent { fn from(mock_component: MockAccountComponent) -> Self { - let metadata = AccountComponentMetadata::new("miden::testing::mock_account") - .with_description("Mock account component for testing") - .with_supports_all_types(); + let metadata = + AccountComponentMetadata::new("miden::testing::mock_account", AccountType::all()) + .with_description("Mock account component for testing"); AccountComponent::new( AccountCode::mock_account_library(), diff --git a/crates/miden-standards/src/testing/account_component/mock_faucet_component.rs b/crates/miden-standards/src/testing/account_component/mock_faucet_component.rs index 3e82734817..23cffa2ec3 100644 --- a/crates/miden-standards/src/testing/account_component/mock_faucet_component.rs +++ b/crates/miden-standards/src/testing/account_component/mock_faucet_component.rs @@ -19,10 +19,11 @@ pub struct MockFaucetComponent; impl From for AccountComponent { fn from(_: MockFaucetComponent) -> Self { - let metadata = AccountComponentMetadata::new("miden::testing::mock_faucet") - .with_description("Mock faucet component for testing") - .with_supported_type(AccountType::FungibleFaucet) - .with_supported_type(AccountType::NonFungibleFaucet); + let metadata = AccountComponentMetadata::new( + "miden::testing::mock_faucet", + [AccountType::FungibleFaucet, AccountType::NonFungibleFaucet], + ) + .with_description("Mock faucet component for testing"); AccountComponent::new(AccountCode::mock_faucet_library(), vec![], metadata).expect( "mock faucet component should satisfy the requirements of a valid account component", 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 cdfe16cf14..211a8731eb 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -622,8 +622,10 @@ fn setup_non_faucet_account() -> anyhow::Result { "pub use ::miden::protocol::faucet::mint pub use ::miden::protocol::faucet::burn", )?; - let metadata = AccountComponentMetadata::new("test::non_faucet_component") - .with_supported_type(AccountType::RegularAccountUpdatableCode); + let metadata = AccountComponentMetadata::new( + "test::non_faucet_component", + [AccountType::RegularAccountUpdatableCode], + ); let faucet_component = AccountComponent::new(faucet_code, vec![], metadata)?; Ok(AccountBuilder::new([4; 32]) .account_type(AccountType::RegularAccountUpdatableCode) From af62bc4c94fdb25bc27f02447626088e652bbada Mon Sep 17 00:00:00 2001 From: Forostovec Date: Fri, 6 Mar 2026 08:44:26 +0200 Subject: [PATCH 13/56] feat: enforce maximum serialized size for output notes (#2205) --- CHANGELOG.md | 1 + .../src/batch/input_output_note_tracker.rs | 16 +- .../src/batch/proposed_batch.rs | 11 +- .../miden-protocol/src/batch/proven_batch.rs | 15 +- crates/miden-protocol/src/block/block_body.rs | 8 +- crates/miden-protocol/src/block/mod.rs | 2 +- .../src/block/proposed_block.rs | 6 +- crates/miden-protocol/src/constants.rs | 3 + crates/miden-protocol/src/errors/mod.rs | 20 + crates/miden-protocol/src/note/assets.rs | 11 +- crates/miden-protocol/src/note/attachment.rs | 25 + crates/miden-protocol/src/note/details.rs | 4 + crates/miden-protocol/src/note/header.rs | 4 + crates/miden-protocol/src/note/metadata.rs | 7 + crates/miden-protocol/src/note/mod.rs | 22 +- crates/miden-protocol/src/note/note_id.rs | 4 + crates/miden-protocol/src/note/note_tag.rs | 4 + crates/miden-protocol/src/note/note_type.rs | 4 + crates/miden-protocol/src/note/nullifier.rs | 6 +- crates/miden-protocol/src/note/partial.rs | 4 + crates/miden-protocol/src/note/recipient.rs | 9 + crates/miden-protocol/src/note/script.rs | 24 +- crates/miden-protocol/src/note/storage.rs | 5 + crates/miden-protocol/src/transaction/mod.rs | 10 +- .../miden-protocol/src/transaction/outputs.rs | 509 ++++++++++++++++-- .../src/transaction/proven_tx.rs | 18 +- .../src/transaction/tx_header.rs | 5 +- .../src/kernel_tests/batch/proposed_batch.rs | 38 +- .../kernel_tests/batch/proven_tx_builder.rs | 6 +- crates/miden-testing/src/mock_chain/chain.rs | 25 +- .../src/mock_chain/chain_builder.rs | 18 +- crates/miden-testing/tests/auth/multisig.rs | 14 +- crates/miden-testing/tests/scripts/faucet.rs | 2 +- crates/miden-tx/src/errors/mod.rs | 3 + crates/miden-tx/src/host/note_builder.rs | 4 +- crates/miden-tx/src/prover/mod.rs | 8 +- 36 files changed, 754 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81937eb05f..acce25aec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Introduce NOTE_MAX_SIZE (32 KiB) and enforce it on individual output notes ([#2205](https://github.com/0xMiden/miden-base/pull/2205)) - Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). - Added metadata extension (`Info`) with name and content URI slots, and MASM procedures `get_token_metadata`, `get_max_supply`, `get_decimals`, and `get_token_symbol` for the token metadata standard. Aligned fungible faucet token metadata with the metadata standard: faucet now uses the canonical slot `miden::standards::metadata::token_metadata` so MASM metadata getters work with faucet storage.([#2439](https://github.com/0xMiden/miden-base/pull/2439)) diff --git a/crates/miden-protocol/src/batch/input_output_note_tracker.rs b/crates/miden-protocol/src/batch/input_output_note_tracker.rs index 296cf021e6..c59316775b 100644 --- a/crates/miden-protocol/src/batch/input_output_note_tracker.rs +++ b/crates/miden-protocol/src/batch/input_output_note_tracker.rs @@ -9,8 +9,8 @@ use crate::errors::{ProposedBatchError, ProposedBlockError}; use crate::note::{NoteHeader, NoteId, NoteInclusionProof, Nullifier}; use crate::transaction::{ InputNoteCommitment, - OutputNote, PartialBlockchain, + ProvenOutputNote, ProvenTransaction, TransactionId, }; @@ -18,8 +18,8 @@ use crate::transaction::{ type BatchInputNotes = Vec; type BlockInputNotes = Vec; type ErasedNotes = Vec; -type BlockOutputNotes = BTreeMap; -type BatchOutputNotes = Vec; +type BlockOutputNotes = BTreeMap; +type BatchOutputNotes = Vec; // INPUT OUTPUT NOTE TRACKER // ================================================================================================ @@ -49,7 +49,7 @@ pub(crate) struct InputOutputNoteTracker { /// An index from [`NoteId`]s to the transaction that creates the note and the note itself. /// The transaction ID is tracked to produce better errors when a duplicate note is /// encountered. - output_notes: BTreeMap, + output_notes: BTreeMap, } impl InputOutputNoteTracker { @@ -136,7 +136,7 @@ impl InputOutputNoteTracker { /// authenticating any unauthenticated notes for which proofs are provided. fn from_iter( input_notes_iter: impl Iterator, - output_notes_iter: impl Iterator, + output_notes_iter: impl Iterator, unauthenticated_note_proofs: &BTreeMap, partial_blockchain: &PartialBlockchain, reference_block: &BlockHeader, @@ -199,7 +199,7 @@ impl InputOutputNoteTracker { ( Vec, ErasedNotes, - BTreeMap, + BTreeMap, ), InputOutputNoteTrackerError, > { @@ -242,7 +242,7 @@ impl InputOutputNoteTracker { /// but their hashes differ (i.e. their metadata is different). fn remove_output_note( input_note_header: &NoteHeader, - output_notes: &mut BTreeMap, + output_notes: &mut BTreeMap, ) -> Result> { let id = input_note_header.id(); if let Some((_, output_note)) = output_notes.remove(&id) { @@ -250,7 +250,7 @@ impl InputOutputNoteTracker { // This could happen if the metadata of the notes is different, which we consider an // error. let input_commitment = input_note_header.commitment(); - let output_commitment = output_note.commitment(); + let output_commitment = output_note.to_commitment(); if output_commitment != input_commitment { return Err(InputOutputNoteTrackerError::NoteCommitmentMismatch { id, diff --git a/crates/miden-protocol/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs index 96403a3f91..394f723c57 100644 --- a/crates/miden-protocol/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -12,8 +12,8 @@ use crate::transaction::{ InputNoteCommitment, InputNotes, OrderedTransactionHeaders, - OutputNote, PartialBlockchain, + ProvenOutputNote, ProvenTransaction, TransactionHeader, }; @@ -58,8 +58,9 @@ pub struct ProposedBatch { /// [`InputNoteCommitment::nullifier`]. input_notes: InputNotes, /// The output notes of this batch. This consists of all notes created by transactions in the - /// batch that are not consumed within the same batch. These are sorted by [`OutputNote::id`]. - output_notes: Vec, + /// batch that are not consumed within the same batch. These are sorted by + /// [`ProvenOutputNote::id`]. + output_notes: Vec, } impl ProposedBatch { @@ -354,7 +355,7 @@ impl ProposedBatch { /// /// This is the aggregation of all output notes by the transactions in the batch, except the /// ones that were consumed within the batch itself. - pub fn output_notes(&self) -> &[OutputNote] { + pub fn output_notes(&self) -> &[ProvenOutputNote] { &self.output_notes } @@ -370,7 +371,7 @@ impl ProposedBatch { BatchId, BTreeMap, InputNotes, - Vec, + Vec, BlockNumber, ) { ( diff --git a/crates/miden-protocol/src/batch/proven_batch.rs b/crates/miden-protocol/src/batch/proven_batch.rs index eb8aae5495..8b4cfdebc6 100644 --- a/crates/miden-protocol/src/batch/proven_batch.rs +++ b/crates/miden-protocol/src/batch/proven_batch.rs @@ -7,7 +7,12 @@ use crate::batch::{BatchAccountUpdate, BatchId}; use crate::block::BlockNumber; use crate::errors::ProvenBatchError; use crate::note::Nullifier; -use crate::transaction::{InputNoteCommitment, InputNotes, OrderedTransactionHeaders, OutputNote}; +use crate::transaction::{ + InputNoteCommitment, + InputNotes, + OrderedTransactionHeaders, + ProvenOutputNote, +}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -27,7 +32,7 @@ pub struct ProvenBatch { reference_block_num: BlockNumber, account_updates: BTreeMap, input_notes: InputNotes, - output_notes: Vec, + output_notes: Vec, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, } @@ -48,7 +53,7 @@ impl ProvenBatch { reference_block_num: BlockNumber, account_updates: BTreeMap, input_notes: InputNotes, - output_notes: Vec, + output_notes: Vec, batch_expiration_block_num: BlockNumber, transactions: OrderedTransactionHeaders, ) -> Result { @@ -132,7 +137,7 @@ impl ProvenBatch { /// /// This is the aggregation of all output notes by the transactions in the batch, except the /// ones that were consumed within the batch itself. - pub fn output_notes(&self) -> &[OutputNote] { + pub fn output_notes(&self) -> &[ProvenOutputNote] { &self.output_notes } @@ -173,7 +178,7 @@ impl Deserializable for ProvenBatch { let reference_block_num = BlockNumber::read_from(source)?; let account_updates = BTreeMap::read_from(source)?; let input_notes = InputNotes::::read_from(source)?; - let output_notes = Vec::::read_from(source)?; + let output_notes = Vec::::read_from(source)?; let batch_expiration_block_num = BlockNumber::read_from(source)?; let transactions = OrderedTransactionHeaders::read_from(source)?; diff --git a/crates/miden-protocol/src/block/block_body.rs b/crates/miden-protocol/src/block/block_body.rs index 4b10460edd..3b6d7ebd55 100644 --- a/crates/miden-protocol/src/block/block_body.rs +++ b/crates/miden-protocol/src/block/block_body.rs @@ -10,7 +10,7 @@ use crate::block::{ ProposedBlock, }; use crate::note::Nullifier; -use crate::transaction::{OrderedTransactionHeaders, OutputNote}; +use crate::transaction::{OrderedTransactionHeaders, ProvenOutputNote}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -91,11 +91,11 @@ impl BlockBody { self.transactions.commitment() } - /// Returns an iterator over all [`OutputNote`]s created in this block. + /// Returns an iterator over all [`ProvenOutputNote`]s created in this block. /// /// Each note is accompanied by a corresponding index specifying where the note is located /// in the block's [`BlockNoteTree`]. - pub fn output_notes(&self) -> impl Iterator { + pub fn output_notes(&self) -> impl Iterator { self.output_note_batches.iter().enumerate().flat_map(|(batch_idx, notes)| { notes.iter().map(move |(note_idx_in_batch, note)| { ( @@ -110,7 +110,7 @@ impl BlockBody { }) } - /// Computes the [`BlockNoteTree`] containing all [`OutputNote`]s created in this block. + /// Computes the [`BlockNoteTree`] containing all [`ProvenOutputNote`]s created in this block. pub fn compute_block_note_tree(&self) -> BlockNoteTree { let entries = self .output_notes() diff --git a/crates/miden-protocol/src/block/mod.rs b/crates/miden-protocol/src/block/mod.rs index cb2df1a489..0d1f9aff19 100644 --- a/crates/miden-protocol/src/block/mod.rs +++ b/crates/miden-protocol/src/block/mod.rs @@ -43,4 +43,4 @@ pub use note_tree::{BlockNoteIndex, BlockNoteTree}; /// output notes of a batch. This means the indices here may not be contiguous, i.e. any missing /// index belongs to an erased note. To correctly build the [`BlockNoteTree`] of a block, this index /// is required. -pub type OutputNoteBatch = alloc::vec::Vec<(usize, crate::transaction::OutputNote)>; +pub type OutputNoteBatch = alloc::vec::Vec<(usize, crate::transaction::ProvenOutputNote)>; diff --git a/crates/miden-protocol/src/block/proposed_block.rs b/crates/miden-protocol/src/block/proposed_block.rs index c828278f60..a5db0f3db5 100644 --- a/crates/miden-protocol/src/block/proposed_block.rs +++ b/crates/miden-protocol/src/block/proposed_block.rs @@ -27,8 +27,8 @@ use crate::errors::ProposedBlockError; use crate::note::{NoteId, Nullifier}; use crate::transaction::{ InputNoteCommitment, - OutputNote, PartialBlockchain, + ProvenOutputNote, TransactionHeader, TransactionKernel, }; @@ -729,7 +729,7 @@ fn check_batch_reference_blocks( /// Returns the set of [`OutputNoteBatch`]es that each batch creates. fn compute_block_output_notes( batches: &[ProvenBatch], - mut block_output_notes: BTreeMap, + mut block_output_notes: BTreeMap, ) -> Vec { let mut block_output_note_batches = Vec::with_capacity(batches.len()); @@ -751,7 +751,7 @@ fn compute_block_output_notes( /// The output note set is returned. fn compute_batch_output_notes( batch: &ProvenBatch, - block_output_notes: &mut BTreeMap, + block_output_notes: &mut BTreeMap, ) -> OutputNoteBatch { // The len of the batch output notes is an upper bound of how many notes the batch could've // produced so we reserve that much space to avoid reallocation. diff --git a/crates/miden-protocol/src/constants.rs b/crates/miden-protocol/src/constants.rs index 964025064e..14950b8fd1 100644 --- a/crates/miden-protocol/src/constants.rs +++ b/crates/miden-protocol/src/constants.rs @@ -6,6 +6,9 @@ pub const ACCOUNT_TREE_DEPTH: u8 = 64; /// The maximum allowed size of an account update is 256 KiB. pub const ACCOUNT_UPDATE_MAX_SIZE: u32 = 2u32.pow(18); +/// The maximum allowed size of a serialized note in bytes (32 KiB). +pub const NOTE_MAX_SIZE: u32 = 2u32.pow(15); + /// The maximum number of assets that can be stored in a single note. pub const MAX_ASSETS_PER_NOTE: usize = 255; diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index fccb7473a7..2481ba51c8 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -42,6 +42,7 @@ use crate::{ MAX_INPUT_NOTES_PER_TX, MAX_NOTE_STORAGE_ITEMS, MAX_OUTPUT_NOTES_PER_TX, + NOTE_MAX_SIZE, }; #[cfg(any(feature = "testing", test))] @@ -760,6 +761,25 @@ pub enum TransactionOutputError { TooManyOutputNotes(usize), #[error("failed to process account update commitment: {0}")] AccountUpdateCommitment(Box), + #[error( + "output note with id {note_id} has size {note_size} bytes which exceeds maximum note size of {NOTE_MAX_SIZE}" + )] + OutputNoteSizeLimitExceeded { note_id: NoteId, note_size: usize }, +} + +// PUBLIC OUTPUT NOTE ERROR +// ================================================================================================ + +/// Errors that can occur when creating a +/// [`PublicOutputNote`](crate::transaction::PublicOutputNote). +#[derive(Debug, Error)] +pub enum PublicOutputNoteError { + #[error("note with id {0} is private but PublicOutputNote requires a public note")] + NoteIsPrivate(NoteId), + #[error( + "note with id {note_id} has size {note_size} bytes which exceeds maximum note size of {NOTE_MAX_SIZE}" + )] + NoteSizeLimitExceeded { note_id: NoteId, note_size: usize }, } // TRANSACTION EVENT PARSING ERROR diff --git a/crates/miden-protocol/src/note/assets.rs b/crates/miden-protocol/src/note/assets.rs index 91fc6a7d34..d34896ad58 100644 --- a/crates/miden-protocol/src/note/assets.rs +++ b/crates/miden-protocol/src/note/assets.rs @@ -18,7 +18,7 @@ use crate::{Felt, Hasher, MAX_ASSETS_PER_NOTE, WORD_SIZE, Word}; /// An asset container for a note. /// -/// A note can contain between 0 and 256 assets. No duplicates are allowed, but the order of assets +/// A note can contain between 0 and 255 assets. No duplicates are allowed, but the order of assets /// is unspecified. /// /// All the assets in a note can be reduced to a single commitment which is computed by @@ -197,6 +197,15 @@ impl Serializable for NoteAssets { target.write_u8(self.assets.len().try_into().expect("Asset number must fit into `u8`")); target.write_many(&self.assets); } + + fn get_size_hint(&self) -> usize { + // Size of the serialized asset count prefix. + let u8_size = 0u8.get_size_hint(); + + let assets_size: usize = self.assets.iter().map(|asset| asset.get_size_hint()).sum(); + + u8_size + assets_size + } } impl Deserializable for NoteAssets { diff --git a/crates/miden-protocol/src/note/attachment.rs b/crates/miden-protocol/src/note/attachment.rs index ddc752671a..fa8e567341 100644 --- a/crates/miden-protocol/src/note/attachment.rs +++ b/crates/miden-protocol/src/note/attachment.rs @@ -116,6 +116,10 @@ impl Serializable for NoteAttachment { self.attachment_scheme().write_into(target); self.content().write_into(target); } + + fn get_size_hint(&self) -> usize { + self.attachment_scheme().get_size_hint() + self.content().get_size_hint() + } } impl Deserializable for NoteAttachment { @@ -217,6 +221,19 @@ impl Serializable for NoteAttachmentContent { }, } } + + fn get_size_hint(&self) -> usize { + let kind_size = self.attachment_kind().get_size_hint(); + match self { + NoteAttachmentContent::None => kind_size, + NoteAttachmentContent::Word(word) => kind_size + word.get_size_hint(), + NoteAttachmentContent::Array(attachment_commitment) => { + kind_size + + attachment_commitment.num_elements().get_size_hint() + + attachment_commitment.elements.len() * crate::ZERO.get_size_hint() + }, + } + } } impl Deserializable for NoteAttachmentContent { @@ -381,6 +398,10 @@ impl Serializable for NoteAttachmentScheme { fn write_into(&self, target: &mut W) { self.as_u32().write_into(target); } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } } impl Deserializable for NoteAttachmentScheme { @@ -471,6 +492,10 @@ impl Serializable for NoteAttachmentKind { fn write_into(&self, target: &mut W) { self.as_u8().write_into(target); } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } } impl Deserializable for NoteAttachmentKind { diff --git a/crates/miden-protocol/src/note/details.rs b/crates/miden-protocol/src/note/details.rs index f3be4b7342..810bece11b 100644 --- a/crates/miden-protocol/src/note/details.rs +++ b/crates/miden-protocol/src/note/details.rs @@ -96,6 +96,10 @@ impl Serializable for NoteDetails { assets.write_into(target); recipient.write_into(target); } + + fn get_size_hint(&self) -> usize { + self.assets.get_size_hint() + self.recipient.get_size_hint() + } } impl Deserializable for NoteDetails { diff --git a/crates/miden-protocol/src/note/header.rs b/crates/miden-protocol/src/note/header.rs index 04aac21de4..f0ca1c1265 100644 --- a/crates/miden-protocol/src/note/header.rs +++ b/crates/miden-protocol/src/note/header.rs @@ -77,6 +77,10 @@ impl Serializable for NoteHeader { self.note_id.write_into(target); self.note_metadata.write_into(target); } + + fn get_size_hint(&self) -> usize { + self.note_id.get_size_hint() + self.note_metadata.get_size_hint() + } } impl Deserializable for NoteHeader { diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 4cdbe2591a..14fd98dd16 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -193,6 +193,13 @@ impl Serializable for NoteMetadata { self.tag().write_into(target); self.attachment().write_into(target); } + + fn get_size_hint(&self) -> usize { + self.note_type().get_size_hint() + + self.sender().get_size_hint() + + self.tag().get_size_hint() + + self.attachment().get_size_hint() + } } impl Deserializable for NoteMetadata { diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index d97b9164ea..856795a7da 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -1,10 +1,15 @@ use miden_crypto::Word; -use miden_crypto::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use crate::account::AccountId; use crate::errors::NoteError; -use crate::utils::serde::DeserializationError; -use crate::{Felt, Hasher, WORD_SIZE, ZERO}; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; +use crate::{Felt, Hasher, ZERO}; mod assets; pub use assets::NoteAssets; @@ -163,6 +168,13 @@ impl Note { pub fn commitment(&self) -> Word { self.header.commitment() } + + /// Consumes self and returns the underlying parts of the [`Note`]. + pub fn into_parts(self) -> (NoteAssets, NoteMetadata, NoteRecipient) { + let (assets, recipient) = self.details.into_parts(); + let metadata = self.header.into_metadata(); + (assets, metadata, recipient) + } } // AS REF @@ -219,6 +231,10 @@ impl Serializable for Note { header.metadata().write_into(target); details.write_into(target); } + + fn get_size_hint(&self) -> usize { + self.header.metadata().get_size_hint() + self.details.get_size_hint() + } } impl Deserializable for Note { diff --git a/crates/miden-protocol/src/note/note_id.rs b/crates/miden-protocol/src/note/note_id.rs index 1d522757de..343285a81e 100644 --- a/crates/miden-protocol/src/note/note_id.rs +++ b/crates/miden-protocol/src/note/note_id.rs @@ -71,6 +71,10 @@ impl Serializable for NoteId { fn write_into(&self, target: &mut W) { target.write_bytes(&self.0.to_bytes()); } + + fn get_size_hint(&self) -> usize { + Word::SERIALIZED_SIZE + } } impl Deserializable for NoteId { diff --git a/crates/miden-protocol/src/note/note_tag.rs b/crates/miden-protocol/src/note/note_tag.rs index 1378ab2d77..2611f6988b 100644 --- a/crates/miden-protocol/src/note/note_tag.rs +++ b/crates/miden-protocol/src/note/note_tag.rs @@ -156,6 +156,10 @@ impl Serializable for NoteTag { fn write_into(&self, target: &mut W) { self.as_u32().write_into(target); } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } } impl Deserializable for NoteTag { diff --git a/crates/miden-protocol/src/note/note_type.rs b/crates/miden-protocol/src/note/note_type.rs index 6d16e9aac0..d72b953f86 100644 --- a/crates/miden-protocol/src/note/note_type.rs +++ b/crates/miden-protocol/src/note/note_type.rs @@ -108,6 +108,10 @@ impl Serializable for NoteType { fn write_into(&self, target: &mut W) { (*self as u8).write_into(target) } + + fn get_size_hint(&self) -> usize { + core::mem::size_of::() + } } impl Deserializable for NoteType { diff --git a/crates/miden-protocol/src/note/nullifier.rs b/crates/miden-protocol/src/note/nullifier.rs index 60c1b94dd5..2f728b4123 100644 --- a/crates/miden-protocol/src/note/nullifier.rs +++ b/crates/miden-protocol/src/note/nullifier.rs @@ -1,6 +1,7 @@ use alloc::string::String; use core::fmt::{Debug, Display, Formatter}; +use miden_core::WORD_SIZE; use miden_crypto::WordError; use miden_protocol_macros::WordWrapper; @@ -13,7 +14,6 @@ use super::{ Hasher, NoteDetails, Serializable, - WORD_SIZE, Word, ZERO, }; @@ -115,6 +115,10 @@ impl Serializable for Nullifier { fn write_into(&self, target: &mut W) { target.write_bytes(&self.0.to_bytes()); } + + fn get_size_hint(&self) -> usize { + Word::SERIALIZED_SIZE + } } impl Deserializable for Nullifier { diff --git a/crates/miden-protocol/src/note/partial.rs b/crates/miden-protocol/src/note/partial.rs index f7aea1bcc6..2cfd911c6a 100644 --- a/crates/miden-protocol/src/note/partial.rs +++ b/crates/miden-protocol/src/note/partial.rs @@ -75,6 +75,10 @@ impl Serializable for PartialNote { self.recipient_digest.write_into(target); self.assets.write_into(target) } + + fn get_size_hint(&self) -> usize { + self.metadata().get_size_hint() + Word::SERIALIZED_SIZE + self.assets.get_size_hint() + } } impl Deserializable for PartialNote { diff --git a/crates/miden-protocol/src/note/recipient.rs b/crates/miden-protocol/src/note/recipient.rs index 9948cfeb39..cdb470c53a 100644 --- a/crates/miden-protocol/src/note/recipient.rs +++ b/crates/miden-protocol/src/note/recipient.rs @@ -60,6 +60,11 @@ impl NoteRecipient { pub fn digest(&self) -> Word { self.digest } + + /// Consumes self and returns the underlying parts of the [`NoteRecipient`]. + pub fn into_parts(self) -> (Word, NoteScript, NoteStorage) { + (self.serial_num, self.script, self.storage) + } } fn compute_recipient_digest(serial_num: Word, script: &NoteScript, storage: &NoteStorage) -> Word { @@ -87,6 +92,10 @@ impl Serializable for NoteRecipient { storage.write_into(target); serial_num.write_into(target); } + + fn get_size_hint(&self) -> usize { + self.script.get_size_hint() + self.storage.get_size_hint() + Word::SERIALIZED_SIZE + } } impl Deserializable for NoteRecipient { diff --git a/crates/miden-protocol/src/note/script.rs b/crates/miden-protocol/src/note/script.rs index eda608617e..5e0266b08e 100644 --- a/crates/miden-protocol/src/note/script.rs +++ b/crates/miden-protocol/src/note/script.rs @@ -157,6 +157,16 @@ impl NoteScript { self.entrypoint } + /// Clears all debug info from this script's [`MastForest`]: decorators, error codes, and + /// procedure names. + /// + /// See [`MastForest::clear_debug_info`] for more details. + pub fn clear_debug_info(&mut self) { + let mut mast = self.mast.clone(); + Arc::make_mut(&mut mast).clear_debug_info(); + self.mast = mast; + } + /// Returns a new [NoteScript] with the provided advice map entries merged into the /// underlying [MastForest]. /// @@ -272,6 +282,16 @@ impl Serializable for NoteScript { self.mast.write_into(target); target.write_u32(u32::from(self.entrypoint)); } + + fn get_size_hint(&self) -> usize { + // TODO: this is a temporary workaround. Replace mast.to_bytes().len() with + // MastForest::get_size_hint() (or a similar size-hint API) once it becomes + // available. + let mast_size = self.mast.to_bytes().len(); + let u32_size = 0u32.get_size_hint(); + + mast_size + u32_size + } } impl Deserializable for NoteScript { @@ -330,8 +350,8 @@ mod tests { #[test] fn test_note_script_to_from_felt() { let assembler = Assembler::default(); - let tx_script_src = DEFAULT_NOTE_CODE; - let program = assembler.assemble_program(tx_script_src).unwrap(); + let script_src = DEFAULT_NOTE_CODE; + let program = assembler.assemble_program(script_src).unwrap(); let note_script = NoteScript::new(program); let encoded: Vec = (¬e_script).into(); diff --git a/crates/miden-protocol/src/note/storage.rs b/crates/miden-protocol/src/note/storage.rs index 33e939402c..0b8b73a976 100644 --- a/crates/miden-protocol/src/note/storage.rs +++ b/crates/miden-protocol/src/note/storage.rs @@ -121,6 +121,11 @@ impl Serializable for NoteStorage { target.write_u16(items.len().try_into().expect("storage items len is not a u16 value")); target.write_many(items); } + + fn get_size_hint(&self) -> usize { + // 2 bytes for u16 length + 8 bytes per Felt + 2 + self.items.len() * 8 + } } impl Deserializable for NoteStorage { diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index feef2fc878..c35d948c56 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -19,7 +19,15 @@ pub use executed_tx::{ExecutedTransaction, TransactionMeasurements}; pub use inputs::{AccountInputs, InputNote, InputNotes, ToInputNoteCommitments, TransactionInputs}; pub use kernel::{TransactionAdviceInputs, TransactionEventId, TransactionKernel, memory}; pub use ordered_transactions::OrderedTransactionHeaders; -pub use outputs::{OutputNote, OutputNotes, TransactionOutputs}; +pub use outputs::{ + OutputNote, + OutputNotes, + ProvenOutputNote, + ProvenOutputNotes, + PublicOutputNote, + RawOutputNotes, + TransactionOutputs, +}; pub use partial_blockchain::PartialBlockchain; pub use proven_tx::{ InputNoteCommitment, diff --git a/crates/miden-protocol/src/transaction/outputs.rs b/crates/miden-protocol/src/transaction/outputs.rs index 87b9db901f..178c103cb2 100644 --- a/crates/miden-protocol/src/transaction/outputs.rs +++ b/crates/miden-protocol/src/transaction/outputs.rs @@ -6,7 +6,8 @@ use core::fmt::Debug; use crate::account::AccountHeader; use crate::asset::FungibleAsset; use crate::block::BlockNumber; -use crate::errors::TransactionOutputError; +use crate::constants::NOTE_MAX_SIZE; +use crate::errors::{PublicOutputNoteError, TransactionOutputError}; use crate::note::{ Note, NoteAssets, @@ -104,35 +105,44 @@ impl Deserializable for TransactionOutputs { /// Contains a list of output notes of a transaction. The list can be empty if the transaction does /// not produce any notes. +/// +/// This struct is generic over the note type `N`, allowing it to be used with both +/// [`OutputNote`] (in [`ExecutedTransaction`](super::ExecutedTransaction)) and +/// [`ProvenOutputNote`] (in [`ProvenTransaction`](super::ProvenTransaction)). #[derive(Debug, Clone, PartialEq, Eq)] -pub struct OutputNotes { - notes: Vec, +pub struct RawOutputNotes { + notes: Vec, commitment: Word, } -impl OutputNotes { +impl RawOutputNotes +where + for<'a> &'a NoteHeader: From<&'a N>, + for<'a> NoteId: From<&'a N>, +{ // CONSTRUCTOR // -------------------------------------------------------------------------------------------- - /// Returns new [OutputNotes] instantiated from the provide vector of notes. + /// Returns new [RawOutputNotes] instantiated from the provided vector of notes. /// /// # Errors /// Returns an error if: /// - The total number of notes is greater than [`MAX_OUTPUT_NOTES_PER_TX`]. /// - The vector of notes contains duplicates. - pub fn new(notes: Vec) -> Result { + pub fn new(notes: Vec) -> Result { if notes.len() > MAX_OUTPUT_NOTES_PER_TX { return Err(TransactionOutputError::TooManyOutputNotes(notes.len())); } let mut seen_notes = BTreeSet::new(); for note in notes.iter() { - if !seen_notes.insert(note.id()) { - return Err(TransactionOutputError::DuplicateOutputNote(note.id())); + let note_id = NoteId::from(note); + if !seen_notes.insert(note_id) { + return Err(TransactionOutputError::DuplicateOutputNote(note_id)); } } - let commitment = Self::compute_commitment(notes.iter().map(OutputNote::header)); + let commitment = Self::compute_commitment(notes.iter().map(<&NoteHeader>::from)); Ok(Self { notes, commitment }) } @@ -147,26 +157,27 @@ impl OutputNotes { pub fn commitment(&self) -> Word { self.commitment } + /// Returns total number of output notes. pub fn num_notes(&self) -> usize { self.notes.len() } - /// Returns true if this [OutputNotes] does not contain any notes. + /// Returns true if this [RawOutputNotes] does not contain any notes. pub fn is_empty(&self) -> bool { self.notes.is_empty() } /// Returns a reference to the note located at the specified index. - pub fn get_note(&self, idx: usize) -> &OutputNote { + pub fn get_note(&self, idx: usize) -> &N { &self.notes[idx] } // ITERATORS // -------------------------------------------------------------------------------------------- - /// Returns an iterator over notes in this [OutputNotes]. - pub fn iter(&self) -> impl Iterator { + /// Returns an iterator over notes in this [RawOutputNotes]. + pub fn iter(&self) -> impl Iterator { self.notes.iter() } @@ -196,6 +207,18 @@ impl OutputNotes { } } +/// Output notes produced during transaction execution (before proving). +/// +/// Contains [`OutputNote`] instances which represent notes as they exist immediately after +/// transaction execution. +pub type OutputNotes = RawOutputNotes; + +/// Output notes in a proven transaction. +/// +/// Contains [`ProvenOutputNote`] instances which have been processed for inclusion in +/// proven transactions, with size limits enforced on public notes. +pub type ProvenOutputNotes = RawOutputNotes; + // SERIALIZATION // ------------------------------------------------------------------------------------------------ @@ -218,14 +241,43 @@ impl Deserializable for OutputNotes { } } +impl Serializable for ProvenOutputNotes { + fn write_into(&self, target: &mut W) { + // assert is OK here because we enforce max number of notes in the constructor + assert!(self.notes.len() <= u16::MAX.into()); + target.write_u16(self.notes.len() as u16); + target.write_many(&self.notes); + } +} + +impl Deserializable for ProvenOutputNotes { + fn read_from(source: &mut R) -> Result { + let num_notes = source.read_u16()?; + let notes = source + .read_many_iter::(num_notes.into())? + .collect::, _>>()?; + Self::new(notes).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + // OUTPUT NOTE // ================================================================================================ -const FULL: u8 = 0; -const PARTIAL: u8 = 1; -const HEADER: u8 = 2; - -/// The types of note outputs supported by the transaction kernel. +const OUTPUT_FULL: u8 = 0; +const OUTPUT_PARTIAL: u8 = 1; +const OUTPUT_HEADER: u8 = 2; + +/// The types of note outputs produced during transaction execution (before proving). +/// +/// This enum represents notes as they exist immediately after transaction execution, +/// before they are processed for inclusion in a proven transaction. It includes: +/// - Full notes with all details (public or private) +/// - Partial notes (notes created with only recipient digest, not full recipient details) +/// - Note headers (minimal note information) +/// +/// During proving, these are converted to [`ProvenOutputNote`] via the +/// [`to_proven_output_note`](Self::to_proven_output_note) method, which enforces size limits +/// on public notes and converts private/partial notes to headers. #[derive(Debug, Clone, PartialEq, Eq)] pub enum OutputNote { Full(Note), @@ -254,8 +306,8 @@ impl OutputNote { } } - /// Returns the recipient of the processed [`Full`](OutputNote::Full) output note, [`None`] if - /// the note type is not [`Full`](OutputNote::Full). + /// Returns the recipient of the processed [`Full`](OutputNote::Full) output note, + /// [`None`] if the note type is not [`Full`](OutputNote::Full). /// /// See [crate::note::NoteRecipient] for more details. pub fn recipient(&self) -> Option<&NoteRecipient> { @@ -288,18 +340,25 @@ impl OutputNote { } } - /// Erase private note information. + /// Converts this output note to a proven output note. + /// + /// This method performs the following transformations: + /// - Full private notes are converted to note headers (only public info retained) + /// - Partial notes are converted to note headers + /// - Full public notes are wrapped in [`PublicOutputNote`], which enforces size limits /// - /// Specifically: - /// - Full private notes are converted into note headers. - /// - All partial notes are converted into note headers. - pub fn shrink(&self) -> Self { + /// # Errors + /// Returns an error if a public note exceeds the maximum allowed size ([`NOTE_MAX_SIZE`]). + pub fn to_proven_output_note(&self) -> Result { match self { OutputNote::Full(note) if note.metadata().is_private() => { - OutputNote::Header(note.header().clone()) + Ok(ProvenOutputNote::Header(note.header().clone())) + }, + OutputNote::Full(note) => { + Ok(ProvenOutputNote::Public(PublicOutputNote::new(note.clone())?)) }, - OutputNote::Partial(note) => OutputNote::Header(note.header().clone()), - _ => self.clone(), + OutputNote::Partial(note) => Ok(ProvenOutputNote::Header(note.header().clone())), + OutputNote::Header(header) => Ok(ProvenOutputNote::Header(header.clone())), } } @@ -320,39 +379,295 @@ impl OutputNote { } } +// CONVERSIONS FROM OUTPUT NOTE +// ================================================================================================ + +impl From<&OutputNote> for NoteId { + fn from(note: &OutputNote) -> Self { + note.id() + } +} + +impl<'note> From<&'note OutputNote> for &'note NoteHeader { + fn from(note: &'note OutputNote) -> Self { + note.header() + } +} + // SERIALIZATION -// ------------------------------------------------------------------------------------------------ +// ================================================================================================ impl Serializable for OutputNote { fn write_into(&self, target: &mut W) { match self { OutputNote::Full(note) => { - target.write(FULL); + target.write(OUTPUT_FULL); target.write(note); }, OutputNote::Partial(note) => { - target.write(PARTIAL); + target.write(OUTPUT_PARTIAL); target.write(note); }, OutputNote::Header(note) => { - target.write(HEADER); + target.write(OUTPUT_HEADER); target.write(note); }, } } + + fn get_size_hint(&self) -> usize { + // Serialized size of the enum tag. + let tag_size = 0u8.get_size_hint(); + + match self { + OutputNote::Full(note) => tag_size + note.get_size_hint(), + OutputNote::Partial(note) => tag_size + note.get_size_hint(), + OutputNote::Header(note) => tag_size + note.get_size_hint(), + } + } } impl Deserializable for OutputNote { fn read_from(source: &mut R) -> Result { match source.read_u8()? { - FULL => Ok(OutputNote::Full(Note::read_from(source)?)), - PARTIAL => Ok(OutputNote::Partial(PartialNote::read_from(source)?)), - HEADER => Ok(OutputNote::Header(NoteHeader::read_from(source)?)), - v => Err(DeserializationError::InvalidValue(format!("invalid note type: {v}"))), + OUTPUT_FULL => Ok(OutputNote::Full(Note::read_from(source)?)), + OUTPUT_PARTIAL => Ok(OutputNote::Partial(PartialNote::read_from(source)?)), + OUTPUT_HEADER => Ok(OutputNote::Header(NoteHeader::read_from(source)?)), + v => Err(DeserializationError::InvalidValue(format!("invalid output note type: {v}"))), + } + } +} + +// PROVEN OUTPUT NOTE +// ================================================================================================ + +const PROVEN_PUBLIC: u8 = 0; +const PROVEN_HEADER: u8 = 1; + +/// Output note types that can appear in a proven transaction. +/// +/// This enum represents the final form of output notes after proving. Unlike [`OutputNote`], +/// this enum: +/// - Does not include partial notes (they are converted to headers) +/// - Wraps public notes in [`PublicOutputNote`] which enforces size limits +/// - Contains only the minimal information needed for verification +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProvenOutputNote { + /// A public note with full details, size-validated. + Public(PublicOutputNote), + /// A note header (for private notes or notes without full details). + Header(NoteHeader), +} + +impl ProvenOutputNote { + /// Unique note identifier. + /// + /// This value is both an unique identifier and a commitment to the note. + pub fn id(&self) -> NoteId { + match self { + ProvenOutputNote::Public(note) => note.id(), + ProvenOutputNote::Header(header) => header.id(), + } + } + + /// Note's metadata. + pub fn metadata(&self) -> &NoteMetadata { + match self { + ProvenOutputNote::Public(note) => note.metadata(), + ProvenOutputNote::Header(header) => header.metadata(), + } + } + + /// The assets contained in the note, if available. + /// + /// Returns `Some` for public notes, `None` for header-only notes. + pub fn assets(&self) -> Option<&NoteAssets> { + match self { + ProvenOutputNote::Public(note) => Some(note.assets()), + ProvenOutputNote::Header(_) => None, + } + } + + /// Returns a commitment to the note and its metadata. + /// + /// > hash(NOTE_ID || NOTE_METADATA_COMMITMENT) + pub fn to_commitment(&self) -> Word { + compute_note_commitment(self.id(), self.metadata()) + } + + /// Returns the recipient of the public note, if this is a public note. + pub fn recipient(&self) -> Option<&NoteRecipient> { + match self { + ProvenOutputNote::Public(note) => Some(note.recipient()), + ProvenOutputNote::Header(_) => None, + } + } +} + +// CONVERSIONS +// ------------------------------------------------------------------------------------------------ + +impl<'note> From<&'note ProvenOutputNote> for &'note NoteHeader { + fn from(value: &'note ProvenOutputNote) -> Self { + match value { + ProvenOutputNote::Public(note) => note.header(), + ProvenOutputNote::Header(header) => header, + } + } +} + +impl From<&ProvenOutputNote> for NoteId { + fn from(value: &ProvenOutputNote) -> Self { + value.id() + } +} + +// SERIALIZATION +// ------------------------------------------------------------------------------------------------ + +impl Serializable for ProvenOutputNote { + fn write_into(&self, target: &mut W) { + match self { + ProvenOutputNote::Public(note) => { + target.write(PROVEN_PUBLIC); + target.write(note); + }, + ProvenOutputNote::Header(header) => { + target.write(PROVEN_HEADER); + target.write(header); + }, + } + } + + fn get_size_hint(&self) -> usize { + let tag_size = 0u8.get_size_hint(); + match self { + ProvenOutputNote::Public(note) => tag_size + note.get_size_hint(), + ProvenOutputNote::Header(header) => tag_size + header.get_size_hint(), + } + } +} + +impl Deserializable for ProvenOutputNote { + fn read_from(source: &mut R) -> Result { + match source.read_u8()? { + PROVEN_PUBLIC => Ok(ProvenOutputNote::Public(PublicOutputNote::read_from(source)?)), + PROVEN_HEADER => Ok(ProvenOutputNote::Header(NoteHeader::read_from(source)?)), + v => Err(DeserializationError::InvalidValue(format!( + "invalid proven output note type: {v}" + ))), } } } +// PUBLIC OUTPUT NOTE +// ================================================================================================ + +/// A public output note with enforced size limits. +/// +/// This struct wraps a [`Note`] and guarantees that: +/// - The note is public (not private) +/// - The serialized size does not exceed [`NOTE_MAX_SIZE`] +/// +/// This type is used in [`ProvenOutputNote::Public`] to ensure that all public notes +/// in proven transactions meet the protocol's size requirements. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicOutputNote { + note: Note, +} + +impl PublicOutputNote { + /// Creates a new [`PublicOutputNote`] from the given note. + /// + /// # Errors + /// Returns an error if: + /// - The note is private (use note headers for private notes) + /// - The serialized size exceeds [`NOTE_MAX_SIZE`] + pub fn new(note: Note) -> Result { + // Ensure the note is public + if note.metadata().is_private() { + return Err(PublicOutputNoteError::NoteIsPrivate(note.id())); + } + + // Strip decorators from the note script. + let previous_note_id = note.id(); + let (assets, metadata, recipient) = note.into_parts(); + let (serial_num, mut script, storage) = recipient.into_parts(); + + script.clear_debug_info(); + let recipient = NoteRecipient::new(serial_num, script, storage); + let note = Note::new(assets, metadata, recipient); + debug_assert_eq!(previous_note_id, note.id()); + + // Check the size limit after stripping decorators + let note_size = note.get_size_hint(); + if note_size > NOTE_MAX_SIZE as usize { + return Err(PublicOutputNoteError::NoteSizeLimitExceeded { + note_id: note.id(), + note_size, + }); + } + + Ok(Self { note }) + } + + /// Returns the unique identifier of this note. + pub fn id(&self) -> NoteId { + self.note.id() + } + + /// Returns the note's metadata. + pub fn metadata(&self) -> &NoteMetadata { + self.note.metadata() + } + + /// Returns the note's assets. + pub fn assets(&self) -> &NoteAssets { + self.note.assets() + } + + /// Returns the note's recipient. + pub fn recipient(&self) -> &NoteRecipient { + self.note.recipient() + } + + /// Returns the note's header. + pub fn header(&self) -> &NoteHeader { + self.note.header() + } + + /// Returns a reference to the underlying note. + pub fn note(&self) -> &Note { + &self.note + } + + /// Consumes this wrapper and returns the underlying note. + pub fn into_note(self) -> Note { + self.note + } +} + +// SERIALIZATION +// ------------------------------------------------------------------------------------------------ + +impl Serializable for PublicOutputNote { + fn write_into(&self, target: &mut W) { + self.note.write_into(target); + } + + fn get_size_hint(&self) -> usize { + self.note.get_size_hint() + } +} + +impl Deserializable for PublicOutputNote { + fn read_from(source: &mut R) -> Result { + let note = Note::read_from(source)?; + Self::new(note).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + // TESTS // ================================================================================================ @@ -360,11 +675,29 @@ impl Deserializable for OutputNote { mod output_notes_tests { use assert_matches::assert_matches; - use super::OutputNotes; - use crate::Word; + use super::{OutputNote, OutputNotes, PublicOutputNote, PublicOutputNoteError}; + use crate::account::AccountId; + use crate::assembly::Assembler; + use crate::asset::FungibleAsset; + use crate::constants::NOTE_MAX_SIZE; use crate::errors::TransactionOutputError; - use crate::note::Note; - use crate::transaction::OutputNote; + use crate::note::{ + Note, + NoteAssets, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteTag, + NoteType, + }; + use crate::testing::account_id::{ + ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_SENDER, + }; + use crate::utils::serde::Serializable; + use crate::{Felt, Word}; #[test] fn test_duplicate_output_notes() -> anyhow::Result<()> { @@ -380,4 +713,100 @@ mod output_notes_tests { Ok(()) } + + #[test] + fn output_note_size_hint_matches_serialized_length() -> anyhow::Result<()> { + let sender_id = ACCOUNT_ID_SENDER.try_into().unwrap(); + + // Build a note with at least two assets. + 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 asset_1 = FungibleAsset::new(faucet_id_1, 100)?.into(); + let asset_2 = FungibleAsset::new(faucet_id_2, 200)?.into(); + + let assets = NoteAssets::new(vec![asset_1, asset_2])?; + + // Build metadata similarly to how mock notes are constructed. + let metadata = NoteMetadata::new(sender_id, NoteType::Private) + .with_tag(NoteTag::with_account_target(sender_id)); + + // Build storage with at least two values. + let storage = NoteStorage::new(vec![Felt::new(1), Felt::new(2)])?; + + let serial_num = Word::empty(); + let script = NoteScript::mock(); + let recipient = NoteRecipient::new(serial_num, script, storage); + + let note = Note::new(assets, metadata, recipient); + let output_note = OutputNote::Full(note); + + let bytes = output_note.to_bytes(); + + assert_eq!(bytes.len(), output_note.get_size_hint()); + + Ok(()) + } + + #[test] + fn oversized_public_note_triggers_size_limit_error() -> anyhow::Result<()> { + // Construct a public note whose serialized size exceeds NOTE_MAX_SIZE by creating + // a very large note script so that the script's serialized MAST alone is larger + // than the configured limit. + + let sender_id = ACCOUNT_ID_SENDER.try_into().unwrap(); + + // Build a large MASM program with many `nop` instructions. + let mut src = alloc::string::String::from("begin\n"); + // The exact threshold is not critical as long as we clearly exceed NOTE_MAX_SIZE. + // After strip_decorators(), the size is reduced, so we need more nops. + for _ in 0..50000 { + src.push_str(" nop\n"); + } + src.push_str("end\n"); + + let assembler = Assembler::default(); + let program = assembler.assemble_program(&src).unwrap(); + let script = NoteScript::new(program); + + let serial_num = Word::empty(); + let storage = NoteStorage::new(alloc::vec::Vec::new())?; + + // Create a public note (NoteType::Public is required for PublicOutputNote) + let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap(); + let asset = FungibleAsset::new(faucet_id, 100)?.into(); + let assets = NoteAssets::new(vec![asset])?; + + let metadata = NoteMetadata::new(sender_id, NoteType::Public) + .with_tag(NoteTag::with_account_target(sender_id)); + + let recipient = NoteRecipient::new(serial_num, script, storage); + let oversized_note = Note::new(assets, metadata, recipient); + + // Sanity-check that our constructed note is indeed larger than the configured + // maximum. + let computed_note_size = oversized_note.get_size_hint(); + assert!(computed_note_size > NOTE_MAX_SIZE as usize); + + // Creating a PublicOutputNote should fail with size limit error + let result = PublicOutputNote::new(oversized_note.clone()); + + assert_matches!( + result, + Err(PublicOutputNoteError::NoteSizeLimitExceeded { note_id: _, note_size }) + if note_size > NOTE_MAX_SIZE as usize + ); + + // to_proven_output_note() should also fail + let output_note = OutputNote::Full(oversized_note); + let result = output_note.to_proven_output_note(); + + assert_matches!( + result, + Err(PublicOutputNoteError::NoteSizeLimitExceeded { note_id: _, note_size }) + if note_size > NOTE_MAX_SIZE as usize + ); + + Ok(()) + } } diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index 484844dd39..4f868d019b 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -13,8 +13,8 @@ use crate::transaction::{ AccountId, InputNotes, Nullifier, - OutputNote, - OutputNotes, + ProvenOutputNote, + ProvenOutputNotes, TransactionId, }; use crate::utils::serde::{ @@ -52,7 +52,7 @@ pub struct ProvenTransaction { /// Notes created by the transaction. For private notes, this will contain only note headers, /// while for public notes this will also contain full note details. - output_notes: OutputNotes, + output_notes: ProvenOutputNotes, /// [`BlockNumber`] of the transaction's reference block. ref_block_num: BlockNumber, @@ -92,7 +92,7 @@ impl ProvenTransaction { } /// Returns a reference to the notes produced by the transaction. - pub fn output_notes(&self) -> &OutputNotes { + pub fn output_notes(&self) -> &ProvenOutputNotes { &self.output_notes } @@ -205,7 +205,7 @@ impl Deserializable for ProvenTransaction { let account_update = TxAccountUpdate::read_from(source)?; let input_notes = >::read_from(source)?; - let output_notes = OutputNotes::read_from(source)?; + let output_notes = ProvenOutputNotes::read_from(source)?; let ref_block_num = BlockNumber::read_from(source)?; let ref_block_commitment = Word::read_from(source)?; @@ -262,8 +262,8 @@ pub struct ProvenTransactionBuilder { /// List of [InputNoteCommitment]s of all consumed notes by the transaction. input_notes: Vec, - /// List of [OutputNote]s of all notes created by the transaction. - output_notes: Vec, + /// List of [`ProvenOutputNote`]s of all notes created by the transaction. + output_notes: Vec, /// [`BlockNumber`] of the transaction's reference block. ref_block_num: BlockNumber, @@ -336,7 +336,7 @@ impl ProvenTransactionBuilder { /// Add notes produced by the transaction. pub fn add_output_notes(mut self, notes: T) -> Self where - T: IntoIterator, + T: IntoIterator, { self.output_notes.extend(notes); self @@ -371,7 +371,7 @@ impl ProvenTransactionBuilder { pub fn build(self) -> Result { let input_notes = InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?; - let output_notes = OutputNotes::new(self.output_notes) + let output_notes = ProvenOutputNotes::new(self.output_notes) .map_err(ProvenTransactionError::OutputNotesError)?; let id = TransactionId::new( self.initial_account_commitment, diff --git a/crates/miden-protocol/src/transaction/tx_header.rs b/crates/miden-protocol/src/transaction/tx_header.rs index a859d7356d..6f5f071067 100644 --- a/crates/miden-protocol/src/transaction/tx_header.rs +++ b/crates/miden-protocol/src/transaction/tx_header.rs @@ -8,7 +8,6 @@ use crate::transaction::{ ExecutedTransaction, InputNoteCommitment, InputNotes, - OutputNote, OutputNotes, ProvenTransaction, TransactionId, @@ -171,7 +170,7 @@ impl From<&ProvenTransaction> for TransactionHeader { tx.account_update().initial_state_commitment(), tx.account_update().final_state_commitment(), tx.input_notes().clone(), - tx.output_notes().iter().map(OutputNote::header).cloned().collect(), + tx.output_notes().iter().map(<&NoteHeader>::from).cloned().collect(), tx.fee(), ) } @@ -186,7 +185,7 @@ impl From<&ExecutedTransaction> for TransactionHeader { tx.initial_account().initial_commitment(), tx.final_account().to_commitment(), tx.input_notes().to_commitments(), - tx.output_notes().iter().map(OutputNote::header).cloned().collect(), + tx.output_notes().iter().map(|n| n.header().clone()).collect(), tx.fee(), ) } diff --git a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs index 6a29dc72a4..43433ecd07 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proposed_batch.rs @@ -11,7 +11,13 @@ use miden_protocol::crypto::merkle::MerkleError; use miden_protocol::errors::{BatchAccountUpdateError, ProposedBatchError}; use miden_protocol::note::{Note, NoteType}; use miden_protocol::testing::account_id::AccountIdBuilder; -use miden_protocol::transaction::{InputNote, InputNoteCommitment, OutputNote, PartialBlockchain}; +use miden_protocol::transaction::{ + InputNote, + InputNoteCommitment, + OutputNote, + PartialBlockchain, + ProvenOutputNote, +}; use miden_standards::testing::account_component::MockAccountComponent; use miden_standards::testing::note::NoteBuilder; use rand::rngs::SmallRng; @@ -30,8 +36,8 @@ pub fn mock_note(num: u8) -> Note { NoteBuilder::new(sender, SmallRng::from_seed([num; 32])).build().unwrap() } -pub fn mock_output_note(num: u8) -> OutputNote { - OutputNote::Full(mock_note(num)) +pub fn mock_output_note(num: u8) -> ProvenOutputNote { + OutputNote::Full(mock_note(num)).to_proven_output_note().unwrap() } struct TestSetup { @@ -91,7 +97,7 @@ fn note_created_and_consumed_in_same_batch() -> anyhow::Result<()> { let tx1 = MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment()) .ref_block_commitment(block1.commitment()) - .output_notes(vec![OutputNote::Full(note.clone())]) + .output_notes(vec![OutputNote::Full(note.clone()).to_proven_output_note().unwrap()]) .build()?; let tx2 = MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment()) @@ -303,14 +309,14 @@ async fn unauthenticated_note_converted_to_authenticated() -> anyhow::Result<()> block1 .body() .output_notes() - .any(|(_, note)| note.commitment() == note1.commitment()), + .any(|(_, note)| note.to_commitment() == note1.commitment()), "block 1 should contain note1" ); assert!( block1 .body() .output_notes() - .any(|(_, note)| note.commitment() == note2.commitment()), + .any(|(_, note)| note.to_commitment() == note2.commitment()), "block 1 should contain note2" ); @@ -427,7 +433,7 @@ fn authenticated_note_created_in_same_batch() -> anyhow::Result<()> { let tx1 = MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment()) .ref_block_commitment(block1.commitment()) - .output_notes(vec![OutputNote::Full(note0.clone())]) + .output_notes(vec![OutputNote::Full(note0.clone()).to_proven_output_note().unwrap()]) .build()?; let tx2 = MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment()) @@ -550,7 +556,11 @@ fn input_and_output_notes_commitment() -> anyhow::Result<()> { MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment()) .ref_block_commitment(block1.commitment()) .unauthenticated_notes(vec![note4.clone(), note6.clone()]) - .output_notes(vec![OutputNote::Full(note1.clone()), note2.clone(), note3.clone()]) + .output_notes(vec![ + OutputNote::Full(note1.clone()).to_proven_output_note().unwrap(), + note2.clone(), + note3.clone(), + ]) .build()?; let batch = ProposedBatch::new( @@ -563,8 +573,8 @@ fn input_and_output_notes_commitment() -> anyhow::Result<()> { // We expect note1 to be erased from the input/output notes as it is created and consumed // in the batch. let mut expected_output_notes = [note0, note2, note3]; - // We expect a vector sorted by NoteId (since InputOutputNoteTracker is set up that way). - expected_output_notes.sort_unstable_by_key(OutputNote::id); + // We expect a vector sorted by NoteId. + expected_output_notes.sort_unstable_by_key(ProvenOutputNote::id); assert_eq!(batch.output_notes().len(), 3); assert_eq!(batch.output_notes(), expected_output_notes); @@ -655,13 +665,13 @@ fn circular_note_dependency() -> anyhow::Result<()> { MockProvenTxBuilder::with_account(account1.id(), Word::empty(), account1.to_commitment()) .ref_block_commitment(block1.commitment()) .unauthenticated_notes(vec![note_x.clone()]) - .output_notes(vec![OutputNote::Full(note_y.clone())]) + .output_notes(vec![OutputNote::Full(note_y.clone()).to_proven_output_note().unwrap()]) .build()?; let tx2 = MockProvenTxBuilder::with_account(account2.id(), Word::empty(), account2.to_commitment()) .ref_block_commitment(block1.commitment()) .unauthenticated_notes(vec![note_y.clone()]) - .output_notes(vec![OutputNote::Full(note_x.clone())]) + .output_notes(vec![OutputNote::Full(note_x.clone()).to_proven_output_note().unwrap()]) .build()?; let batch = ProposedBatch::new( @@ -736,7 +746,7 @@ fn noop_tx_before_state_updating_tx_against_same_account() -> anyhow::Result<()> ) .ref_block_commitment(block1.commitment()) .authenticated_notes(vec![note1]) - .output_notes(vec![OutputNote::Full(note.clone())]) + .output_notes(vec![OutputNote::Full(note.clone()).to_proven_output_note().unwrap()]) .build()?; // sanity check @@ -797,7 +807,7 @@ fn noop_tx_after_state_updating_tx_against_same_account() -> anyhow::Result<()> ) .ref_block_commitment(block1.commitment()) .authenticated_notes(vec![note1]) - .output_notes(vec![OutputNote::Full(note.clone())]) + .output_notes(vec![OutputNote::Full(note.clone()).to_proven_output_note().unwrap()]) .build()?; // sanity check diff --git a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs index 95be1c04c5..da8bc3f9ea 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs @@ -9,7 +9,7 @@ use miden_protocol::crypto::merkle::SparseMerklePath; use miden_protocol::note::{Note, NoteInclusionProof, Nullifier}; use miden_protocol::transaction::{ InputNote, - OutputNote, + ProvenOutputNote, ProvenTransaction, ProvenTransactionBuilder, }; @@ -23,7 +23,7 @@ pub struct MockProvenTxBuilder { ref_block_commitment: Option, fee: FungibleAsset, expiration_block_num: BlockNumber, - output_notes: Option>, + output_notes: Option>, input_notes: Option>, nullifiers: Option>, } @@ -86,7 +86,7 @@ impl MockProvenTxBuilder { /// Adds notes to the transaction's output notes. #[must_use] - pub fn output_notes(mut self, notes: Vec) -> Self { + pub fn output_notes(mut self, notes: Vec) -> Self { self.output_notes = Some(notes); self diff --git a/crates/miden-testing/src/mock_chain/chain.rs b/crates/miden-testing/src/mock_chain/chain.rs index 430bdbcf36..7d596c95b1 100644 --- a/crates/miden-testing/src/mock_chain/chain.rs +++ b/crates/miden-testing/src/mock_chain/chain.rs @@ -25,8 +25,8 @@ use miden_protocol::transaction::{ ExecutedTransaction, InputNote, InputNotes, - OutputNote, PartialBlockchain, + ProvenOutputNote, ProvenTransaction, TransactionInputs, }; @@ -236,6 +236,7 @@ impl MockChain { account_tree: AccountTree, account_authenticators: BTreeMap, secret_key: SecretKey, + genesis_notes: Vec, ) -> anyhow::Result { let mut chain = MockChain { chain: Blockchain::default(), @@ -255,6 +256,20 @@ impl MockChain { .apply_block(genesis_block) .context("failed to build account from builder")?; + // Update committed_notes with full note details for genesis notes. + // This is needed because apply_block only stores headers for private notes, + // but tests need full note details to create input notes. + for note in genesis_notes { + if let Some(MockChainNote::Private(_, _, inclusion_proof)) = + chain.committed_notes.get(¬e.id()) + { + chain.committed_notes.insert( + note.id(), + MockChainNote::Public(note.clone(), inclusion_proof.clone()), + ); + } + } + debug_assert_eq!(chain.blocks.len(), 1); debug_assert_eq!(chain.committed_accounts.len(), chain.account_tree.num_accounts()); @@ -917,9 +932,11 @@ impl MockChain { ) .context("failed to create inclusion proof for output note")?; - if let OutputNote::Full(note) = created_note { - self.committed_notes - .insert(note.id(), MockChainNote::Public(note.clone(), note_inclusion_proof)); + if let ProvenOutputNote::Public(public_note) = created_note { + self.committed_notes.insert( + public_note.id(), + MockChainNote::Public(public_note.note().clone(), note_inclusion_proof), + ); } else { self.committed_notes.insert( created_note.id(), diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 3829e38fcd..6c38c20add 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -204,7 +204,22 @@ impl MockChainBuilder { ) .context("failed to create genesis account tree")?; - let note_chunks = self.notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH); + // Extract full notes before shrinking for later use in MockChain + let full_notes: Vec = self + .notes + .iter() + .filter_map(|note| match note { + OutputNote::Full(n) => Some(n.clone()), + _ => None, + }) + .collect(); + + let proven_notes: Vec<_> = self + .notes + .into_iter() + .map(|note| note.to_proven_output_note().expect("genesis note should be valid")) + .collect(); + let note_chunks = proven_notes.into_iter().chunks(MAX_OUTPUT_NOTES_PER_BATCH); let output_note_batches: Vec = note_chunks .into_iter() .map(|batch_notes| batch_notes.into_iter().enumerate().collect::>()) @@ -262,6 +277,7 @@ impl MockChainBuilder { account_tree, self.account_authenticators, validator_secret_key, + full_notes, ) } diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index 8f8f8d1ec0..734cce269a 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -356,12 +356,12 @@ async fn test_multisig_replay_protection(#[case] auth_scheme: AuthScheme) -> any .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) .add_signature(public_keys[1].to_commitment(), msg, sig_2.clone()) .auth_args(salt) - .build()?; - - let executed_tx = tx_context_execute.execute().await.expect("First transaction should succeed"); + .build()? + .execute() + .await?; // Apply the transaction to the mock chain - mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.add_pending_executed_transaction(&tx_context_execute)?; mock_chain.prove_next_block()?; // Attempt to execute the same transaction again - should fail due to replay protection @@ -518,8 +518,7 @@ async fn test_multisig_update_signers(#[case] auth_scheme: AuthScheme) -> anyhow .extend_advice_inputs(advice_inputs) .build()? .execute() - .await - .unwrap(); + .await?; // Verify the transaction executed successfully assert_eq!(update_approvers_tx.account_delta().nonce_delta(), Felt::new(1)); @@ -781,8 +780,7 @@ async fn test_multisig_update_signers_remove_owner( .extend_advice_inputs(advice_inputs) .build()? .execute() - .await - .unwrap(); + .await?; // Verify transaction success assert_eq!(update_approvers_tx.account_delta().nonce_delta(), Felt::new(1)); diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index fb6bddf6d8..4170bc0da1 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -1299,7 +1299,7 @@ async fn test_mint_note_output_note_types(#[case] note_type: NoteType) -> anyhow // For public notes, we get OutputNote::Full and can compare key properties let created_note = match output_note { OutputNote::Full(note) => note, - _ => panic!("Expected OutputNote::Full variant for public note"), + _ => panic!("Expected OutputNote::Full variant"), }; assert_eq!(created_note, &p2id_mint_output_note); diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index a270e3b884..015d7ec761 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -17,6 +17,7 @@ use miden_protocol::errors::{ AssetError, NoteError, ProvenTransactionError, + PublicOutputNoteError, TransactionInputError, TransactionInputsExtractionError, TransactionOutputError, @@ -149,6 +150,8 @@ pub enum TransactionProverError { RemoveFeeAssetFromDelta(#[source] AccountDeltaError), #[error("failed to construct transaction outputs")] TransactionOutputConstructionFailed(#[source] TransactionOutputError), + #[error("failed to shrink output note")] + OutputNoteShrinkFailed(#[source] PublicOutputNoteError), #[error("failed to build proven transaction")] ProvenTransactionBuildFailed(#[source] ProvenTransactionError), // Print the diagnostic directly instead of returning the source error. In the source error diff --git a/crates/miden-tx/src/host/note_builder.rs b/crates/miden-tx/src/host/note_builder.rs index eac4f8a006..d6e4c71526 100644 --- a/crates/miden-tx/src/host/note_builder.rs +++ b/crates/miden-tx/src/host/note_builder.rs @@ -91,8 +91,8 @@ impl OutputNoteBuilder { /// Converts this builder to an [OutputNote]. /// - /// Depending on the available information, this may result in [OutputNote::Full] or - /// [OutputNote::Partial] notes. + /// Depending on the available information, this may result in [`OutputNote::Full`] or + /// [`OutputNote::Partial`] notes. pub fn build(self) -> OutputNote { match self.recipient { Some(recipient) => { diff --git a/crates/miden-tx/src/prover/mod.rs b/crates/miden-tx/src/prover/mod.rs index 576a19f582..3ebd574b3b 100644 --- a/crates/miden-tx/src/prover/mod.rs +++ b/crates/miden-tx/src/prover/mod.rs @@ -8,7 +8,6 @@ use miden_protocol::block::BlockNumber; use miden_protocol::transaction::{ InputNote, InputNotes, - OutputNote, ProvenTransaction, ProvenTransactionBuilder, TransactionInputs, @@ -56,7 +55,12 @@ impl LocalTransactionProver { proof: ExecutionProof, ) -> Result { // erase private note information (convert private full notes to just headers) - let output_notes: Vec<_> = tx_outputs.output_notes.iter().map(OutputNote::shrink).collect(); + let output_notes: Vec<_> = tx_outputs + .output_notes + .iter() + .map(|note| note.to_proven_output_note()) + .collect::, _>>() + .map_err(TransactionProverError::OutputNoteShrinkFailed)?; // Compute the commitment of the pre-fee delta, which goes into the proven transaction, // since it is the output of the transaction and so is needed for proof verification. From 8fa4ab0e6bf67218420a8f2e17682d7c1f96062b Mon Sep 17 00:00:00 2001 From: Nikhil Patil Date: Fri, 6 Mar 2026 19:00:44 +0530 Subject: [PATCH 14/56] feat: introduce `AccountIdKey` (#2495) * Introducing a dedicated AccountIdKey type to unify and centralize all AccountId * changelog for introduce AccountIdKey type * refactor: clean up comments and improve code readability in AccountIdKey and tests * refactor: update AccountIdKey conversion method and clean up imports * refactor: reorganize AccountIdKey indices and clean up related code * Apply suggestions from code review * Update crates/miden-protocol/src/block/account_tree/account_id_key.rs * Update crates/miden-protocol/src/block/account_tree/account_id_key.rs * feat: validate account ID in `AccountTree::new` --------- Co-authored-by: Bobbin Threadbare <43513081+bobbinth@users.noreply.github.com> Co-authored-by: Philipp Gackstatter --- CHANGELOG.md | 1 + .../src/block/account_tree/account_id_key.rs | 158 ++++++++++++++++++ .../src/block/account_tree/backend.rs | 4 +- .../src/block/account_tree/mod.rs | 91 +++++----- .../src/block/account_tree/partial.rs | 16 +- .../src/block/account_tree/witness.rs | 14 +- crates/miden-protocol/src/errors/mod.rs | 2 + crates/miden-protocol/src/testing/block.rs | 4 +- .../src/transaction/inputs/mod.rs | 18 +- .../src/transaction/inputs/tests.rs | 16 +- .../src/transaction/kernel/advice_inputs.rs | 20 +-- .../src/kernel_tests/tx/test_prologue.rs | 10 +- 12 files changed, 248 insertions(+), 106 deletions(-) create mode 100644 crates/miden-protocol/src/block/account_tree/account_id_key.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index acce25aec8..47ee33d17b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ - [BREAKING] Renamed `AccountHeader::commitment`, `Account::commitment` and `PartialAccount::commitment` to `to_commitment` ([#2442](https://github.com/0xMiden/miden-base/pull/2442)). - [BREAKING] Remove `BlockSigner` trait ([#2447](https://github.com/0xMiden/miden-base/pull/2447)). - Updated account schema commitment construction to accept borrowed schema iterators; added extension trait to enable `AccountBuilder::with_schema_commitment()` helper ([#2419](https://github.com/0xMiden/miden-base/pull/2419)). +- Introducing a dedicated AccountIdKey type to unify and centralize all AccountId → SMT and advice-map key conversions ([#2495](https://github.com/0xMiden/miden-base/pull/2495)). - [BREAKING] Renamed `SchemaTypeId` to `SchemaType` ([#2494](https://github.com/0xMiden/miden-base/pull/2494)). - Updated stale `miden-base` references to `protocol` across docs, READMEs, code comments, and Cargo.toml repository URL ([#2503](https://github.com/0xMiden/protocol/pull/2503)). - [BREAKING] Reverse the order of the transaction summary on the stack ([#2512](https://github.com/0xMiden/miden-base/pull/2512)). diff --git a/crates/miden-protocol/src/block/account_tree/account_id_key.rs b/crates/miden-protocol/src/block/account_tree/account_id_key.rs new file mode 100644 index 0000000000..277b083978 --- /dev/null +++ b/crates/miden-protocol/src/block/account_tree/account_id_key.rs @@ -0,0 +1,158 @@ +use miden_crypto::merkle::smt::LeafIndex; + +use super::AccountId; +use crate::Word; +use crate::block::account_tree::AccountIdPrefix; +use crate::crypto::merkle::smt::SMT_DEPTH; +use crate::errors::AccountIdError; + +/// The account ID encoded as a key for use in AccountTree and advice maps in +/// `TransactionAdviceInputs`. +/// +/// Canonical word layout: +/// +/// [0, 0, suffix, prefix] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AccountIdKey(AccountId); + +impl AccountIdKey { + // Indices in the word layout where the prefix and suffix are stored. + const KEY_SUFFIX_IDX: usize = 2; + const KEY_PREFIX_IDX: usize = 3; + + /// Create from AccountId + pub fn new(id: AccountId) -> Self { + Self(id) + } + + /// Returns the underlying AccountId + pub fn account_id(&self) -> AccountId { + self.0 + } + + // SMT WORD REPRESENTATION + //--------------------------------------------------------------------------------------------------- + + /// Returns `[0, 0, suffix, prefix]` + pub fn as_word(&self) -> Word { + let mut key = Word::empty(); + + key[Self::KEY_SUFFIX_IDX] = self.0.suffix(); + key[Self::KEY_PREFIX_IDX] = self.0.prefix().as_felt(); + + key + } + + /// Construct from SMT word representation. + /// + /// Validates structure before converting. + pub fn try_from_word(word: Word) -> Result { + AccountId::try_from_elements(word[Self::KEY_SUFFIX_IDX], word[Self::KEY_PREFIX_IDX]) + } + + // LEAF INDEX + //--------------------------------------------------------------------------------------------------- + + /// Converts to SMT leaf index used by AccountTree + pub fn to_leaf_index(&self) -> LeafIndex { + LeafIndex::from(self.as_word()) + } + +} + +impl From for AccountIdKey { + fn from(id: AccountId) -> Self { + Self(id) + } +} + +// TESTS +//--------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + + use miden_core::ZERO; + + use super::{AccountId, *}; + use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; + #[test] + fn test_as_word_layout() { + let id = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let key = AccountIdKey::from(id); + let word = key.as_word(); + + assert_eq!(word[0], ZERO); + assert_eq!(word[1], ZERO); + assert_eq!(word[2], id.suffix()); + assert_eq!(word[3], id.prefix().as_felt()); + } + + #[test] + fn test_roundtrip_word_conversion() { + let id = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let key = AccountIdKey::from(id); + let recovered = + AccountIdKey::try_from_word(key.as_word()).expect("valid account id conversion"); + + assert_eq!(id, recovered); + } + + #[test] + fn test_leaf_index_consistency() { + let id = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let key = AccountIdKey::from(id); + + let idx1 = key.to_leaf_index(); + let idx2 = key.to_leaf_index(); + + assert_eq!(idx1, idx2); + } + + #[test] + fn test_from_conversion() { + let id = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let key: AccountIdKey = id.into(); + + assert_eq!(key.account_id(), id); + } + + #[test] + fn test_multiple_roundtrips() { + for _ in 0..100 { + let id = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let key = AccountIdKey::from(id); + + let recovered = + AccountIdKey::try_from_word(key.as_word()).expect("valid account id conversion"); + + assert_eq!(id, recovered); + } + } +} diff --git a/crates/miden-protocol/src/block/account_tree/backend.rs b/crates/miden-protocol/src/block/account_tree/backend.rs index 67995decfa..58963f0e44 100644 --- a/crates/miden-protocol/src/block/account_tree/backend.rs +++ b/crates/miden-protocol/src/block/account_tree/backend.rs @@ -1,7 +1,7 @@ use alloc::boxed::Box; use alloc::vec::Vec; -use super::{AccountId, AccountIdPrefix, AccountTree, AccountTreeError, account_id_to_smt_key}; +use super::{AccountId, AccountIdKey, AccountIdPrefix, AccountTree, AccountTreeError}; use crate::Word; use crate::crypto::merkle::MerkleError; #[cfg(feature = "std")] @@ -203,7 +203,7 @@ impl AccountTree { let smt = Smt::with_entries( entries .into_iter() - .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), + .map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment)), ) .map_err(|err| { let MerkleError::DuplicateValuesForIndex(leaf_idx) = err else { diff --git a/crates/miden-protocol/src/block/account_tree/mod.rs b/crates/miden-protocol/src/block/account_tree/mod.rs index 87c6fea85a..e594684be1 100644 --- a/crates/miden-protocol/src/block/account_tree/mod.rs +++ b/crates/miden-protocol/src/block/account_tree/mod.rs @@ -1,8 +1,6 @@ use alloc::string::ToString; use alloc::vec::Vec; -use miden_crypto::merkle::smt::LeafIndex; - use crate::Word; use crate::account::{AccountId, AccountIdPrefix}; use crate::crypto::merkle::MerkleError; @@ -25,39 +23,8 @@ pub use witness::AccountWitness; mod backend; pub use backend::AccountTreeBackend; -// FREE HELPER FUNCTIONS -// ================================================================================================ -// These module-level functions provide conversions between AccountIds and SMT keys. -// They avoid the need for awkward syntax like account_id_to_smt_key(). - -const KEY_SUFFIX_IDX: usize = 2; -const KEY_PREFIX_IDX: usize = 3; - -/// Converts an [`AccountId`] to an SMT key for use in account trees. -/// -/// The key is constructed with the account ID suffix at index 2 and prefix at index 3. -pub fn account_id_to_smt_key(account_id: AccountId) -> Word { - let mut key = Word::empty(); - key[KEY_SUFFIX_IDX] = account_id.suffix(); - key[KEY_PREFIX_IDX] = account_id.prefix().as_felt(); - key -} - -/// Recovers an [`AccountId`] from an SMT key. -/// -/// # Panics -/// -/// Panics if the key does not represent a valid account ID. This should never happen when used -/// with keys from account trees, as the tree only stores valid IDs. -pub fn smt_key_to_account_id(key: Word) -> AccountId { - AccountId::try_from_elements(key[KEY_SUFFIX_IDX], key[KEY_PREFIX_IDX]) - .expect("account tree should only contain valid IDs") -} - -/// Converts an AccountId to an SMT leaf index for use with MerkleStore operations. -pub fn account_id_to_smt_index(account_id: AccountId) -> LeafIndex { - account_id_to_smt_key(account_id).into() -} +mod account_id_key; +pub use account_id_key::AccountIdKey; // ACCOUNT TREE // ================================================================================================ @@ -110,7 +77,8 @@ where /// # Errors /// /// Returns an error if: - /// - The SMT contains duplicate account ID prefixes + /// - The SMT contains invalid account IDs. + /// - The SMT contains duplicate account ID prefixes. pub fn new(smt: S) -> Result { for (_leaf_idx, leaf) in smt.leaves() { match leaf { @@ -120,13 +88,19 @@ where }, SmtLeaf::Single((key, _)) => { // Single entry is good - verify it's a valid account ID - smt_key_to_account_id(key); + AccountIdKey::try_from_word(key).map_err(|err| { + AccountTreeError::InvalidAccountIdKey { key, source: err } + })?; }, SmtLeaf::Multiple(entries) => { // Multiple entries means duplicate prefixes // Extract one of the keys to identify the duplicate prefix if let Some((key, _)) = entries.first() { - let account_id = smt_key_to_account_id(*key); + let key = *key; + let account_id = AccountIdKey::try_from_word(key).map_err(|err| { + AccountTreeError::InvalidAccountIdKey { key, source: err } + })?; + return Err(AccountTreeError::DuplicateIdPrefix { duplicate_prefix: account_id.prefix(), }); @@ -164,7 +138,7 @@ where /// /// Panics if the SMT backend fails to open the leaf (only possible with `LargeSmt` backend). pub fn open(&self, account_id: AccountId) -> AccountWitness { - let key = account_id_to_smt_key(account_id); + let key = AccountIdKey::from(account_id).as_word(); let proof = self.smt.open(&key); AccountWitness::from_smt_proof(account_id, proof) @@ -172,7 +146,7 @@ where /// Returns the current state commitment of the given account ID. pub fn get(&self, account_id: AccountId) -> Word { - let key = account_id_to_smt_key(account_id); + let key = AccountIdKey::from(account_id).as_word(); self.smt.get_value(&key) } @@ -240,7 +214,7 @@ where .compute_mutations(Vec::from_iter( account_commitments .into_iter() - .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), + .map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment)), )) .map_err(AccountTreeError::ComputeMutations)?; @@ -254,7 +228,9 @@ where // valid. If it does not match, then we would insert a duplicate. if existing_key != *id_key { return Err(AccountTreeError::DuplicateIdPrefix { - duplicate_prefix: smt_key_to_account_id(*id_key).prefix(), + duplicate_prefix: AccountIdKey::try_from_word(*id_key) + .expect("account tree should only contain valid IDs") + .prefix(), }); } }, @@ -287,7 +263,7 @@ where account_id: AccountId, state_commitment: Word, ) -> Result { - let key = account_id_to_smt_key(account_id); + let key = AccountIdKey::from(account_id).as_word(); // SAFETY: account tree should not contain multi-entry leaves and so the maximum number // of entries per leaf should never be exceeded. let prev_value = self.smt.insert(key, state_commitment) @@ -378,9 +354,10 @@ impl Deserializable for AccountTree { } // Create the SMT with validated entries - let smt = - Smt::with_entries(entries.into_iter().map(|(k, v)| (account_id_to_smt_key(k), v))) - .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + let smt = Smt::with_entries( + entries.into_iter().map(|(k, v)| (AccountIdKey::from(k).as_word(), v)), + ) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; Ok(Self::new_unchecked(smt)) } } @@ -562,7 +539,7 @@ pub(super) mod tests { assert_eq!(tree.num_accounts(), 2); for id in [id0, id1] { - let proof = tree.smt.open(&account_id_to_smt_key(id)); + let proof = tree.smt.open(&AccountIdKey::from(id).as_word()); let (control_path, control_leaf) = proof.into_parts(); let witness = tree.open(id); @@ -606,7 +583,10 @@ pub(super) mod tests { // Create AccountTree with LargeSmt backend let tree = LargeSmt::::with_entries( MemoryStorage::default(), - [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + [ + (AccountIdKey::from(id0).as_word(), digest0), + (AccountIdKey::from(id1).as_word(), digest1), + ], ) .map(AccountTree::new_unchecked) .unwrap(); @@ -623,7 +603,10 @@ pub(super) mod tests { // Test mutations let mut tree_mut = LargeSmt::::with_entries( MemoryStorage::default(), - [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + [ + (AccountIdKey::from(id0).as_word(), digest0), + (AccountIdKey::from(id1).as_word(), digest1), + ], ) .map(AccountTree::new_unchecked) .unwrap(); @@ -672,7 +655,10 @@ pub(super) mod tests { let mut tree = LargeSmt::with_entries( MemoryStorage::default(), - [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + [ + (AccountIdKey::from(id0).as_word(), digest0), + (AccountIdKey::from(id1).as_word(), digest1), + ], ) .map(AccountTree::new_unchecked) .unwrap(); @@ -703,7 +689,10 @@ pub(super) mod tests { // Create tree with LargeSmt backend let large_tree = LargeSmt::with_entries( MemoryStorage::default(), - [(account_id_to_smt_key(id0), digest0), (account_id_to_smt_key(id1), digest1)], + [ + (AccountIdKey::from(id0).as_word(), digest0), + (AccountIdKey::from(id1).as_word(), digest1), + ], ) .map(AccountTree::new_unchecked) .unwrap(); diff --git a/crates/miden-protocol/src/block/account_tree/partial.rs b/crates/miden-protocol/src/block/account_tree/partial.rs index 653ba18cbc..f663bf5405 100644 --- a/crates/miden-protocol/src/block/account_tree/partial.rs +++ b/crates/miden-protocol/src/block/account_tree/partial.rs @@ -1,6 +1,6 @@ use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf}; -use super::{AccountWitness, account_id_to_smt_key}; +use super::{AccountIdKey, AccountWitness}; use crate::Word; use crate::account::AccountId; use crate::errors::AccountTreeError; @@ -68,7 +68,7 @@ impl PartialAccountTree { /// Returns an error if: /// - the account ID is not tracked by this account tree. pub fn open(&self, account_id: AccountId) -> Result { - let key = account_id_to_smt_key(account_id); + let key = AccountIdKey::from(account_id).as_word(); self.smt .open(&key) @@ -83,7 +83,7 @@ impl PartialAccountTree { /// Returns an error if: /// - the account ID is not tracked by this account tree. pub fn get(&self, account_id: AccountId) -> Result { - let key = account_id_to_smt_key(account_id); + let key = AccountIdKey::from(account_id).as_word(); self.smt .get_value(&key) .map_err(|source| AccountTreeError::UntrackedAccountId { id: account_id, source }) @@ -109,7 +109,7 @@ impl PartialAccountTree { /// witness. pub fn track_account(&mut self, witness: AccountWitness) -> Result<(), AccountTreeError> { let id_prefix = witness.id().prefix(); - let id_key = account_id_to_smt_key(witness.id()); + let id_key = AccountIdKey::from(witness.id()).as_word(); // If a leaf with the same prefix is already tracked by this partial tree, consider it an // error. @@ -165,7 +165,7 @@ impl PartialAccountTree { account_id: AccountId, state_commitment: Word, ) -> Result { - let key = account_id_to_smt_key(account_id); + let key = AccountIdKey::from(account_id).as_word(); // If there exists a tracked leaf whose key is _not_ the one we're about to overwrite, then // we would insert the new commitment next to an existing account ID with the same prefix, @@ -281,14 +281,14 @@ mod tests { // account IDs with the same prefix. let full_tree = Smt::with_entries( setup_duplicate_prefix_ids() - .map(|(id, commitment)| (account_id_to_smt_key(id), commitment)), + .map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment)), ) .unwrap(); let [(id0, _), (id1, _)] = setup_duplicate_prefix_ids(); - let key0 = account_id_to_smt_key(id0); - let key1 = account_id_to_smt_key(id1); + let key0 = AccountIdKey::from(id0).as_word(); + let key1 = AccountIdKey::from(id1).as_word(); let proof0 = full_tree.open(&key0); let proof1 = full_tree.open(&key1); assert_eq!(proof0.leaf(), proof1.leaf()); diff --git a/crates/miden-protocol/src/block/account_tree/witness.rs b/crates/miden-protocol/src/block/account_tree/witness.rs index 8c819fc86c..98be6b8310 100644 --- a/crates/miden-protocol/src/block/account_tree/witness.rs +++ b/crates/miden-protocol/src/block/account_tree/witness.rs @@ -1,11 +1,11 @@ use alloc::string::ToString; -use miden_crypto::merkle::smt::{LeafIndex, SMT_DEPTH, SmtLeaf, SmtProof, SmtProofError}; +use miden_crypto::merkle::smt::{SMT_DEPTH, SmtLeaf, SmtProof, SmtProofError}; use miden_crypto::merkle::{InnerNodeInfo, SparseMerklePath}; use crate::Word; use crate::account::AccountId; -use crate::block::account_tree::{account_id_to_smt_key, smt_key_to_account_id}; +use crate::block::account_tree::AccountIdKey; use crate::errors::AccountTreeError; use crate::utils::serde::{ ByteReader, @@ -74,6 +74,7 @@ impl AccountWitness { /// # Panics /// /// Panics if: + /// - the proof contains an entry whose key contains an invalid account ID. /// - the merkle path in the proof does not have depth equal to [`SMT_DEPTH`]. /// - the proof contains an SmtLeaf::Multiple. pub(super) fn from_smt_proof(requested_account_id: AccountId, proof: SmtProof) -> Self { @@ -89,7 +90,8 @@ impl AccountWitness { SmtLeaf::Empty(_) => requested_account_id, SmtLeaf::Single((key_in_leaf, _)) => { // SAFETY: By construction, the tree only contains valid IDs. - smt_key_to_account_id(*key_in_leaf) + AccountIdKey::try_from_word(*key_in_leaf) + .expect("account tree should only contain valid IDs") }, SmtLeaf::Multiple(_) => { unreachable!("account tree should only contain zero or one entry per ID prefix") @@ -97,7 +99,7 @@ impl AccountWitness { }; let commitment = proof - .get(&account_id_to_smt_key(witness_id)) + .get(&AccountIdKey::from(witness_id).as_word()) .expect("we should have received a proof for the witness key"); // SAFETY: The proof is guaranteed to have depth SMT_DEPTH if it comes from one of @@ -138,10 +140,10 @@ impl AccountWitness { /// Returns the [`SmtLeaf`] of the account witness. pub fn leaf(&self) -> SmtLeaf { if self.commitment == Word::empty() { - let leaf_idx = LeafIndex::from(account_id_to_smt_key(self.id)); + let leaf_idx = AccountIdKey::from(self.id).to_leaf_index(); SmtLeaf::new_empty(leaf_idx) } else { - let key = account_id_to_smt_key(self.id); + let key = AccountIdKey::from(self.id).as_word(); SmtLeaf::new_single(key, self.commitment) } } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 2481ba51c8..8467111652 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -274,6 +274,8 @@ pub enum AccountTreeError { ApplyMutations(#[source] MerkleError), #[error("failed to compute account tree mutations")] ComputeMutations(#[source] MerkleError), + #[error("provided smt contains an invalid account ID in key {key}")] + InvalidAccountIdKey { key: Word, source: AccountIdError }, #[error("smt leaf's index is not a valid account ID prefix")] InvalidAccountIdPrefix(#[source] AccountIdError), #[error("account witness merkle path depth {0} does not match AccountTree::DEPTH")] diff --git a/crates/miden-protocol/src/testing/block.rs b/crates/miden-protocol/src/testing/block.rs index 1e03c32561..cb4fbc446f 100644 --- a/crates/miden-protocol/src/testing/block.rs +++ b/crates/miden-protocol/src/testing/block.rs @@ -4,7 +4,7 @@ use miden_crypto::rand::test_utils::rand_value; use crate::Word; use crate::account::Account; -use crate::block::account_tree::{AccountTree, account_id_to_smt_key}; +use crate::block::account_tree::{AccountIdKey, AccountTree}; use crate::block::{BlockHeader, BlockNumber, FeeParameters}; use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; use crate::testing::random_secret_key::random_secret_key; @@ -26,7 +26,7 @@ impl BlockHeader { let smt = Smt::with_entries( accounts .iter() - .map(|acct| (account_id_to_smt_key(acct.id()), acct.to_commitment())), + .map(|acct| (AccountIdKey::from(acct.id()).as_word(), acct.to_commitment())), ) .expect("failed to create account db"); let acct_db = AccountTree::new(smt).expect("failed to create account tree"); diff --git a/crates/miden-protocol/src/transaction/inputs/mod.rs b/crates/miden-protocol/src/transaction/inputs/mod.rs index fa7decdc57..d483fc657f 100644 --- a/crates/miden-protocol/src/transaction/inputs/mod.rs +++ b/crates/miden-protocol/src/transaction/inputs/mod.rs @@ -20,12 +20,12 @@ use crate::account::{ StorageSlotName, }; use crate::asset::{Asset, AssetVaultKey, AssetWitness, PartialVault}; -use crate::block::account_tree::{AccountWitness, account_id_to_smt_index}; +use crate::block::account_tree::{AccountIdKey, AccountWitness}; use crate::block::{BlockHeader, BlockNumber}; use crate::crypto::merkle::SparseMerklePath; use crate::errors::{TransactionInputError, TransactionInputsExtractionError}; use crate::note::{Note, NoteInclusionProof}; -use crate::transaction::{TransactionAdviceInputs, TransactionArgs, TransactionScript}; +use crate::transaction::{TransactionArgs, TransactionScript}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -365,12 +365,12 @@ impl TransactionInputs { Ok(asset) } - /// Reads AccountInputs for a foreign account from the advice inputs. + /// Reads `AccountInputs` for a foreign account from the advice inputs. /// - /// This function reverses the process of [`TransactionAdviceInputs::add_foreign_accounts`] by: + /// This function reverses the process of `TransactionAdviceInputs::add_foreign_accounts` by: /// 1. Reading the account header from the advice map using the account_id_key. - /// 2. Building a PartialAccount from the header and foreign account code. - /// 3. Creating an AccountWitness. + /// 2. Building a `PartialAccount` from the header and foreign account code. + /// 3. Creating an `AccountWitness`. pub fn read_foreign_account_inputs( &self, account_id: AccountId, @@ -380,11 +380,11 @@ impl TransactionInputs { } // Read the account header elements from the advice map. - let account_id_key = TransactionAdviceInputs::account_id_map_key(account_id); + let account_id_key = AccountIdKey::from(account_id); let header_elements = self .advice_inputs .map - .get(&account_id_key) + .get(&account_id_key.as_word()) .ok_or(TransactionInputsExtractionError::ForeignAccountNotFound(account_id))?; // Parse the header from elements. @@ -450,7 +450,7 @@ impl TransactionInputs { ) -> Result { // Get the account tree root from the block header. let account_tree_root = self.block_header.account_root(); - let leaf_index: NodeIndex = account_id_to_smt_index(header.id()).into(); + let leaf_index = AccountIdKey::from(header.id()).to_leaf_index().into(); // Get the Merkle path from the merkle store. let merkle_path = self.advice_inputs.store.get_path(account_tree_root, leaf_index)?; diff --git a/crates/miden-protocol/src/transaction/inputs/tests.rs b/crates/miden-protocol/src/transaction/inputs/tests.rs index 494f46a6f6..cc2fcee7a2 100644 --- a/crates/miden-protocol/src/transaction/inputs/tests.rs +++ b/crates/miden-protocol/src/transaction/inputs/tests.rs @@ -16,6 +16,7 @@ use crate::account::{ StorageSlotType, }; use crate::asset::PartialVault; +use crate::block::account_tree::AccountIdKey; use crate::errors::TransactionInputsExtractionError; use crate::testing::account_id::{ ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, @@ -119,9 +120,10 @@ fn test_read_foreign_account_inputs_with_storage_data() { // Create advice inputs with both account header and storage header. let mut advice_inputs = crate::vm::AdviceInputs::default(); - let account_id_key = - crate::transaction::TransactionAdviceInputs::account_id_map_key(foreign_account_id); - advice_inputs.map.insert(account_id_key, foreign_header.to_elements()); + let account_id_key = AccountIdKey::from(foreign_account_id); + advice_inputs + .map + .insert(account_id_key.as_word(), foreign_header.to_elements().to_vec()); advice_inputs .map .insert(foreign_header.storage_commitment(), foreign_storage_header.to_elements()); @@ -232,10 +234,10 @@ fn test_read_foreign_account_inputs_with_proper_witness() { let mut advice_inputs = crate::vm::AdviceInputs::default(); // Add account header to advice map. - let account_id_key = - crate::transaction::TransactionAdviceInputs::account_id_map_key(foreign_account_id); - advice_inputs.map.insert(account_id_key, foreign_header.to_elements().to_vec()); - + let account_id_key = AccountIdKey::from(foreign_account_id); + advice_inputs + .map + .insert(account_id_key.as_word(), foreign_header.to_elements().to_vec()); // Add storage header to advice map. advice_inputs .map diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index d113944288..295a3ef03b 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -2,8 +2,8 @@ use alloc::vec::Vec; use miden_processor::advice::AdviceMutation; -use crate::account::{AccountHeader, AccountId, PartialAccount}; -use crate::block::account_tree::{AccountWitness, account_id_to_smt_key}; +use crate::account::{AccountHeader, PartialAccount}; +use crate::block::account_tree::{AccountIdKey, AccountWitness}; use crate::crypto::SequentialCommit; use crate::crypto::merkle::InnerNodeInfo; use crate::note::NoteAttachmentContent; @@ -56,8 +56,8 @@ impl TransactionAdviceInputs { // If a seed was provided, extend the map appropriately. if let Some(seed) = tx_inputs.account().seed() { // ACCOUNT_ID |-> ACCOUNT_SEED - let account_id_key = Self::account_id_map_key(partial_native_acc.id()); - inputs.add_map_entry(account_id_key, seed.to_vec()); + let account_id_key = AccountIdKey::from(partial_native_acc.id()); + inputs.add_map_entry(account_id_key.as_word(), seed.to_vec()); } // if the account is new, insert the storage map entries into the advice provider. @@ -104,14 +104,6 @@ impl TransactionAdviceInputs { // PUBLIC UTILITIES // -------------------------------------------------------------------------------------------- - /// Returns the advice map key where: - /// - the seed for native accounts is stored. - /// - the account header for foreign accounts is stored. - pub fn account_id_map_key(id: AccountId) -> Word { - // The format is equivalent to the SMT key format, so we avoid defining it twice. - account_id_to_smt_key(id) - } - // MUTATORS // -------------------------------------------------------------------------------------------- @@ -131,11 +123,11 @@ impl TransactionAdviceInputs { // for foreign accounts, we need to insert the id to state mapping // NOTE: keep this in sync with the account::load_from_advice procedure - let account_id_key = Self::account_id_map_key(foreign_acc.id()); + let account_id_key = AccountIdKey::from(foreign_acc.id()); let header = AccountHeader::from(foreign_acc.account()); // ACCOUNT_ID |-> [ID_AND_NONCE, VAULT_ROOT, STORAGE_COMMITMENT, CODE_COMMITMENT] - self.add_map_entry(account_id_key, header.to_elements()); + self.add_map_entry(account_id_key.as_word(), header.to_elements()); } } 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 76f48b7b15..3cb661b3a2 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_prologue.rs @@ -15,6 +15,7 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; +use miden_protocol::block::account_tree::AccountIdKey; use miden_protocol::errors::tx_kernel::ERR_ACCOUNT_SEED_AND_COMMITMENT_DIGEST_MISMATCH; use miden_protocol::note::NoteId; use miden_protocol::testing::account_id::{ @@ -75,12 +76,7 @@ use miden_protocol::transaction::memory::{ VALIDATOR_KEY_COMMITMENT_PTR, VERIFICATION_BASE_FEE_IDX, }; -use miden_protocol::transaction::{ - ExecutedTransaction, - TransactionAdviceInputs, - TransactionArgs, - TransactionKernel, -}; +use miden_protocol::transaction::{ExecutedTransaction, TransactionArgs, TransactionKernel}; use miden_protocol::{EMPTY_WORD, WORD_SIZE}; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; @@ -637,7 +633,7 @@ pub async fn create_account_invalid_seed() -> anyhow::Result<()> { .expect("failed to get transaction inputs from mock chain"); // override the seed with an invalid seed to ensure the kernel fails - let account_seed_key = TransactionAdviceInputs::account_id_map_key(account.id()); + let account_seed_key = AccountIdKey::from(account.id()).as_word(); let adv_inputs = AdviceInputs::default().with_map([(account_seed_key, vec![ZERO; WORD_SIZE])]); let tx_context = TransactionContextBuilder::new(account) From af78cb010f4a0e17be5bc873ff4d619ed56414cd Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 6 Mar 2026 14:49:57 +0100 Subject: [PATCH 15/56] fix: make format and remove unused import (#2564) --- crates/miden-protocol/src/block/account_tree/account_id_key.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/miden-protocol/src/block/account_tree/account_id_key.rs b/crates/miden-protocol/src/block/account_tree/account_id_key.rs index 277b083978..1974e866b5 100644 --- a/crates/miden-protocol/src/block/account_tree/account_id_key.rs +++ b/crates/miden-protocol/src/block/account_tree/account_id_key.rs @@ -2,7 +2,6 @@ use miden_crypto::merkle::smt::LeafIndex; use super::AccountId; use crate::Word; -use crate::block::account_tree::AccountIdPrefix; use crate::crypto::merkle::smt::SMT_DEPTH; use crate::errors::AccountIdError; @@ -57,7 +56,6 @@ impl AccountIdKey { pub fn to_leaf_index(&self) -> LeafIndex { LeafIndex::from(self.as_word()) } - } impl From for AccountIdKey { From 25c65a5a2a0e8fa0ee2544f80cbe275bf5359a28 Mon Sep 17 00:00:00 2001 From: Santiago Pittella <87827390+SantiagoPittella@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:37:44 -0300 Subject: [PATCH 16/56] feat: make `NoteMetadataHeader` public (#2561) --- CHANGELOG.md | 1 + crates/miden-protocol/src/errors/mod.rs | 24 +++++++- crates/miden-protocol/src/note/metadata.rs | 68 ++++++++++++++++++++-- crates/miden-protocol/src/note/mod.rs | 2 +- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ee33d17b..154331f3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Made `NoteMetadataHeader` and `NoteMetadata::to_header()` public, added `NoteMetadata::from_header()` constructor, and exported `NoteMetadataHeader` from the `note` module ([#2561](https://github.com/0xMiden/protocol/pull/2561)). - Introduce NOTE_MAX_SIZE (32 KiB) and enforce it on individual output notes ([#2205](https://github.com/0xMiden/miden-base/pull/2205)) - Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). - Added metadata extension (`Info`) with name and content URI slots, and MASM procedures `get_token_metadata`, `get_max_supply`, `get_decimals`, and `get_token_symbol` for the token metadata standard. Aligned fungible faucet token metadata with the metadata standard: faucet now uses the canonical slot `miden::standards::metadata::token_metadata` so MASM metadata getters work with faucet storage.([#2439](https://github.com/0xMiden/miden-base/pull/2439)) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 8467111652..94f1936e1e 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -31,7 +31,15 @@ use crate::address::AddressType; use crate::asset::AssetId; use crate::batch::BatchId; use crate::block::BlockNumber; -use crate::note::{NoteAssets, NoteAttachmentArray, NoteTag, NoteType, Nullifier}; +use crate::note::{ + NoteAssets, + NoteAttachmentArray, + NoteAttachmentKind, + NoteAttachmentScheme, + NoteTag, + NoteType, + Nullifier, +}; use crate::transaction::{TransactionEventId, TransactionId}; use crate::utils::serde::DeserializationError; use crate::vm::EventId; @@ -596,6 +604,20 @@ pub enum NoteError { UnknownNoteAttachmentKind(u8), #[error("note attachment of kind None must have attachment scheme None")] AttachmentKindNoneMustHaveAttachmentSchemeNone, + #[error( + "note attachment kind mismatch: header has {header_kind:?} but attachment has {attachment_kind:?}" + )] + AttachmentKindMismatch { + header_kind: NoteAttachmentKind, + attachment_kind: NoteAttachmentKind, + }, + #[error( + "note attachment scheme mismatch: header has {header_scheme:?} but attachment has {attachment_scheme:?}" + )] + AttachmentSchemeMismatch { + header_scheme: NoteAttachmentScheme, + attachment_scheme: NoteAttachmentScheme, + }, #[error("{error_msg}")] Other { error_msg: Box, diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index 14fd98dd16..bc0877df8b 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -89,6 +89,38 @@ impl NoteMetadata { } } + /// Reconstructs a [`NoteMetadata`] from a [`NoteMetadataHeader`] and a + /// [`NoteAttachment`]. + /// + /// # Errors + /// + /// Returns an error if the attachment's kind or scheme do not match those in the header. + pub fn try_from_header( + header: NoteMetadataHeader, + attachment: NoteAttachment, + ) -> Result { + if header.attachment_kind != attachment.attachment_kind() { + return Err(NoteError::AttachmentKindMismatch { + header_kind: header.attachment_kind, + attachment_kind: attachment.attachment_kind(), + }); + } + + if header.attachment_scheme != attachment.attachment_scheme() { + return Err(NoteError::AttachmentSchemeMismatch { + header_scheme: header.attachment_scheme, + attachment_scheme: attachment.attachment_scheme(), + }); + } + + Ok(Self { + sender: header.sender, + note_type: header.note_type, + tag: header.tag, + attachment, + }) + } + // ACCESSORS // -------------------------------------------------------------------------------------------- @@ -120,7 +152,7 @@ impl NoteMetadata { /// Returns the header of a [`NoteMetadata`] as a [`Word`]. /// /// See [`NoteMetadata`] docs for more details. - fn to_header(&self) -> NoteMetadataHeader { + pub fn to_header(&self) -> NoteMetadataHeader { NoteMetadataHeader { sender: self.sender, note_type: self.note_type, @@ -219,10 +251,8 @@ impl Deserializable for NoteMetadata { /// The header representation of [`NoteMetadata`]. /// /// See the metadata's type for details on this type's [`Word`] layout. -/// -/// This is intended to be a private type meant for encapsulating the conversion from and to words. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct NoteMetadataHeader { +pub struct NoteMetadataHeader { sender: AccountId, note_type: NoteType, tag: NoteTag, @@ -230,6 +260,36 @@ struct NoteMetadataHeader { attachment_scheme: NoteAttachmentScheme, } +impl NoteMetadataHeader { + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the account which created the note. + pub fn sender(&self) -> AccountId { + self.sender + } + + /// Returns the note's type. + pub fn note_type(&self) -> NoteType { + self.note_type + } + + /// Returns the tag associated with the note. + pub fn tag(&self) -> NoteTag { + self.tag + } + + /// Returns the attachment kind. + pub fn attachment_kind(&self) -> NoteAttachmentKind { + self.attachment_kind + } + + /// Returns the attachment scheme. + pub fn attachment_scheme(&self) -> NoteAttachmentScheme { + self.attachment_scheme + } +} + impl From for Word { fn from(header: NoteMetadataHeader) -> Self { let mut metadata = Word::empty(); diff --git a/crates/miden-protocol/src/note/mod.rs b/crates/miden-protocol/src/note/mod.rs index 856795a7da..0b13780a90 100644 --- a/crates/miden-protocol/src/note/mod.rs +++ b/crates/miden-protocol/src/note/mod.rs @@ -24,7 +24,7 @@ mod storage; pub use storage::NoteStorage; mod metadata; -pub use metadata::NoteMetadata; +pub use metadata::{NoteMetadata, NoteMetadataHeader}; mod attachment; pub use attachment::{ From 527695f4dfe0f55ee5c60a00576ad42b3cdbf8c9 Mon Sep 17 00:00:00 2001 From: Serge Radinovich <47865535+sergerad@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:18:35 +1300 Subject: [PATCH 17/56] chore: ProvenBlock constructor with validation (#2553) --- .../miden-protocol/src/block/proven_block.rs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/crates/miden-protocol/src/block/proven_block.rs b/crates/miden-protocol/src/block/proven_block.rs index 5d43b072b4..68abc97d23 100644 --- a/crates/miden-protocol/src/block/proven_block.rs +++ b/crates/miden-protocol/src/block/proven_block.rs @@ -1,3 +1,4 @@ +use miden_core::Word; use miden_crypto::dsa::ecdsa_k256_keccak::Signature; use crate::MIN_PROOF_SECURITY_LEVEL; @@ -10,6 +11,28 @@ use crate::utils::serde::{ Serializable, }; +// PROVEN BLOCK ERROR +// ================================================================================================ + +#[derive(Debug, thiserror::Error)] +pub enum ProvenBlockError { + #[error( + "ECDSA signature verification failed based on the proven block's header commitment, validator public key and signature" + )] + InvalidSignature, + #[error( + "header tx commitment ({header_tx_commitment}) does not match body tx commitment ({body_tx_commitment})" + )] + TxCommitmentMismatch { + header_tx_commitment: Word, + body_tx_commitment: Word, + }, + #[error( + "proven block header note root ({header_root}) does not match the corresponding body's note root ({body_root})" + )] + NoteRootMismatch { header_root: Word, body_root: Word }, +} + // PROVEN BLOCK // ================================================================================================ @@ -35,6 +58,44 @@ pub struct ProvenBlock { } impl ProvenBlock { + /// Returns a new [`ProvenBlock`] instantiated from the provided components. + /// + /// Validates that the provided components correspond to each other by verifying the signature, + /// and checking for matching transaction commitments and note roots. + /// + /// Involves non-trivial computation. Use [`Self::new_unchecked`] if the validation is not + /// necessary. + /// + /// Note: this does not fully validate the consistency of provided components. Specifically, + /// we cannot validate that: + /// - That applying the account updates in the block body to the account tree represented by the + /// root from the previous block header would actually result in the account root in the + /// provided header. + /// - That inserting the created nullifiers in the block body to the nullifier tree represented + /// by the root from the previous block header would actually result in the nullifier root in + /// the provided header. + /// + /// # Errors + /// Returns an error if: + /// - If the validator signature does not verify against the block header commitment and the + /// validator key. + /// - If the transaction commitment in the block header is inconsistent with the transactions + /// included in the block body. + /// - If the note root in the block header is inconsistent with the notes included in the block + /// body. + pub fn new( + header: BlockHeader, + body: BlockBody, + signature: Signature, + proof: BlockProof, + ) -> Result { + let proven_block = Self { header, signature, body, proof }; + + proven_block.validate()?; + + Ok(proven_block) + } + /// Returns a new [`ProvenBlock`] instantiated from the provided components. /// /// # Warning @@ -50,6 +111,42 @@ impl ProvenBlock { Self { header, signature, body, proof } } + /// Validates that the components of the proven block correspond to each other by verifying the + /// signature, and checking for matching transaction commitments and note roots. + /// + /// Validation involves non-trivial computation, and depending on the size of the block may + /// take non-negligible amount of time. + /// + /// Note: this does not fully validate the consistency of internal components. Specifically, + /// we cannot validate that: + /// - That applying the account updates in the block body to the account tree represented by the + /// root from the previous block header would actually result in the account root in the + /// provided header. + /// - That inserting the created nullifiers in the block body to the nullifier tree represented + /// by the root from the previous block header would actually result in the nullifier root in + /// the provided header. + /// + /// # Errors + /// Returns an error if: + /// - If the validator signature does not verify against the block header commitment and the + /// validator key. + /// - If the transaction commitment in the block header is inconsistent with the transactions + /// included in the block body. + /// - If the note root in the block header is inconsistent with the notes included in the block + /// body. + pub fn validate(&self) -> Result<(), ProvenBlockError> { + // Verify signature. + self.validate_signature()?; + + // Validate that header / body transaction commitments match. + self.validate_tx_commitment()?; + + // Validate that header / body note roots match. + self.validate_note_root()?; + + Ok(()) + } + /// Returns the proof security level of the block. pub fn proof_security_level(&self) -> u32 { MIN_PROOF_SECURITY_LEVEL @@ -79,6 +176,45 @@ impl ProvenBlock { pub fn into_parts(self) -> (BlockHeader, BlockBody, Signature, BlockProof) { (self.header, self.body, self.signature, self.proof) } + + // HELPER METHODS + // -------------------------------------------------------------------------------------------- + + /// Performs ECDSA signature verification against the header commitment and validator key. + fn validate_signature(&self) -> Result<(), ProvenBlockError> { + if !self.signature.verify(self.header.commitment(), self.header.validator_key()) { + Err(ProvenBlockError::InvalidSignature) + } else { + Ok(()) + } + } + + /// Validates that the transaction commitments between the header and body match for this proven + /// block. + /// + /// Involves non-trivial computation of the body's transaction commitment. + fn validate_tx_commitment(&self) -> Result<(), ProvenBlockError> { + let header_tx_commitment = self.header.tx_commitment(); + let body_tx_commitment = self.body.transactions().commitment(); + if header_tx_commitment != body_tx_commitment { + Err(ProvenBlockError::TxCommitmentMismatch { header_tx_commitment, body_tx_commitment }) + } else { + Ok(()) + } + } + + /// Validates that the header's note tree root matches that of the body. + /// + /// Involves non-trivial computation of the body's note tree. + fn validate_note_root(&self) -> Result<(), ProvenBlockError> { + let header_root = self.header.note_root(); + let body_root = self.body.compute_block_note_tree().root(); + if header_root != body_root { + Err(ProvenBlockError::NoteRootMismatch { header_root, body_root }) + } else { + Ok(()) + } + } } // SERIALIZATION From daebbbc47d5551441fed927b8914f2fd669296db Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Fri, 6 Mar 2026 13:33:44 -0800 Subject: [PATCH 18/56] refactor: include fee in TransactionId computation --- CHANGELOG.md | 3 ++- .../src/transaction/executed_tx.rs | 1 + .../miden-protocol/src/transaction/proven_tx.rs | 2 ++ .../src/transaction/transaction_id.rs | 17 +++++++++++++---- .../miden-protocol/src/transaction/tx_header.rs | 13 ++++++++----- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 154331f3ca..fddeba6bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,8 @@ - [BREAKING] Renamed `SchemaTypeId` to `SchemaType` ([#2494](https://github.com/0xMiden/miden-base/pull/2494)). - Updated stale `miden-base` references to `protocol` across docs, READMEs, code comments, and Cargo.toml repository URL ([#2503](https://github.com/0xMiden/protocol/pull/2503)). - [BREAKING] Reverse the order of the transaction summary on the stack ([#2512](https://github.com/0xMiden/miden-base/pull/2512)). -- Use `@auth_script` MASM attribute instead of `auth_` prefix to identify authentication procedures in account components ([#2534](https://github.com/0xMiden/protocol/pull/2534)). +- [BREAKING] Use `@auth_script` MASM attribute instead of `auth_` prefix to identify authentication procedures in account components ([#2534](https://github.com/0xMiden/protocol/pull/2534)). +- [BREAKING] Changed `TransactionId` to include fee asset in hash computation, making it commit to entire `TransactionHeader` contents. ## 0.13.3 (2026-01-27) diff --git a/crates/miden-protocol/src/transaction/executed_tx.rs b/crates/miden-protocol/src/transaction/executed_tx.rs index 4892709e59..2b047d435e 100644 --- a/crates/miden-protocol/src/transaction/executed_tx.rs +++ b/crates/miden-protocol/src/transaction/executed_tx.rs @@ -71,6 +71,7 @@ impl ExecutedTransaction { tx_outputs.account.to_commitment(), tx_inputs.input_notes().commitment(), tx_outputs.output_notes.commitment(), + tx_outputs.fee, ); Self { diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index 4f868d019b..4a9b53303b 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -218,6 +218,7 @@ impl Deserializable for ProvenTransaction { account_update.final_state_commitment(), input_notes.commitment(), output_notes.commitment(), + fee, ); let proven_transaction = Self { @@ -378,6 +379,7 @@ impl ProvenTransactionBuilder { self.final_account_commitment, input_notes.commitment(), output_notes.commitment(), + self.fee, ); let account_update = TxAccountUpdate::new( self.account_id, diff --git a/crates/miden-protocol/src/transaction/transaction_id.rs b/crates/miden-protocol/src/transaction/transaction_id.rs index e75f7662f7..ddf14c7532 100644 --- a/crates/miden-protocol/src/transaction/transaction_id.rs +++ b/crates/miden-protocol/src/transaction/transaction_id.rs @@ -4,6 +4,7 @@ use core::fmt::{Debug, Display}; use miden_protocol_macros::WordWrapper; use super::{Felt, Hasher, ProvenTransaction, WORD_SIZE, Word, ZERO}; +use crate::asset::{Asset, FungibleAsset}; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -19,8 +20,13 @@ use crate::utils::serde::{ /// /// Transaction ID is computed as: /// -/// hash(init_account_commitment, final_account_commitment, input_notes_commitment, -/// output_notes_commitment) +/// hash( +/// INIT_ACCOUNT_COMMITMENT, +/// FINAL_ACCOUNT_COMMITMENT, +/// INPUT_NOTES_COMMITMENT, +/// OUTPUT_NOTES_COMMITMENT, +/// FEE_ASSET, +/// ) /// /// This achieves the following properties: /// - Transactions are identical if and only if they have the same ID. @@ -35,12 +41,14 @@ impl TransactionId { final_account_commitment: Word, input_notes_commitment: Word, output_notes_commitment: Word, + fee_asset: FungibleAsset, ) -> Self { - let mut elements = [ZERO; 4 * WORD_SIZE]; + let mut elements = [ZERO; 6 * WORD_SIZE]; elements[..4].copy_from_slice(init_account_commitment.as_elements()); elements[4..8].copy_from_slice(final_account_commitment.as_elements()); elements[8..12].copy_from_slice(input_notes_commitment.as_elements()); - elements[12..].copy_from_slice(output_notes_commitment.as_elements()); + elements[12..16].copy_from_slice(output_notes_commitment.as_elements()); + elements[16..].copy_from_slice(&Asset::from(fee_asset).as_elements()); Self(Hasher::hash_elements(&elements)) } } @@ -67,6 +75,7 @@ impl From<&ProvenTransaction> for TransactionId { tx.account_update().final_state_commitment(), tx.input_notes().commitment(), tx.output_notes().commitment(), + tx.fee(), ) } } diff --git a/crates/miden-protocol/src/transaction/tx_header.rs b/crates/miden-protocol/src/transaction/tx_header.rs index 6f5f071067..70f8a917be 100644 --- a/crates/miden-protocol/src/transaction/tx_header.rs +++ b/crates/miden-protocol/src/transaction/tx_header.rs @@ -23,10 +23,11 @@ use crate::utils::serde::{ /// A transaction header derived from a /// [`ProvenTransaction`](crate::transaction::ProvenTransaction). /// -/// The header is essentially a direct copy of the transaction's commitments, in particular the -/// initial and final account state commitment as well as all nullifiers of consumed notes and all -/// note IDs of created notes. While account updates may be aggregated and notes may be erased as -/// part of batch and block building, the header retains the original transaction's data. +/// The header is essentially a direct copy of the transaction's public commitments, in particular +/// the initial and final account state commitment as well as all nullifiers of consumed notes and +/// all note IDs of created notes together with the fee asset. While account updates may be +/// aggregated and notes may be erased as part of batch and block building, the header retains the +/// original transaction's data. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TransactionHeader { id: TransactionId, @@ -44,7 +45,8 @@ impl TransactionHeader { /// Constructs a new [`TransactionHeader`] from the provided parameters. /// - /// The [`TransactionId`] is computed from the provided parameters. + /// The [`TransactionId`] is computed from the provided parameters, committing to the initial + /// and final account commitments, input and output note commitments, and the fee asset. /// /// The input notes and output notes must be in the same order as they appeared in the /// transaction that this header represents, otherwise an incorrect ID will be computed. @@ -67,6 +69,7 @@ impl TransactionHeader { final_state_commitment, input_notes_commitment, output_notes_commitment, + fee, ); Self { From cbfdd1c57ecb116ba20445d975ef377da929ba87 Mon Sep 17 00:00:00 2001 From: Percy Dikec <112529374+PercyDikec@users.noreply.github.com> Date: Sat, 7 Mar 2026 01:49:33 +0300 Subject: [PATCH 19/56] refactor: remove `Ord` and `PartialOrd` from `StorageSlot` (#2549) --- crates/miden-protocol/src/account/storage/mod.rs | 4 ++-- .../src/account/storage/slot/storage_slot.rs | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/crates/miden-protocol/src/account/storage/mod.rs b/crates/miden-protocol/src/account/storage/mod.rs index fc1fc6614b..4f3217f905 100644 --- a/crates/miden-protocol/src/account/storage/mod.rs +++ b/crates/miden-protocol/src/account/storage/mod.rs @@ -76,7 +76,7 @@ impl AccountStorage { } // Unstable sort is fine because we require all names to be unique. - slots.sort_unstable(); + slots.sort_unstable_by(|a, b| a.name().cmp(b.name())); // Check for slot name uniqueness by checking each neighboring slot's IDs. This is // sufficient because the slots are sorted. @@ -422,7 +422,7 @@ mod tests { assert_eq!(name, slot_name0); }); - slots.sort_unstable(); + slots.sort_unstable_by(|a, b| a.name().cmp(b.name())); let err = AccountStorageHeader::new(slots.iter().map(StorageSlotHeader::from).collect()) .unwrap_err(); diff --git a/crates/miden-protocol/src/account/storage/slot/storage_slot.rs b/crates/miden-protocol/src/account/storage/slot/storage_slot.rs index 2346389932..37da4c86a9 100644 --- a/crates/miden-protocol/src/account/storage/slot/storage_slot.rs +++ b/crates/miden-protocol/src/account/storage/slot/storage_slot.rs @@ -108,18 +108,6 @@ impl StorageSlot { } } -impl Ord for StorageSlot { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.name().cmp(&other.name) - } -} - -impl PartialOrd for StorageSlot { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - // SERIALIZATION // ================================================================================================ From 330f939eee376c767b72196506e207e064e59cb0 Mon Sep 17 00:00:00 2001 From: Serge Radinovich <47865535+sergerad@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:09:31 +1300 Subject: [PATCH 20/56] chore: Replace SMT leaf conversion function (#2271) --- Cargo.lock | 141 +++++++++--------- .../src/transaction/inputs/mod.rs | 61 +------- 2 files changed, 71 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f121261ce..3df2fa023c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,20 +1020,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -1234,9 +1234,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -1247,9 +1247,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1413,23 +1413,22 @@ dependencies = [ [[package]] name = "miden-air" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3ec8445c7ff598e312f2f88332333581a1882080ffe40e22181efdad68ad2c" +checksum = "d8aa2b3bc95d9eece8b47edbc6621b5742e212b359ff6b82ebb813b3d9b28985" dependencies = [ "miden-core", "miden-crypto", "miden-utils-indexing", - "serde", "thiserror", "tracing", ] [[package]] name = "miden-assembly" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0204034c9424d52669677eeacd408cd59483d7a87a66489ad9ae716b5bda9a6e" +checksum = "89369e85051e14e21c52f8e38456b4db958151afb32a3cef0a522e04163ec5c2" dependencies = [ "env_logger", "log", @@ -1442,9 +1441,9 @@ dependencies = [ [[package]] name = "miden-assembly-syntax" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a73ed7c463e68374f6eb595b6286f8d5d116a97004eeaee39e89cdc694dd5be1" +checksum = "9069e6fa110d918662ce77eecfc3d7f906050023fad899f414fc63122e31b771" dependencies = [ "aho-corasick", "env_logger", @@ -1474,9 +1473,9 @@ dependencies = [ [[package]] name = "miden-core" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f53957feff616a43cd4ad5be9efeaeb7c3adfb589115e67a19eaf518f5c5e53a" +checksum = "9a9ebf937ab3ebc6d540cc7c48dd5cfc08da8b19e38757f71229d6b50414268b" dependencies = [ "derive_more", "itertools 0.14.0", @@ -1490,15 +1489,14 @@ dependencies = [ "num-traits", "proptest", "proptest-derive", - "serde", "thiserror", ] [[package]] name = "miden-core-lib" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2367698f1883037351102291408cab81d1535ace3b7f5f2b4f493a52e126df" +checksum = "fa496b3a7546c0022e8d5a92d88726907e380074f1fb634859b5e2094270dacf" dependencies = [ "env_logger", "fs-err", @@ -1512,9 +1510,9 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cfbecd3d91ece4489b78ca2ea67b31a7b635164285c1a828b7386d691c7ec72" +checksum = "4b8fc3ec2033d3e17a40611f3ab7c20b0578ccf5e6ddcc9a1df9f26599e6ebdd" dependencies = [ "blake3", "cc", @@ -1552,7 +1550,6 @@ dependencies = [ "rand_core 0.9.5", "rand_hc", "rayon", - "serde", "sha2", "sha3", "subtle", @@ -1562,9 +1559,9 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6757c8a32e2e42e95ee39ca21d1f362c099cf938f482f69ba518771b773222" +checksum = "207828f24e358b4e1e0641c37802816b8730816ff92ddb4d271ef3a00f8696bb" dependencies = [ "quote", "syn 2.0.117", @@ -1572,9 +1569,9 @@ dependencies = [ [[package]] name = "miden-debug-types" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d76def09875650d6d2ea568015ae99c892aa34c3393a0c3ba43891c8c4bd5ba" +checksum = "6bbdee85c103fe0979ed05f888da8c0b078446b2feee17a67f56d75d6189adae" dependencies = [ "memchr", "miden-crypto", @@ -1590,9 +1587,9 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764dd20d71bf30c09981d6223963d3d633956e9dc252932540c089f94dee5981" +checksum = "f821a07c16cfa6e500d5a56d05c11523984e3cd562cfc80ef657e4264d708067" dependencies = [ "miden-serde-utils", "num-bigint", @@ -1616,9 +1613,9 @@ dependencies = [ [[package]] name = "miden-mast-package" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23d0cf5de7df1273c14614fef84cb5fb464f2f0f354018228b0355ecbcb4206" +checksum = "47f6dfbe2e3a2ca9977a46551d378cf4c5232624d50bd604c644eaa95342a5c1" dependencies = [ "derive_more", "miden-assembly-syntax", @@ -1665,9 +1662,9 @@ dependencies = [ [[package]] name = "miden-processor" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e173de74f1085c66aa269c817f3340c33d5f03221ce9bdf1fff3cf53d910016e" +checksum = "13c83cc2f87364c88f6b7d7acb0c7908b63064ed94e0b2b68a0f5990f74a42c5" dependencies = [ "itertools 0.14.0", "miden-air", @@ -1730,9 +1727,9 @@ dependencies = [ [[package]] name = "miden-prover" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5087f55ce792bdf2633dd9157375186b9f36ec5d9eb9ca9301c3d4162df527b" +checksum = "d0fe4c03cc2a5c0404596f10c076e8e265d87fb7a9c5fbe21b15bc12874f7855" dependencies = [ "bincode", "miden-air", @@ -1747,9 +1744,9 @@ dependencies = [ [[package]] name = "miden-serde-utils" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8f83e29603783ff4effc90c3c56579cc979960ac2a647f05554dc27a7faf6a" +checksum = "1fe74c2e7d8a8b8758e067de10665816928222c1d0561d95c12ac4bcaefc2a2a" dependencies = [ "p3-field", "p3-goldilocks", @@ -1828,9 +1825,9 @@ dependencies = [ [[package]] name = "miden-utils-core-derive" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cd50b0e70fafee54f3fe7a503e4b269442b7edf018fad4cf2c67fa4bd5959a5" +checksum = "ad5c364abe484d43d171afc320e7560db37ece00fe625569068c1053ed186540" dependencies = [ "proc-macro2", "quote", @@ -1839,9 +1836,9 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf331d197c2361e922fbd67e3402d4c94df2b44768337fe612e6fe84618c88b" +checksum = "467d8eafd735ab1e0db7bf6a6a8b5bcf4c31a56c0cd7f80cba1932d4bb984b12" dependencies = [ "miden-crypto", "miden-debug-types", @@ -1852,20 +1849,19 @@ dependencies = [ [[package]] name = "miden-utils-indexing" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda3bea085c3f32230a177c170497ae7d069bf4b9a02712e61dc5c7ae2ba19e8" +checksum = "bc42cfa3aef68d21238b3ce4c2db00a1278f8075ef492c23c035ab6c75774790" dependencies = [ "miden-crypto", - "serde", "thiserror", ] [[package]] name = "miden-utils-sync" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e6ff341b5fee89fad3e35d8cedc4480e6c254fd51d68c526cbb9273147d8c0" +checksum = "b7e09bb239449e63e9a81f9b4ca5db1762327f44fb50777527fdba6fdbcab890" dependencies = [ "lock_api", "loom", @@ -1875,9 +1871,9 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7bf1405c5322f3a9f96e126d3b4c2f0f042118329f42fbb2a6e96717c96f16" +checksum = "fbb4d3120e2c9cce41b5dac7507cd86154951938b9effbc322c57983065bfa4a" dependencies = [ "bincode", "miden-air", @@ -2592,9 +2588,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -2645,9 +2641,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2658,6 +2654,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.2" @@ -3197,7 +3199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", @@ -3282,9 +3284,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "pin-project-lite", "tokio-macros", @@ -3325,28 +3327,19 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.3+spec-1.1.0" +version = "1.0.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "8825697d11e3935e3ab440a9d672022e540d016ff2f193de4295d11d18244774" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -3358,12 +3351,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] @@ -3563,9 +3556,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "js-sys", "wasm-bindgen", @@ -3772,9 +3765,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] diff --git a/crates/miden-protocol/src/transaction/inputs/mod.rs b/crates/miden-protocol/src/transaction/inputs/mod.rs index d483fc657f..c8e8d75777 100644 --- a/crates/miden-protocol/src/transaction/inputs/mod.rs +++ b/crates/miden-protocol/src/transaction/inputs/mod.rs @@ -3,7 +3,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::fmt::Debug; -use miden_crypto::merkle::smt::{LeafIndex, SmtLeaf, SmtProof}; +use miden_crypto::merkle::smt::{SmtLeaf, SmtProof}; use miden_crypto::merkle::{MerkleError, NodeIndex}; use super::PartialBlockchain; @@ -44,7 +44,6 @@ pub use account::AccountInputs; mod notes; pub use notes::{InputNote, InputNotes, ToInputNoteCommitments}; -use crate::crypto::merkle::smt::SMT_DEPTH; use crate::vm::AdviceInputs; // TRANSACTION INPUTS @@ -262,7 +261,7 @@ impl TransactionInputs { .map .get(&merkle_node) .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; - let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, leaf_index)?; + let smt_leaf = SmtLeaf::try_from_elements(smt_leaf_elements, leaf_index)?; // Construct SMT proof and witness. let smt_proof = SmtProof::new(sparse_path, smt_leaf)?; @@ -297,7 +296,7 @@ impl TransactionInputs { .map .get(&merkle_node) .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; - let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, smt_index)?; + let smt_leaf = SmtLeaf::try_from_elements(smt_leaf_elements, smt_index)?; // Construct SMT proof and witness. let smt_proof = SmtProof::new(sparse_path, smt_leaf)?; @@ -352,7 +351,7 @@ impl TransactionInputs { .map .get(&merkle_node) .ok_or(TransactionInputsExtractionError::MissingVaultRoot)?; - let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, smt_index)?; + let smt_leaf = SmtLeaf::try_from_elements(smt_leaf_elements, smt_index)?; // Find the asset in the SMT leaf let asset = smt_leaf @@ -537,58 +536,6 @@ impl Deserializable for TransactionInputs { // HELPER FUNCTIONS // ================================================================================================ -// TODO(sergerad): Move this fn to crypto SmtLeaf::try_from_elements. -pub fn smt_leaf_from_elements( - elements: &[Felt], - leaf_index: LeafIndex, -) -> Result { - use miden_crypto::merkle::smt::SmtLeaf; - - // Based on the miden-crypto SMT leaf serialization format. - - if elements.is_empty() { - return Ok(SmtLeaf::new_empty(leaf_index)); - } - - // Elements should be organized into a contiguous array of K/V Words (4 Felts each). - if !elements.len().is_multiple_of(8) { - return Err(TransactionInputsExtractionError::LeafConversionError( - "invalid SMT leaf format: elements length must be divisible by 8".into(), - )); - } - - let num_entries = elements.len() / 8; - - if num_entries == 1 { - // Single entry. - let key = Word::new([elements[0], elements[1], elements[2], elements[3]]); - let value = Word::new([elements[4], elements[5], elements[6], elements[7]]); - Ok(SmtLeaf::new_single(key, value)) - } else { - // Multiple entries. - let mut entries = Vec::with_capacity(num_entries); - // Read k/v pairs from each entry. - for i in 0..num_entries { - let base_idx = i * 8; - let key = Word::new([ - elements[base_idx], - elements[base_idx + 1], - elements[base_idx + 2], - elements[base_idx + 3], - ]); - let value = Word::new([ - elements[base_idx + 4], - elements[base_idx + 5], - elements[base_idx + 6], - elements[base_idx + 7], - ]); - entries.push((key, value)); - } - let leaf = SmtLeaf::new_multiple(entries)?; - Ok(leaf) - } -} - /// Validates whether the provided note belongs to the note tree of the specified block. fn validate_is_in_block( note: &Note, From a7ae72bea9c6d99e92d7afe6a137e5356e208bc9 Mon Sep 17 00:00:00 2001 From: igamigo Date: Fri, 6 Mar 2026 20:56:07 -0300 Subject: [PATCH 21/56] feat: add ability to submit user batches for the MockChain (#2565) --- CHANGELOG.md | 1 + crates/miden-testing/src/mock_chain/chain.rs | 38 +++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fddeba6bbe..c8a4a7d8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Added `DEFAULT_TAG` constant to `miden::standards::note_tag` MASM module ([#2482](https://github.com/0xMiden/miden-base/pull/2482)). - Added `NoteExecutionHint` variant constants (`NONE`, `ALWAYS`, `AFTER_BLOCK`, `ON_BLOCK_SLOT`) to `miden::standards::note::execution_hint` MASM module ([#2493](https://github.com/0xMiden/miden-base/pull/2493)). - Added `CodeBuilder::with_warnings_as_errors()` to promote assembler warning diagnostics to errors ([#2558](https://github.com/0xMiden/protocol/pull/2558)). +- Added `MockChain::add_pending_batch()` to allow submitting user batches directly ([#2565](https://github.com/0xMiden/protocol/pull/2565)). ### Changes diff --git a/crates/miden-testing/src/mock_chain/chain.rs b/crates/miden-testing/src/mock_chain/chain.rs index 7d596c95b1..99abe9899a 100644 --- a/crates/miden-testing/src/mock_chain/chain.rs +++ b/crates/miden-testing/src/mock_chain/chain.rs @@ -187,6 +187,9 @@ pub struct MockChain { /// block. pending_transactions: Vec, + /// Batches that have been submitted to the chain but have not yet been included in a block. + pending_batches: Vec, + /// NoteID |-> MockChainNote mapping to simplify note retrieval. committed_notes: BTreeMap, @@ -244,6 +247,7 @@ impl MockChain { nullifier_tree: NullifierTree::default(), account_tree, pending_transactions: Vec::new(), + pending_batches: Vec::new(), committed_notes: BTreeMap::new(), committed_accounts: BTreeMap::new(), account_authenticators, @@ -870,6 +874,14 @@ impl MockChain { self.pending_transactions.push(transaction); } + /// Adds the given [`ProvenBatch`] to the list of pending batches. + /// + /// A block has to be created to apply the batch effects to the chain state, e.g. using + /// [`MockChain::prove_next_block`]. + pub fn add_pending_batch(&mut self, batch: ProvenBatch) { + self.pending_batches.push(batch); + } + // PRIVATE HELPERS // ---------------------------------------------------------------------------------------- @@ -996,7 +1008,8 @@ impl MockChain { // Create batches from pending transactions. // ---------------------------------------------------------------------------------------- - let batches = self.pending_transactions_to_batches()?; + let mut batches = self.pending_transactions_to_batches()?; + batches.extend(core::mem::take(&mut self.pending_batches)); // Create block. // ---------------------------------------------------------------------------------------- @@ -1073,6 +1086,7 @@ impl Deserializable for MockChain { nullifier_tree, account_tree, pending_transactions, + pending_batches: Vec::new(), committed_notes, committed_accounts, account_authenticators, @@ -1349,4 +1363,26 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn add_pending_batch() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let account = builder.add_existing_mock_account(Auth::IncrNonce)?; + let mut chain = builder.build()?; + + // Execute a noop transaction and create a batch from it. + let tx = chain.build_tx_context(account.id(), &[], &[])?.build()?.execute().await?; + let proven_tx = LocalTransactionProver::default().prove_dummy(tx)?; + let proposed_batch = chain.propose_transaction_batch(vec![proven_tx])?; + let proven_batch = chain.prove_transaction_batch(proposed_batch)?; + + // Submit the batch directly and prove the block. + let num_blocks_before = chain.proven_blocks().len(); + chain.add_pending_batch(proven_batch); + chain.prove_next_block()?; + + assert_eq!(chain.proven_blocks().len(), num_blocks_before + 1); + + Ok(()) + } } From 1105a93117dded0d0749c35fe7655bb0f0119859 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Fri, 6 Mar 2026 16:00:41 -0800 Subject: [PATCH 22/56] chore: Explicitly use `get_native_account_active_storage_slots_ptr` in `account::set_item` and `account::set_map_item` --- CHANGELOG.md | 1 + .../asm/kernels/transaction/lib/account.masm | 15 ++++++++------- .../kernels/transaction/lib/account_delta.masm | 2 +- .../src/kernel_tests/tx/test_account.rs | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a4a7d8c1..d5bf34ab8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ - [BREAKING] Reverse the order of the transaction summary on the stack ([#2512](https://github.com/0xMiden/miden-base/pull/2512)). - [BREAKING] Use `@auth_script` MASM attribute instead of `auth_` prefix to identify authentication procedures in account components ([#2534](https://github.com/0xMiden/protocol/pull/2534)). - [BREAKING] Changed `TransactionId` to include fee asset in hash computation, making it commit to entire `TransactionHeader` contents. +- Explicitly use `get_native_account_active_storage_slots_ptr` in `account::set_item` and `account::set_map_item`. ## 0.13.3 (2026-01-27) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index b6e4f399c5..efa5408911 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -487,7 +487,7 @@ pub proc get_initial_item # => [INIT_VALUE] end -#! Sets an item in the account storage. +#! Sets an item in the account storage of the native account. #! #! Inputs: [slot_id_suffix, slot_id_prefix, VALUE] #! Outputs: [OLD_VALUE] @@ -505,7 +505,7 @@ pub proc set_item emit.ACCOUNT_STORAGE_BEFORE_SET_ITEM_EVENT # => [slot_id_suffix, slot_id_prefix, VALUE] - exec.memory::get_account_active_storage_slots_section_ptr + exec.memory::get_native_account_active_storage_slots_ptr # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, VALUE] exec.find_storage_slot @@ -580,7 +580,8 @@ pub proc get_initial_map_item exec.get_map_item_raw end -#! Stores NEW_VALUE under the specified KEY within the map contained in the given account storage slot. +#! Stores NEW_VALUE under the specified KEY within the map contained in the specified storage slot +#! of the native account. #! #! Inputs: [slot_id_suffix, slot_id_prefix, KEY, NEW_VALUE] #! Outputs: [OLD_VALUE] @@ -598,7 +599,7 @@ end #! - the storage slot type is not map. #! - no map with the root of the slot is found. pub proc set_map_item - exec.memory::get_account_active_storage_slots_section_ptr + exec.memory::get_native_account_active_storage_slots_ptr # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, KEY, NEW_VALUE] # resolve the slot name to its pointer @@ -618,7 +619,7 @@ pub proc set_map_item # => [OLD_VALUE] end -#! Returns the type of the storage slot at the provided index. +#! Returns the type of the storage slot at the provided index for the native account. #! #! WARNING: The index must be in bounds. #! @@ -628,7 +629,7 @@ end #! Where: #! - index is the location in memory of the storage slot. #! - slot_type is the type of the storage slot. -pub proc get_storage_slot_type +pub proc get_native_storage_slot_type # convert the index into a memory offset mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH # => [offset] @@ -1249,7 +1250,7 @@ pub proc insert_new_storage sub.1 # => [slot_idx] - dup exec.get_storage_slot_type + dup exec.get_native_storage_slot_type # => [slot_type, slot_idx] push.STORAGE_SLOT_TYPE_MAP eq 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 99c870677f..7b9f7d6762 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm @@ -144,7 +144,7 @@ end #! Inputs: [slot_idx, RATE0, RATE1, CAPACITY] #! Outputs: [RATE0, RATE1, CAPACITY] proc update_slot_delta - dup exec.account::get_storage_slot_type + dup exec.account::get_native_storage_slot_type # => [storage_slot_type, slot_idx, RATE0, RATE1, CAPACITY] # check if slot is of type value 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 8d1dfe5559..41611251b6 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -467,7 +467,7 @@ async fn test_get_map_item() -> anyhow::Result<()> { } #[tokio::test] -async fn test_get_storage_slot_type() -> anyhow::Result<()> { +async fn test_get_native_storage_slot_type() -> anyhow::Result<()> { for slot_name in [ AccountStorage::mock_value_slot0().name(), AccountStorage::mock_value_slot1().name(), @@ -495,7 +495,7 @@ async fn test_get_storage_slot_type() -> anyhow::Result<()> { push.{slot_idx} # get the type of the respective storage slot - exec.account::get_storage_slot_type + exec.account::get_native_storage_slot_type # truncate the stack swap drop From e04970bb8147e1d75a7939b72fba76c4ae53f3fa Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Sat, 7 Mar 2026 10:14:41 -0800 Subject: [PATCH 23/56] chore: fix typos --- README.md | 22 +++++++++++----------- crates/miden-protocol/src/note/metadata.rs | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8fd2a2178a..0f5a0fbe68 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,15 @@ Miden is currently on release v0.13. This is an early version of the protocol an ### Feature highlights - **Private accounts**. The Miden Operator tracks only commitments to account data in the public database. The users are responsible for keeping track of the state of their accounts. -- **Public accounts**. With public accounts users are be able to store the entire state of their accounts on-chain, thus, eliminating the need to keep track of account states locally (albeit by sacrificing privacy and at a higher cost). +- **Public accounts**. With public accounts, users are able to store the entire state of their accounts on-chain, thus, eliminating the need to keep track of account states locally (albeit by sacrificing privacy and at a higher cost). - **Private notes**. Like with private accounts, the Miden Operator tracks only commitments to notes in the public database. Users need to communicate note details to each other via side channels. -- **Public notes**. With public notes, the users are be able to store all note details on-chain, thus, eliminating the need to communicate note details via side-channels. +- **Public notes**. With public notes, users are able to store all note details on-chain, thus, eliminating the need to communicate note details via side-channels. - **Local transactions**. Users can execute and prove transactions locally on their devices. The Miden Operator verifies the proofs and if the proofs are valid, updates the state of the rollup accordingly. - **Standard account**. Users can create accounts using a small number of standard account interfaces (e.g., basic wallet). In the future, the set of standard smart contracts will be expanded. -- **Standard notes**. Can create notes using standardized note scripts such as Pay-to-ID (`P2ID`) and atomic swap (`SWAP`). In the future, the set of standardized notes will be expanded. -- **Delegated note inclusion proofs**. By delegating note inclusion proofs, users can create chains of dependent notes which are included into a block as a single batch. +- **Standard notes**. Users can create notes using standardized note scripts such as Pay-to-ID (`P2ID`) and atomic swap (`SWAP`). In the future, the set of standardized notes will be expanded. +- **Delegated note inclusion proofs**. By delegating note inclusion proofs, users can create chains of dependent transactions which are included into a block as a single batch. - **Transaction recency conditions**. Users are able to specify how close to the chain tip their transactions are to be executed. This enables things like rate limiting and oracles. -- **Network transactions**. Users will be able to create notes intended for network execution. Such notes will be included into transactions executed and proven by the Miden operator. +- **Network transactions**. Users are able to create notes intended for network execution. Such notes are included into transactions executed and proven by the Miden operator. ### Planned features @@ -44,12 +44,12 @@ Miden is currently on release v0.13. This is an early version of the protocol an ## Project structure -| Crate | Description | -| ------------------------------- | ------------------------------------------------------------------------------- | -| [miden-protocol](crates/miden-protocol) | Contains core components defining the Miden protocol, including the transaction kernel. | -| [miden-standards](crates/miden-standards) | Contains the code of Miden's standardized smart contracts. | -| [miden-tx](crates/miden-tx) | Contains tool for creating, executing, and proving Miden rollup transaction. | -| [bench-tx](bin/bench-tx) | Contains transaction execution and proving benchmarks. | +| Crate | Description | +| ----------------------------------------- | --------------------------------------------------------------------------------------- | +| [miden-protocol](crates/miden-protocol) | Contains core components defining the Miden protocol, including the transaction kernel. | +| [miden-standards](crates/miden-standards) | Contains the code of Miden's standardized smart contracts. | +| [miden-tx](crates/miden-tx) | Contains tools for creating, executing, and proving Miden rollup transactions. | +| [bench-tx](bin/bench-tx) | Contains transaction execution and proving benchmarks. | ## Make commands diff --git a/crates/miden-protocol/src/note/metadata.rs b/crates/miden-protocol/src/note/metadata.rs index bc0877df8b..04c36b9c08 100644 --- a/crates/miden-protocol/src/note/metadata.rs +++ b/crates/miden-protocol/src/note/metadata.rs @@ -43,8 +43,8 @@ use crate::note::{NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme}; /// The felt validity of each part of the layout is guaranteed: /// - 1st felt: The lower 8 bits of the account ID suffix are `0` by construction, so that they can /// be overwritten with other data. The suffix' most significant bit must be zero such that the -/// entire felt retains its validity even if all of its lower 8 bits are be set to `1`. So the -/// note type can be comfortably encoded. +/// entire felt retains its validity even if all of its lower 8 bits are set to `1`. So the note +/// type can be comfortably encoded. /// - 2nd felt: Is equivalent to the prefix of the account ID so it inherits its validity. /// - 3rd felt: The upper 32 bits are always zero. /// - 4th felt: The upper 30 bits are always zero. From 33440ac7283cc0f04807b84071c0321f5787cb22 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 9 Mar 2026 05:00:46 +0100 Subject: [PATCH 24/56] feat: integrate PSM contracts to AuthMultisig (#2527) --- CHANGELOG.md | 1 + .../asm/account_components/auth/multisig.masm | 598 +------------- .../account_components/auth/multisig_psm.masm | 37 + .../asm/standards/auth/multisig.masm | 732 ++++++++++++++++++ .../asm/standards/auth/psm.masm | 158 ++++ .../asm/standards/auth/tx_policy.masm | 80 ++ .../miden-standards/src/account/auth/mod.rs | 3 + .../src/account/auth/multisig.rs | 22 +- .../src/account/auth/multisig_psm.rs | 566 ++++++++++++++ .../src/account/auth/singlesig.rs | 3 + .../src/account/auth/singlesig_acl.rs | 6 + .../src/account/components/mod.rs | 20 + .../src/account/interface/component.rs | 15 +- .../src/account/interface/extension.rs | 5 + crates/miden-testing/src/mock_chain/auth.rs | 37 +- .../miden-testing/tests/agglayer/bridge_in.rs | 4 +- .../tests/agglayer/bridge_out.rs | 20 +- .../tests/agglayer/config_bridge.rs | 4 +- .../tests/agglayer/update_ger.rs | 4 +- .../tests/auth/hybrid_multisig.rs | 254 +----- crates/miden-testing/tests/auth/mod.rs | 2 + crates/miden-testing/tests/auth/multisig.rs | 327 +++++++- .../miden-testing/tests/auth/multisig_psm.rs | 531 +++++++++++++ 23 files changed, 2550 insertions(+), 879 deletions(-) create mode 100644 crates/miden-standards/asm/account_components/auth/multisig_psm.masm create mode 100644 crates/miden-standards/asm/standards/auth/multisig.masm create mode 100644 crates/miden-standards/asm/standards/auth/psm.masm create mode 100644 crates/miden-standards/asm/standards/auth/tx_policy.masm create mode 100644 crates/miden-standards/src/account/auth/multisig_psm.rs create mode 100644 crates/miden-testing/tests/auth/multisig_psm.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bf34ab8c..1bbc97a7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Resolve standard note scripts directly in `TransactionExecutorHost` instead of querying the data store ([#2417](https://github.com/0xMiden/miden-base/pull/2417)). - Added `DEFAULT_TAG` constant to `miden::standards::note_tag` MASM module ([#2482](https://github.com/0xMiden/miden-base/pull/2482)). - Added `NoteExecutionHint` variant constants (`NONE`, `ALWAYS`, `AFTER_BLOCK`, `ON_BLOCK_SLOT`) to `miden::standards::note::execution_hint` MASM module ([#2493](https://github.com/0xMiden/miden-base/pull/2493)). +- Added PSM authentication procedures and integrated them into `AuthMultisig` ([#2527](https://github.com/0xMiden/protocol/pull/2527)). - Added `CodeBuilder::with_warnings_as_errors()` to promote assembler warning diagnostics to errors ([#2558](https://github.com/0xMiden/protocol/pull/2558)). - Added `MockChain::add_pending_batch()` to allow submitting user batches directly ([#2565](https://github.com/0xMiden/protocol/pull/2565)). diff --git a/crates/miden-standards/asm/account_components/auth/multisig.masm b/crates/miden-standards/asm/account_components/auth/multisig.masm index 99b7195a61..5e698bc886 100644 --- a/crates/miden-standards/asm/account_components/auth/multisig.masm +++ b/crates/miden-standards/asm/account_components/auth/multisig.masm @@ -2,605 +2,27 @@ # # See the `AuthMultisig` Rust type's documentation for more details. -use miden::protocol::active_account -use miden::protocol::auth::AUTH_UNAUTHORIZED_EVENT -use miden::protocol::native_account -use miden::standards::auth -use miden::core::word +use miden::standards::auth::multisig -# Local Memory Addresses -const IS_SIGNER_FOUND_LOC=0 -const CURRENT_SIGNER_INDEX_LOC=1 +pub use multisig::update_signers_and_threshold +pub use multisig::get_threshold_and_num_approvers +pub use multisig::set_procedure_threshold +pub use multisig::get_signer_at +pub use multisig::is_signer -const NEW_NUM_OF_APPROVERS_LOC=0 -const INIT_NUM_OF_APPROVERS_LOC=1 - -const DEFAULT_THRESHOLD_LOC=0 - -# CONSTANTS -# ================================================================================================= - -# Storage Slots -# -# This authentication component uses named storage slots. -# - THRESHOLD_CONFIG_SLOT: -# [default_threshold, num_approvers, 0, 0] -# -# - APPROVER_PUBLIC_KEYS_SLOT (map): -# APPROVER_MAP_KEY => APPROVER_PUBLIC_KEY -# where APPROVER_MAP_KEY = [key_index, 0, 0, 0] -# -# - APPROVER_SCHEME_ID_SLOT (map): -# APPROVER_MAP_KEY => [scheme_id, 0, 0, 0] -# where APPROVER_MAP_KEY = [key_index, 0, 0, 0] -# -# - EXECUTED_TXS_SLOT (map): -# TRANSACTION_MESSAGE => [is_executed, 0, 0, 0] -# -# - PROC_THRESHOLD_ROOTS_SLOT (map): -# PROC_ROOT => [proc_threshold, 0, 0, 0] - - -# The slot in this component's storage layout where the default signature threshold and -# number of approvers are stored as: -# [default_threshold, num_approvers, 0, 0]. -# The threshold is guaranteed to be less than or equal to num_approvers. -const THRESHOLD_CONFIG_SLOT = word("miden::standards::auth::multisig::threshold_config") - -# The slot in this component's storage layout where the public keys map is stored. -# Map entries: [key_index, 0, 0, 0] => APPROVER_PUBLIC_KEY -const APPROVER_PUBLIC_KEYS_SLOT = word("miden::standards::auth::multisig::approver_public_keys") - -# The slot in this component's storage layout where the scheme id for the corresponding public keys map is stored. -# Map entries: [key_index, 0, 0, 0] => [scheme_id, 0, 0, 0] -const APPROVER_SCHEME_ID_SLOT = word("miden::standards::auth::multisig::approver_schemes") - -# The slot in this component's storage layout where executed transactions are stored. -# Map entries: transaction_message => [is_executed, 0, 0, 0] -const EXECUTED_TXS_SLOT = word("miden::standards::auth::multisig::executed_transactions") - -# The slot in this component's storage layout where procedure thresholds are stored. -# Map entries: PROC_ROOT => [proc_threshold, 0, 0, 0] -const PROC_THRESHOLD_ROOTS_SLOT = word("miden::standards::auth::multisig::procedure_thresholds") - -# Executed Transaction Flag Constant -const IS_EXECUTED_FLAG = [1, 0, 0, 0] - -# ERRORS -# ================================================================================================= - -const ERR_TX_ALREADY_EXECUTED = "failed to approve multisig transaction as it was already executed" - -const ERR_MALFORMED_MULTISIG_CONFIG = "number of approvers must be equal to or greater than threshold" - -const ERR_ZERO_IN_MULTISIG_CONFIG = "number of approvers or threshold must not be zero" - -const ERR_APPROVER_COUNTS_NOT_U32 = "initial and new number of approvers must be u32" - -const ERR_SIGNER_INDEX_NOT_U32 = "signer index must be u32" - -#! Check if transaction has already been executed and add it to executed transactions for replay protection. -#! -#! Inputs: [MSG] -#! Outputs: [] -#! -#! Panics if: -#! - the same transaction has already been executed -proc assert_new_tx(msg: word) - push.IS_EXECUTED_FLAG - # => [[is_executed, 0, 0, 0], MSG] - - swapw - # => [MSG, IS_EXECUTED_FLAG] - - push.EXECUTED_TXS_SLOT[0..2] - # => [txs_slot_suffix, txs_slot_prefix, MSG, IS_EXECUTED_FLAG] - - # Set the key value pair in the map to mark transaction as executed - exec.native_account::set_map_item - # => [[is_executed, 0, 0, 0]] - - movdn.3 drop drop drop - # => [is_executed] - - assertz.err=ERR_TX_ALREADY_EXECUTED - # => [] -end - -#! Remove old approver public keys and the corresponding scheme ids -#! from the approver public key and scheme id mappings. -#! -#! This procedure cleans up the storage by removing public keys and signature scheme ids of approvers -#! that are no longer part of the multisig configuration. -#! -#! Inputs: [init_num_of_approvers, new_num_of_approvers] -#! Outputs: [] -#! -#! Where: -#! - init_num_of_approvers is the original number of approvers before the update -#! - new_num_of_approvers is the new number of approvers after the update -#! -#! Panics if: -#! - init_num_of_approvers is not a u32 value. -#! - new_num_of_approvers is not a u32 value. -proc cleanup_pubkey_and_scheme_id_mapping(init_num_of_approvers: u32, new_num_of_approvers: u32) - dup.1 dup.1 - u32assert2.err=ERR_APPROVER_COUNTS_NOT_U32 - u32lt - # => [should_loop, i = init_num_of_approvers, new_num_of_approvers] - - while.true - # => [i, new_num_of_approvers] - - sub.1 - # => [i-1, new_num_of_approvers] - - # clear scheme id at APPROVER_MAP_KEY(i-1) - dup exec.create_approver_map_key - # => [APPROVER_MAP_KEY, i-1, new_num_of_approvers] - - padw swapw - # => [APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] - - push.APPROVER_SCHEME_ID_SLOT[0..2] - # => [scheme_id_slot_suffix, scheme_id_slot_prefix, APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] - - exec.native_account::set_map_item - # => [OLD_VALUE, i-1, new_num_of_approvers] - - dropw - # => [i-1, new_num_of_approvers] - - # clear public key at APPROVER_MAP_KEY(i-1) - dup exec.create_approver_map_key - # => [APPROVER_MAP_KEY, i-1, new_num_of_approvers] - - padw swapw - # => [APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] - - push.APPROVER_PUBLIC_KEYS_SLOT[0..2] - # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] - - exec.native_account::set_map_item - # => [OLD_VALUE, i-1, new_num_of_approvers] - - dropw - # => [i-1, new_num_of_approvers] - - dup.1 dup.1 - u32lt - # => [should_loop, i-1, new_num_of_approvers] - end - - drop drop -end - -#! Builds the storage map key for a signer index. -#! -#! Inputs: [key_index] -#! Outputs: [APPROVER_MAP_KEY] -proc create_approver_map_key - push.0.0.0 movup.3 - # => [[key_index, 0, 0, 0]] - # => [APPROVER_MAP_KEY] -end - -#! Update threshold config, add / remove approvers, -#! and update the approver scheme ids -#! -#! Inputs: -#! Operand stack: [MULTISIG_CONFIG_HASH, pad(12)] -#! Advice map: { -#! MULTISIG_CONFIG_HASH => -#! [ -#! CONFIG, -#! PUB_KEY_N, PUB_KEY_N-1, ..., PUB_KEY_0, -#! SCHEME_ID_N, SCHEME_ID_N-1, ..., SCHEME_ID_0 -#! ] -#! } -#! Outputs: -#! Operand stack: [] -#! -#! Where: -#! - MULTISIG_CONFIG_HASH is the hash of the threshold, -#! new public key vector, and the corresponding scheme identifiers -#! - MULTISIG_CONFIG is [threshold, num_approvers, 0, 0] -#! - PUB_KEY_i is the public key of the i-th signer -#! - SCHEME_ID_i is the signature scheme id of the i-th signer -#! -#! Locals: -#! 0: new_num_of_approvers -#! 1: init_num_of_approvers -#! -#! Invocation: call -@locals(2) -pub proc update_signers_and_threshold(multisig_config_hash: word) - adv.push_mapval - # => [MULTISIG_CONFIG_HASH, pad(12)] - - adv_loadw - # => [MULTISIG_CONFIG, pad(12)] - - # store new_num_of_approvers for later - dup.1 loc_store.NEW_NUM_OF_APPROVERS_LOC - # => [MULTISIG_CONFIG, pad(12)] - - dup dup.2 - # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] - - # make sure that the threshold is smaller than the number of approvers - u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG - u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG - # => [MULTISIG_CONFIG, pad(12)] - - dup dup.2 - # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] - - # make sure that threshold or num_approvers are not zero - eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG - eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG - # => [MULTISIG_CONFIG, pad(12)] - - push.THRESHOLD_CONFIG_SLOT[0..2] - # => [config_slot_suffix, config_slot_prefix, MULTISIG_CONFIG, pad(12)] - - exec.native_account::set_item - # => [OLD_THRESHOLD_CONFIG, pad(12)] - - # store init_num_of_approvers for later - drop loc_store.INIT_NUM_OF_APPROVERS_LOC drop drop - # => [pad(12)] - - loc_load.NEW_NUM_OF_APPROVERS_LOC - # => [num_approvers] - - dup neq.0 - while.true - sub.1 - # => [i-1, pad(12)] - - dup exec.create_approver_map_key - # => [APPROVER_MAP_KEY, i-1, pad(12)] - - padw adv_loadw - # => [PUB_KEY, APPROVER_MAP_KEY, i-1, pad(12)] - - swapw - # => [APPROVER_MAP_KEY, PUB_KEY, i-1, pad(12)] - - push.APPROVER_PUBLIC_KEYS_SLOT[0..2] - # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, PUB_KEY, i-1, pad(12)] - - exec.native_account::set_map_item - # => [OLD_VALUE, i-1, pad(12)] - - # override OLD_VALUE with SCHEME_ID_WORD - adv_loadw - # => [SCHEME_ID_WORD, i-1, pad(12)] - - # validate the scheme id word is in a correct form - exec.auth::signature::assert_supported_scheme_word - # => [SCHEME_ID_WORD, i-1, pad(12)] - - dup.4 exec.create_approver_map_key - # => [APPROVER_MAP_KEY, SCHEME_ID_WORD, i-1, pad(12)] - - push.APPROVER_SCHEME_ID_SLOT[0..2] - # => [scheme_id_slot_id_suffix, scheme_id_slot_id_prefix, APPROVER_MAP_KEY, SCHEME_ID_WORD, i-1, pad(12)] - - exec.native_account::set_map_item - # => [OLD_VALUE, i-1, pad(12)] - - dropw - # => [i-1, pad(12)] - - dup neq.0 - # => [is_non_zero, i-1, pad(12)] - end - # => [pad(13)] - - drop - # => [pad(12)] - - # compare initial vs current multisig config - - # load init_num_of_approvers & new_num_of_approvers - loc_load.NEW_NUM_OF_APPROVERS_LOC loc_load.INIT_NUM_OF_APPROVERS_LOC - # => [init_num_of_approvers, new_num_of_approvers, pad(12)] - - exec.cleanup_pubkey_and_scheme_id_mapping - # => [pad(12)] -end - -# Computes the effective transaction threshold based on called procedures and per-procedure -# overrides stored in PROC_THRESHOLD_ROOTS_SLOT. Falls back to default_threshold if no -# overrides apply. -# -#! Inputs: [default_threshold] -#! Outputs: [transaction_threshold] -@locals(1) -proc compute_transaction_threshold(default_threshold: u32) -> u32 - # 1. initialize transaction_threshold = 0 - # 2. iterate through all account procedures - # a. check if the procedure was called during the transaction - # b. if called, get the override threshold of that procedure from the config map - # c. if proc_threshold > transaction_threshold, set transaction_threshold = proc_threshold - # 3. if transaction_threshold == 0 at the end, revert to using default_threshold - - # store default_threshold for later - loc_store.DEFAULT_THRESHOLD_LOC - # => [] - - # 1. initialize transaction_threshold = 0 - push.0 - # => [transaction_threshold] - - # get the number of account procedures - exec.active_account::get_num_procedures - # => [num_procedures, transaction_threshold] - - # 2. iterate through all account procedures - dup neq.0 - # => [should_continue, num_procedures, transaction_threshold] - while.true - sub.1 dup - # => [num_procedures-1, num_procedures-1, transaction_threshold] - - # get procedure root of the procedure with index i - exec.active_account::get_procedure_root dupw - # => [PROC_ROOT, PROC_ROOT, num_procedures-1, transaction_threshold] - - # 2a. check if this procedure has been called in the transaction - exec.native_account::was_procedure_called - # => [was_called, PROC_ROOT, num_procedures-1, transaction_threshold] - - # if it has been called, get the override threshold of that procedure - if.true - # => [PROC_ROOT, num_procedures-1, transaction_threshold] - - push.PROC_THRESHOLD_ROOTS_SLOT[0..2] - # => [proc_roots_slot_suffix, proc_roots_slot_prefix, PROC_ROOT, num_procedures-1, transaction_threshold] - - # 2b. get the override proc_threshold of that procedure - # if the procedure has no override threshold, the returned map item will be [0, 0, 0, 0] - exec.active_account::get_initial_map_item - # => [[proc_threshold, 0, 0, 0], num_procedures-1, transaction_threshold] - - movdn.3 drop drop drop dup dup.3 - # => [transaction_threshold, proc_threshold, proc_threshold, num_procedures-1, transaction_threshold] - - u32assert2.err="transaction threshold or procedure threshold are not u32" - u32gt - # => [is_gt, proc_threshold, num_procedures-1, transaction_threshold] - # 2c. if proc_threshold > transaction_threshold, update transaction_threshold - movup.2 movdn.3 - # => [is_gt, proc_threshold, transaction_threshold, num_procedures-1] - cdrop - # => [updated_transaction_threshold, num_procedures-1] - swap - # => [num_procedures-1, updated_transaction_threshold] - # if it has not been called during this transaction, nothing to do, move to the next procedure - else - dropw - # => [num_procedures-1, transaction_threshold] - end - - dup neq.0 - # => [should_continue, num_procedures-1, transaction_threshold] - end - - drop - # => [transaction_threshold] - - loc_load.DEFAULT_THRESHOLD_LOC - # => [default_threshold, transaction_threshold] - - # 3. if transaction_threshold == 0 at the end, revert to using default_threshold - dup.1 eq.0 - # => [is_zero, default_threshold, transaction_threshold] - - cdrop - # => [effective_transaction_threshold] -end - -#! Returns current num_approvers and the threshold `THRESHOLD_CONFIG_SLOT` -#! Inputs: [] -#! Outputs: [threshold, num_approvers] -#! -#! Invocation: call -pub proc get_threshold_and_num_approvers - push.THRESHOLD_CONFIG_SLOT[0..2] - exec.active_account::get_initial_item - # => [threshold, num_approvers, 0, 0] - - movup.2 drop movup.2 drop - # => [threshold, num_approvers] -end - -#! Returns signer public key at index i -#! -#! Inputs: [index] -#! Outputs: [PUB_KEY] -#! -#! Panics if: -#! - index is not a u32 value. -#! -#! Invocation: call -pub proc get_signer_at - u32assert.err=ERR_SIGNER_INDEX_NOT_U32 - # => [index] - - exec.create_approver_map_key - # => [APPROVER_MAP_KEY] - - push.APPROVER_PUBLIC_KEYS_SLOT[0..2] - # => [APPROVER_PUBLIC_KEYS_SLOT, APPROVER_MAP_KEY] - - exec.active_account::get_initial_map_item - # => [PUB_KEY] -end - - -#! Returns 1 if PUB_KEY is a current signer, else 0. -#! Inputs: [PUB_KEY] -#! Outputs: [is_signer] -#! Locals: -#! 0: is_signer_found -#! 1: current_signer_index -#! -#! Invocation: call -@locals(2) -pub proc is_signer(pub_key: word) -> felt - # initialize is_signer_found = false - push.0 loc_store.IS_SIGNER_FOUND_LOC - # => [PUB_KEY] - - exec.get_threshold_and_num_approvers - # => [threshold, num_approvers, PUB_KEY] - - drop - # => [num_approvers, PUB_KEY] - - dup neq.0 - # => [has_remaining_signers, num_approvers, PUB_KEY] - - while.true - # => [i, PUB_KEY] - - sub.1 - # => [i-1, PUB_KEY] - - # store i-1 for this loop iteration before map lookup - dup loc_store.CURRENT_SIGNER_INDEX_LOC - # => [i-1, PUB_KEY] - - exec.create_approver_map_key - # => [APPROVER_MAP_KEY, PUB_KEY] - - push.APPROVER_PUBLIC_KEYS_SLOT[0..2] - # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, PUB_KEY] - - exec.active_account::get_initial_map_item - # => [CURRENT_PUB_KEY, PUB_KEY] - - dupw.1 exec.word::eq - # => [is_pub_key_match, PUB_KEY] - - loc_store.IS_SIGNER_FOUND_LOC - # => [PUB_KEY] - - loc_load.CURRENT_SIGNER_INDEX_LOC - # => [i-1, PUB_KEY] - - dup neq.0 - # => [has_remaining_signers, i-1, PUB_KEY] - - loc_load.IS_SIGNER_FOUND_LOC not - # => [!is_signer_found, has_remaining_signers, i-1, PUB_KEY] - - and - # => [should_loop, i-1, PUB_KEY] - end - - drop dropw - # => [] - - loc_load.IS_SIGNER_FOUND_LOC - # => [is_signer] -end - - -#! Authenticate a transaction using the signature scheme specified by scheme_id -#! with multi-signature support -#! -#! Supported schemes: -#! - 1 => ecdsa_k256_keccak -#! - 2 => falcon512_poseidon2 -#! -#! This procedure implements multi-signature authentication by: -#! 1. Computing the transaction summary message that needs to be signed -#! 2. Verifying signatures from multiple required signers against their public keys -#! 3. Ensuring the minimum threshold of valid signatures is met -#! 4. Implementing replay protection by tracking executed transactions +#! Authenticate a transaction with multi-signature support. #! #! Inputs: #! Operand stack: [SALT] -#! Advice map: { -#! h(SIG_0, MSG): SIG_0, -#! h(SIG_1, MSG): SIG_1, -#! h(SIG_n, MSG): SIG_n -#! } #! Outputs: #! Operand stack: [] #! -#! Where: -#! - SALT is a cryptographically random nonce that enables multiple concurrent -#! multisig transactions while maintaining replay protection. Each transaction -#! must use a unique SALT value to ensure transaction uniqueness. -#! - SIG_i is the signature from the i-th signer. -#! - MSG is the transaction message being signed. -#! - h(SIG_i, MSG) is the hash of the signature and message used as the advice map key. -#! -#! Panics if: -#! - insufficient number of valid signatures (below threshold). -#! - the same transaction has already been executed (replay protection). -#! #! Invocation: call @auth_script -@locals(1) pub proc auth_tx_multisig(salt: word) - exec.native_account::incr_nonce drop - # => [SALT] - - # ------ Computing transaction summary ------ - - exec.auth::create_tx_summary - # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT] - - # insert tx summary into advice provider for extraction by the host - adv.insert_hqword - # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT] - - # the commitment to the tx summary is the message that is signed - exec.auth::hash_tx_summary + exec.multisig::auth_tx # => [TX_SUMMARY_COMMITMENT] - # ------ Verifying approver signatures ------ - - exec.get_threshold_and_num_approvers - # => [default_threshold, num_of_approvers, TX_SUMMARY_COMMITMENT] - - movdn.5 - # => [num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - - push.APPROVER_PUBLIC_KEYS_SLOT[0..2] - # => [pub_key_slot_suffix, pub_key_slot_prefix, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - - push.APPROVER_SCHEME_ID_SLOT[0..2] - # => [scheme_id_slot_suffix, scheme_id_slot_prefix, pub_key_slot_suffix, pub_key_slot_prefix, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] - - exec.::miden::standards::auth::signature::verify_signatures - # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, default_threshold] - - # ------ Checking threshold is >= num_verified_signatures ------ - - movup.5 - # => [default_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] - - exec.compute_transaction_threshold - # => [transaction_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] - - u32assert2 u32lt - # => [is_unauthorized, TX_SUMMARY_COMMITMENT] - - # If signatures are non-existent the tx will fail here. - if.true - emit.AUTH_UNAUTHORIZED_EVENT - push.0 assert.err="insufficient number of signatures" - end - - # ------ Writing executed transaction MSG to map ------ - - exec.assert_new_tx - # => [TX_SUMMARY_COMMITMENT] + exec.multisig::assert_new_tx + # => [] end diff --git a/crates/miden-standards/asm/account_components/auth/multisig_psm.masm b/crates/miden-standards/asm/account_components/auth/multisig_psm.masm new file mode 100644 index 0000000000..591ba376ab --- /dev/null +++ b/crates/miden-standards/asm/account_components/auth/multisig_psm.masm @@ -0,0 +1,37 @@ +# The MASM code of the Multi-Signature Authentication Component with Private State Manager. +# +# See the `AuthMultisigPsm` Rust type's documentation for more details. + +use miden::standards::auth::multisig +use miden::standards::auth::psm + +pub use multisig::update_signers_and_threshold +pub use multisig::get_threshold_and_num_approvers +pub use multisig::set_procedure_threshold +pub use multisig::get_signer_at +pub use multisig::is_signer + +pub use psm::update_psm_public_key + +#! Authenticate a transaction with multi-signature support and optional PSM verification. +#! +#! Inputs: +#! Operand stack: [SALT] +#! Outputs: +#! Operand stack: [] +#! +#! Invocation: call +@auth_script +pub proc auth_tx_multisig_psm(salt: word) + exec.multisig::auth_tx + # => [TX_SUMMARY_COMMITMENT] + + dupw + # => [TX_SUMMARY_COMMITMENT, TX_SUMMARY_COMMITMENT] + + exec.psm::verify_signature + # => [TX_SUMMARY_COMMITMENT] + + exec.multisig::assert_new_tx + # => [] +end diff --git a/crates/miden-standards/asm/standards/auth/multisig.masm b/crates/miden-standards/asm/standards/auth/multisig.masm new file mode 100644 index 0000000000..ed20ff2325 --- /dev/null +++ b/crates/miden-standards/asm/standards/auth/multisig.masm @@ -0,0 +1,732 @@ +# The MASM code of the Multi-Signature Authentication Component. +# +# See the `AuthMultisig` Rust type's documentation for more details. + +use miden::protocol::active_account +use miden::protocol::auth::AUTH_UNAUTHORIZED_EVENT +use miden::protocol::native_account +use miden::standards::auth +use miden::core::word + +# Local Memory Addresses +const IS_SIGNER_FOUND_LOC=0 +const CURRENT_SIGNER_INDEX_LOC=1 + +const NEW_NUM_OF_APPROVERS_LOC=0 +const INIT_NUM_OF_APPROVERS_LOC=1 + +const DEFAULT_THRESHOLD_LOC=0 + +# CONSTANTS +# ================================================================================================= + +# Storage Slots +# +# This authentication component uses named storage slots. +# - THRESHOLD_CONFIG_SLOT: +# [default_threshold, num_approvers, 0, 0] +# +# - APPROVER_PUBLIC_KEYS_SLOT (map): +# APPROVER_MAP_KEY => APPROVER_PUBLIC_KEY +# where APPROVER_MAP_KEY = [key_index, 0, 0, 0] +# +# - APPROVER_SCHEME_ID_SLOT (map): +# APPROVER_MAP_KEY => [scheme_id, 0, 0, 0] +# where APPROVER_MAP_KEY = [key_index, 0, 0, 0] +# +# - EXECUTED_TXS_SLOT (map): +# TRANSACTION_MESSAGE => [is_executed, 0, 0, 0] +# +# - PROC_THRESHOLD_ROOTS_SLOT (map): +# PROC_ROOT => [proc_threshold, 0, 0, 0] + + +# The slot in this component's storage layout where the default signature threshold and +# number of approvers are stored as: +# [default_threshold, num_approvers, 0, 0]. +# The threshold is guaranteed to be less than or equal to num_approvers. +const THRESHOLD_CONFIG_SLOT = word("miden::standards::auth::multisig::threshold_config") + +# The slot in this component's storage layout where the public keys map is stored. +# Map entries: [key_index, 0, 0, 0] => APPROVER_PUBLIC_KEY +const APPROVER_PUBLIC_KEYS_SLOT = word("miden::standards::auth::multisig::approver_public_keys") + +# The slot in this component's storage layout where the scheme id for the corresponding public keys map is stored. +# Map entries: [key_index, 0, 0, 0] => [scheme_id, 0, 0, 0] +const APPROVER_SCHEME_ID_SLOT = word("miden::standards::auth::multisig::approver_schemes") + +# The slot in this component's storage layout where executed transactions are stored. +# Map entries: transaction_message => [is_executed, 0, 0, 0] +const EXECUTED_TXS_SLOT = word("miden::standards::auth::multisig::executed_transactions") + +# The slot in this component's storage layout where procedure thresholds are stored. +# Map entries: PROC_ROOT => [proc_threshold, 0, 0, 0] +const PROC_THRESHOLD_ROOTS_SLOT = word("miden::standards::auth::multisig::procedure_thresholds") + +# Executed Transaction Flag Constant +const IS_EXECUTED_FLAG = [1, 0, 0, 0] + +# ERRORS +# ================================================================================================= + +const ERR_TX_ALREADY_EXECUTED = "failed to approve multisig transaction as it was already executed" + +const ERR_MALFORMED_MULTISIG_CONFIG = "number of approvers must be equal to or greater than threshold" + +const ERR_ZERO_IN_MULTISIG_CONFIG = "number of approvers or threshold must not be zero" + +const ERR_APPROVER_COUNTS_NOT_U32 = "initial and new number of approvers must be u32" + +const ERR_SIGNER_INDEX_NOT_U32 = "signer index must be u32" + +const ERR_PROC_THRESHOLD_NOT_U32 = "procedure threshold must be u32" + +const ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32 = "number of approvers and procedure threshold must be u32" + +const ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS = "procedure threshold exceeds new number of approvers" + +#! Remove old approver public keys and the corresponding scheme ids +#! from the approver public key and scheme id mappings. +#! +#! This procedure cleans up the storage by removing public keys and signature scheme ids of approvers +#! that are no longer part of the multisig configuration. +#! +#! Inputs: [init_num_of_approvers, new_num_of_approvers] +#! Outputs: [] +#! +#! Where: +#! - init_num_of_approvers is the original number of approvers before the update +#! - new_num_of_approvers is the new number of approvers after the update +#! +#! Panics if: +#! - init_num_of_approvers is not a u32 value. +#! - new_num_of_approvers is not a u32 value. +proc cleanup_pubkey_and_scheme_id_mapping(init_num_of_approvers: u32, new_num_of_approvers: u32) + dup.1 dup.1 + u32assert2.err=ERR_APPROVER_COUNTS_NOT_U32 + u32lt + # => [should_loop, i = init_num_of_approvers, new_num_of_approvers] + + while.true + # => [i, new_num_of_approvers] + + sub.1 + # => [i-1, new_num_of_approvers] + + # clear scheme id at APPROVER_MAP_KEY(i-1) + dup exec.create_approver_map_key + # => [APPROVER_MAP_KEY, i-1, new_num_of_approvers] + + padw swapw + # => [APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] + + push.APPROVER_SCHEME_ID_SLOT[0..2] + # => [scheme_id_slot_suffix, scheme_id_slot_prefix, APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] + + exec.native_account::set_map_item + # => [OLD_VALUE, i-1, new_num_of_approvers] + + dropw + # => [i-1, new_num_of_approvers] + + # clear public key at APPROVER_MAP_KEY(i-1) + dup exec.create_approver_map_key + # => [APPROVER_MAP_KEY, i-1, new_num_of_approvers] + + padw swapw + # => [APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] + + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, EMPTY_WORD, i-1, new_num_of_approvers] + + exec.native_account::set_map_item + # => [OLD_VALUE, i-1, new_num_of_approvers] + + dropw + # => [i-1, new_num_of_approvers] + + dup.1 dup.1 + u32lt + # => [should_loop, i-1, new_num_of_approvers] + end + + drop drop +end + +#! Builds the storage map key for a signer index. +#! +#! Inputs: [key_index] +#! Outputs: [APPROVER_MAP_KEY] +proc create_approver_map_key + push.0.0.0 movup.3 + # => [[key_index, 0, 0, 0]] + # => [APPROVER_MAP_KEY] +end + +#! Asserts that all configured per-procedure threshold overrides are less than or equal to +#! number of approvers +#! +#! Inputs: [num_approvers] +#! Outputs: [] +#! Panics if: +#! - any configured procedure threshold is not a u32 value. +#! - any configured procedure threshold exceeds num_approvers. +proc assert_proc_thresholds_lte_num_approvers(num_approvers: u32) + exec.active_account::get_num_procedures + # => [num_procedures, num_approvers] + + dup neq.0 + # => [should_continue, num_procedures, num_approvers] + while.true + sub.1 dup + # => [proc_index, proc_index, num_approvers] + + exec.active_account::get_procedure_root + # => [PROC_ROOT, proc_index, num_approvers] + + push.PROC_THRESHOLD_ROOTS_SLOT[0..2] + # => [proc_roots_slot_suffix, proc_roots_slot_prefix, PROC_ROOT, proc_index, num_approvers] + + exec.active_account::get_map_item + # => [[proc_threshold, 0, 0, 0], proc_index, num_approvers] + + movdn.3 drop drop drop + # => [proc_threshold, proc_index, num_approvers] + + dup.2 + # => [num_approvers, proc_threshold, proc_index, num_approvers] + + u32assert2.err=ERR_PROC_THRESHOLD_NOT_U32 + u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS + # => [proc_index, num_approvers] + + dup neq.0 + # => [should_continue, proc_index, num_approvers] + end + # => [proc_index, num_approvers] + + drop drop + # => [] +end + +#! Update threshold config, add & remove approvers, and update the approver scheme ids +#! +#! Inputs: +#! Operand stack: [MULTISIG_CONFIG_HASH, pad(12)] +#! Advice map: { +#! MULTISIG_CONFIG_HASH => +#! [ +#! CONFIG, +#! PUB_KEY_N, PUB_KEY_N-1, ..., PUB_KEY_0, +#! SCHEME_ID_N, SCHEME_ID_N-1, ..., SCHEME_ID_0 +#! ] +#! } +#! Outputs: +#! Operand stack: [] +#! +#! Where: +#! - MULTISIG_CONFIG_HASH is the hash of the threshold, +#! new public key vector, and the corresponding scheme identifiers +#! - MULTISIG_CONFIG is [threshold, num_approvers, 0, 0] +#! - PUB_KEY_i is the public key of the i-th signer +#! - SCHEME_ID_i is the signature scheme id of the i-th signer +#! +#! Locals: +#! 0: new_num_of_approvers +#! 1: init_num_of_approvers +#! +#! Invocation: call +@locals(2) +pub proc update_signers_and_threshold(multisig_config_hash: word) + adv.push_mapval + # => [MULTISIG_CONFIG_HASH, pad(12)] + + adv_loadw + # => [MULTISIG_CONFIG, pad(12)] + + # store new_num_of_approvers for later + dup.1 loc_store.NEW_NUM_OF_APPROVERS_LOC + # => [MULTISIG_CONFIG, pad(12)] + + dup dup.2 + # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] + + # make sure that the threshold is smaller than the number of approvers + u32assert2.err=ERR_MALFORMED_MULTISIG_CONFIG + u32gt assertz.err=ERR_MALFORMED_MULTISIG_CONFIG + # => [MULTISIG_CONFIG, pad(12)] + + dup dup.2 + # => [num_approvers, threshold, MULTISIG_CONFIG, pad(12)] + + # make sure that threshold or num_approvers are not zero + eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG + eq.0 assertz.err=ERR_ZERO_IN_MULTISIG_CONFIG + # => [MULTISIG_CONFIG, pad(12)] + + loc_load.NEW_NUM_OF_APPROVERS_LOC + # => [num_approvers, MULTISIG_CONFIG, pad(12)] + + # make sure that all existing procedure threshold overrides remain reachable + exec.assert_proc_thresholds_lte_num_approvers + # => [MULTISIG_CONFIG, pad(12)] + + push.THRESHOLD_CONFIG_SLOT[0..2] + # => [config_slot_suffix, config_slot_prefix, MULTISIG_CONFIG, pad(12)] + + exec.native_account::set_item + # => [OLD_THRESHOLD_CONFIG, pad(12)] + + # store init_num_of_approvers for later + drop loc_store.INIT_NUM_OF_APPROVERS_LOC drop drop + # => [pad(12)] + + loc_load.NEW_NUM_OF_APPROVERS_LOC + # => [num_approvers] + + dup neq.0 + while.true + sub.1 + # => [i-1, pad(12)] + + dup exec.create_approver_map_key + # => [APPROVER_MAP_KEY, i-1, pad(12)] + + padw adv_loadw + # => [PUB_KEY, APPROVER_MAP_KEY, i-1, pad(12)] + + swapw + # => [APPROVER_MAP_KEY, PUB_KEY, i-1, pad(12)] + + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, PUB_KEY, i-1, pad(12)] + + exec.native_account::set_map_item + # => [OLD_VALUE, i-1, pad(12)] + + # override OLD_VALUE with SCHEME_ID_WORD + adv_loadw + # => [SCHEME_ID_WORD, i-1, pad(12)] + + # validate the scheme id word is in a correct form + exec.auth::signature::assert_supported_scheme_word + # => [SCHEME_ID_WORD, i-1, pad(12)] + + dup.4 exec.create_approver_map_key + # => [APPROVER_MAP_KEY, SCHEME_ID_WORD, i-1, pad(12)] + + push.APPROVER_SCHEME_ID_SLOT[0..2] + # => [scheme_id_slot_id_suffix, scheme_id_slot_id_prefix, APPROVER_MAP_KEY, SCHEME_ID_WORD, i-1, pad(12)] + + exec.native_account::set_map_item + # => [OLD_VALUE, i-1, pad(12)] + + dropw + # => [i-1, pad(12)] + + dup neq.0 + # => [is_non_zero, i-1, pad(12)] + end + # => [pad(13)] + + drop + # => [pad(12)] + + # compare initial vs current multisig config + + # load init_num_of_approvers & new_num_of_approvers + loc_load.NEW_NUM_OF_APPROVERS_LOC loc_load.INIT_NUM_OF_APPROVERS_LOC + # => [init_num_of_approvers, new_num_of_approvers, pad(12)] + + exec.cleanup_pubkey_and_scheme_id_mapping + # => [pad(12)] +end + +# Computes the effective transaction threshold based on called procedures and per-procedure +# overrides stored in PROC_THRESHOLD_ROOTS_SLOT. Falls back to default_threshold if no +# overrides apply. +# +#! Inputs: [default_threshold] +#! Outputs: [transaction_threshold] +@locals(1) +proc compute_transaction_threshold(default_threshold: u32) -> u32 + # 1. initialize transaction_threshold = 0 + # 2. iterate through all account procedures + # a. check if the procedure was called during the transaction + # b. if called, get the override threshold of that procedure from the config map + # c. if proc_threshold > transaction_threshold, set transaction_threshold = proc_threshold + # 3. if transaction_threshold == 0 at the end, revert to using default_threshold + + # store default_threshold for later + loc_store.DEFAULT_THRESHOLD_LOC + # => [] + + # 1. initialize transaction_threshold = 0 + push.0 + # => [transaction_threshold] + + # get the number of account procedures + exec.active_account::get_num_procedures + # => [num_procedures, transaction_threshold] + + # 2. iterate through all account procedures + dup neq.0 + # => [should_continue, num_procedures, transaction_threshold] + while.true + sub.1 dup + # => [num_procedures-1, num_procedures-1, transaction_threshold] + + # get procedure root of the procedure with index i + exec.active_account::get_procedure_root dupw + # => [PROC_ROOT, PROC_ROOT, num_procedures-1, transaction_threshold] + + # 2a. check if this procedure has been called in the transaction + exec.native_account::was_procedure_called + # => [was_called, PROC_ROOT, num_procedures-1, transaction_threshold] + + # if it has been called, get the override threshold of that procedure + if.true + # => [PROC_ROOT, num_procedures-1, transaction_threshold] + + push.PROC_THRESHOLD_ROOTS_SLOT[0..2] + # => [proc_roots_slot_suffix, proc_roots_slot_prefix, PROC_ROOT, num_procedures-1, transaction_threshold] + + # 2b. get the override proc_threshold of that procedure + # if the procedure has no override threshold, the returned map item will be [0, 0, 0, 0] + exec.active_account::get_initial_map_item + # => [[proc_threshold, 0, 0, 0], num_procedures-1, transaction_threshold] + + movdn.3 drop drop drop dup dup.3 + # => [transaction_threshold, proc_threshold, proc_threshold, num_procedures-1, transaction_threshold] + + u32assert2.err="transaction threshold or procedure threshold are not u32" + u32gt + # => [is_gt, proc_threshold, num_procedures-1, transaction_threshold] + # 2c. if proc_threshold > transaction_threshold, update transaction_threshold + movup.2 movdn.3 + # => [is_gt, proc_threshold, transaction_threshold, num_procedures-1] + cdrop + # => [updated_transaction_threshold, num_procedures-1] + swap + # => [num_procedures-1, updated_transaction_threshold] + # if it has not been called during this transaction, nothing to do, move to the next procedure + else + dropw + # => [num_procedures-1, transaction_threshold] + end + + dup neq.0 + # => [should_continue, num_procedures-1, transaction_threshold] + end + + drop + # => [transaction_threshold] + + loc_load.DEFAULT_THRESHOLD_LOC + # => [default_threshold, transaction_threshold] + + # 3. if transaction_threshold == 0 at the end, revert to using default_threshold + dup.1 eq.0 + # => [is_zero, default_threshold, transaction_threshold] + + cdrop + # => [effective_transaction_threshold] +end + +#! Returns current num_approvers and the threshold `THRESHOLD_CONFIG_SLOT` +#! +#! Inputs: [] +#! Outputs: [threshold, num_approvers] +#! +#! Invocation: call +pub proc get_threshold_and_num_approvers + push.THRESHOLD_CONFIG_SLOT[0..2] + exec.active_account::get_initial_item + # => [threshold, num_approvers, 0, 0] + + movup.2 drop movup.2 drop + # => [threshold, num_approvers] +end + +#! Sets or clears a per-procedure threshold override. +#! +#! Inputs: [proc_threshold, PROC_ROOT] +#! Outputs: [] +#! +#! Where: +#! - PROC_ROOT is the root of the account procedure whose override is being updated. +#! - proc_threshold is the override threshold to set. +#! - if proc_threshold == 0, override is cleared and the default threshold applies. +#! - if proc_threshold > 0, it must be <= current num_approvers. +#! +#! Panics if: +#! - proc_threshold is not a u32 value. +#! - current num_approvers is not a u32 value. +#! - proc_threshold > current num_approvers. +#! +#! Invocation: call +pub proc set_procedure_threshold + exec.get_threshold_and_num_approvers + # => [default_threshold, num_approvers, proc_threshold, PROC_ROOT] + + drop + # => [num_approvers, proc_threshold, PROC_ROOT] + + dup.1 swap + # => [num_approvers, proc_threshold, proc_threshold, PROC_ROOT] + + u32assert2.err=ERR_NUM_APPROVERS_OR_PROC_THRESHOLD_NOT_U32 + u32gt assertz.err=ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS + # => [proc_threshold, PROC_ROOT] + + # Store [proc_threshold, 0, 0, 0] = PROC_THRESHOLD_WORD, where proc_threshold == 0 acts as clear. + push.0.0.0 + movup.3 + swapw + # => [PROC_ROOT, PROC_THRESHOLD_WORD] + + push.PROC_THRESHOLD_ROOTS_SLOT[0..2] + # => [proc_roots_slot_suffix, proc_roots_slot_prefix, PROC_ROOT, PROC_THRESHOLD_WORD] + + exec.native_account::set_map_item + # => [OLD_PROC_THRESHOLD_WORD] + + dropw + # => [] +end + +#! Returns signer public key at index i +#! +#! Inputs: [index] +#! Outputs: [PUB_KEY, scheme_id] +#! +#! Panics if: +#! - index is not a u32 value. +#! +#! Invocation: call +pub proc get_signer_at + u32assert.err=ERR_SIGNER_INDEX_NOT_U32 + # => [index] + + dup + # => [index, index] + + exec.create_approver_map_key + # => [APPROVER_MAP_KEY, index] + + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [APPROVER_PUBLIC_KEYS_SLOT, APPROVER_MAP_KEY, index] + + exec.active_account::get_initial_map_item + # => [PUB_KEY, index] + + movup.4 + # => [index, PUB_KEY] + + exec.create_approver_map_key + # => [APPROVER_MAP_KEY, PUB_KEY] + + push.APPROVER_SCHEME_ID_SLOT[0..2] + # => [APPROVER_SCHEME_ID_SLOT, APPROVER_MAP_KEY, PUB_KEY] + + exec.active_account::get_initial_map_item + # => [SCHEME_ID_WORD, PUB_KEY] + + movdn.3 drop drop drop + # => [scheme_id, PUB_KEY] + + movdn.4 + # => [PUB_KEY, scheme_id] +end + + +#! Returns 1 if PUB_KEY is a current signer, else 0. +#! +#! Inputs: [PUB_KEY] +#! Outputs: [is_signer] +#! Locals: +#! 0: is_signer_found +#! 1: current_signer_index +#! +#! Invocation: call +@locals(2) +pub proc is_signer(pub_key: word) -> felt + # initialize is_signer_found = false + push.0 loc_store.IS_SIGNER_FOUND_LOC + # => [PUB_KEY] + + exec.get_threshold_and_num_approvers + # => [threshold, num_approvers, PUB_KEY] + + drop + # => [num_approvers, PUB_KEY] + + dup neq.0 + # => [has_remaining_signers, num_approvers, PUB_KEY] + + while.true + # => [i, PUB_KEY] + + sub.1 + # => [i-1, PUB_KEY] + + # store i-1 for this loop iteration before map lookup + dup loc_store.CURRENT_SIGNER_INDEX_LOC + # => [i-1, PUB_KEY] + + exec.create_approver_map_key + # => [APPROVER_MAP_KEY, PUB_KEY] + + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_suffix, pub_key_slot_prefix, APPROVER_MAP_KEY, PUB_KEY] + + exec.active_account::get_initial_map_item + # => [CURRENT_PUB_KEY, PUB_KEY] + + dupw.1 exec.word::eq + # => [is_pub_key_match, PUB_KEY] + + loc_store.IS_SIGNER_FOUND_LOC + # => [PUB_KEY] + + loc_load.CURRENT_SIGNER_INDEX_LOC + # => [i-1, PUB_KEY] + + dup neq.0 + # => [has_remaining_signers, i-1, PUB_KEY] + + loc_load.IS_SIGNER_FOUND_LOC not + # => [!is_signer_found, has_remaining_signers, i-1, PUB_KEY] + + and + # => [should_loop, i-1, PUB_KEY] + end + + drop dropw + # => [] + + loc_load.IS_SIGNER_FOUND_LOC + # => [is_signer] +end + +#! Check if transaction has already been executed and add it to executed transactions for replay protection, and +#! finalizes multisig authentication. +#! +#! Inputs: [MSG] +#! Outputs: [] +#! +#! Panics if: +#! - the same transaction has already been executed +#! +#! Invocation: exec +pub proc assert_new_tx(msg: word) + push.IS_EXECUTED_FLAG + # => [[0, 0, 0, is_executed], MSG] + + swapw + # => [TX_SUMMARY_COMMITMENT, IS_EXECUTED_FLAG] + + push.EXECUTED_TXS_SLOT[0..2] + # => [txs_slot_suffix, txs_slot_prefix, MSG, IS_EXECUTED_FLAG] + + # Set the key value pair in the map to mark transaction as executed + exec.native_account::set_map_item + # => [[0, 0, 0, is_executed]] + + movdn.3 drop drop drop + # => [is_executed] + + assertz.err=ERR_TX_ALREADY_EXECUTED + # => [] +end + +#! Authenticate a transaction using the signature scheme specified by scheme_id +#! with multi-signature support +#! +#! Supported schemes: +#! - 1 => ecdsa_k256_keccak +#! - 2 => falcon512_poseidon2 +#! +#! This procedure implements multi-signature authentication by: +#! 1. Computing the transaction summary message that needs to be signed +#! 2. Verifying signatures from multiple required signers against their public keys +#! 3. Ensuring the minimum threshold of valid signatures is met +#! +#! Inputs: +#! Operand stack: [SALT] +#! Advice map: { +#! h(SIG_0, MSG): SIG_0, +#! h(SIG_1, MSG): SIG_1, +#! h(SIG_n, MSG): SIG_n +#! } +#! Outputs: +#! Operand stack: [TX_SUMMARY_COMMITMENT] +#! +#! Where: +#! - SALT is a cryptographically random nonce that enables multiple concurrent +#! multisig transactions while maintaining replay protection. Each transaction +#! must use a unique SALT value to ensure transaction uniqueness. +#! - SIG_i is the signature from the i-th signer. +#! - MSG is the transaction message being signed. +#! - h(SIG_i, MSG) is the hash of the signature and message used as the advice map key. +#! +#! Panics if: +#! - insufficient number of valid signatures (below threshold). +#! +#! Invocation: call +@locals(1) +pub proc auth_tx(salt: word) + exec.native_account::incr_nonce drop + # => [SALT] + + # ------ Computing transaction summary ------ + + exec.auth::create_tx_summary + # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT] + + # insert tx summary into advice provider for extraction by the host + adv.insert_hqword + # => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, SALT] + + # the commitment to the tx summary is the message that is signed + exec.auth::hash_tx_summary + # => [TX_SUMMARY_COMMITMENT] + + # ------ Verifying approver signatures ------ + + exec.get_threshold_and_num_approvers + # => [default_threshold, num_of_approvers, TX_SUMMARY_COMMITMENT] + + movdn.5 + # => [num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] + + push.APPROVER_PUBLIC_KEYS_SLOT[0..2] + # => [pub_key_slot_suffix, pub_key_slot_prefix, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] + + push.APPROVER_SCHEME_ID_SLOT[0..2] + # => [scheme_id_slot_suffix, scheme_id_slot_prefix, pub_key_slot_suffix, pub_key_slot_prefix, num_of_approvers, TX_SUMMARY_COMMITMENT, default_threshold] + + exec.::miden::standards::auth::signature::verify_signatures + # => [num_verified_signatures, TX_SUMMARY_COMMITMENT, default_threshold] + + # ------ Checking threshold is >= num_verified_signatures ------ + + movup.5 + # => [default_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] + + exec.compute_transaction_threshold + # => [transaction_threshold, num_verified_signatures, TX_SUMMARY_COMMITMENT] + + u32assert2 u32lt + # => [is_unauthorized, TX_SUMMARY_COMMITMENT] + + # If signatures are non-existent the tx will fail here. + if.true + emit.AUTH_UNAUTHORIZED_EVENT + push.0 assert.err="insufficient number of signatures" + end + + # TX_SUMMARY_COMMITMENT is returned so wrappers can run optional checks + # (e.g. PSM) before replay-protection finalization. + # => [TX_SUMMARY_COMMITMENT] +end diff --git a/crates/miden-standards/asm/standards/auth/psm.masm b/crates/miden-standards/asm/standards/auth/psm.masm new file mode 100644 index 0000000000..d778cafb14 --- /dev/null +++ b/crates/miden-standards/asm/standards/auth/psm.masm @@ -0,0 +1,158 @@ +# Private State Manager (PSM) account component. +# This component is composed into account auth flows especially for multisig and adds +# an extra signature check by a dedicated private state manager signer. +# +# Private State Manager (PSM) is a cloud backup and synchronization layer for Miden private accounts +# See: https://github.com/OpenZeppelin/private-state-manager + +use miden::protocol::auth::AUTH_UNAUTHORIZED_EVENT +use miden::protocol::native_account +use miden::standards::auth::tx_policy +use miden::standards::auth::signature + +# IMPORTANT SECURITY NOTES +# -------------------------------------------------------------------------------- +# - By default, exactly one valid PSM signature is required. +# - If `update_psm_public_key` is the only non-auth account procedure called in the current +# transaction, `verify_signature` skips the PSM signature check so key rotation can proceed +# without the old PSM signer. +# - `update_psm_public_key` rotates the PSM public key and corresponding scheme id using the fixed +# map key `PSM_MAP_KEY`. + + +# CONSTANTS +# ================================================================================================= + +# Storage Slots +# +# This authentication component uses named storage slots. +# - PSM_PUBLIC_KEYS_SLOT (map): +# PSM_MAP_KEY => PSM_PUBLIC_KEY +# where: PSM_MAP_KEY = [0, 0, 0, 0] +# +# - PSM_SCHEME_ID_SLOT (map): +# PSM_MAP_KEY => [scheme_id, 0, 0, 0] +# where: PSM_MAP_KEY = [0, 0, 0, 0] + +# The slot in this component's storage layout where the PSM public key map is stored. +# Map entries: [PSM_MAP_KEY] => [PSM_PUBLIC_KEY] +const PSM_PUBLIC_KEYS_SLOT = word("miden::standards::auth::psm::pub_key") + +# The slot in this component's storage layout where the scheme id for the corresponding PSM public key map is stored. +# Map entries: [PSM_MAP_KEY] => [scheme_id, 0, 0, 0] +const PSM_SCHEME_ID_SLOT = word("miden::standards::auth::psm::scheme") + +# Single-entry storage map key where private state manager signer data is stored. +const PSM_MAP_KEY = [0, 0, 0, 0] + +# ERRORS +# ------------------------------------------------------------------------------------------------- +const ERR_INVALID_PSM_SIGNATURE = "invalid private state manager signature" + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Updates the private state manager public key. +#! +#! Inputs: [new_psm_scheme_id, NEW_PSM_PUBLIC_KEY] +#! Outputs: [] +#! +#! Notes: +#! - This procedure only updates the PSM public key and corresponding scheme id. +#! - `verify_signature` skips PSM verification only when this is the only non-auth account +#! procedure called in the transaction. +#! +#! Invocation: call +@locals(1) +pub proc update_psm_public_key(new_psm_scheme_id: felt, new_psm_public_key: word) + # Validate supported signature scheme before committing it to storage. + dup exec.signature::assert_supported_scheme + # => [new_psm_scheme_id, NEW_PSM_PUBLIC_KEY] + + loc_store.0 + # => [NEW_PSM_PUBLIC_KEY] + + push.PSM_MAP_KEY + # => [PSM_MAP_KEY, NEW_PSM_PUBLIC_KEY] + + push.PSM_PUBLIC_KEYS_SLOT[0..2] + # => [psm_pubkeys_slot_prefix, psm_pubkeys_slot_suffix, PSM_MAP_KEY, NEW_PSM_PUBLIC_KEY] + + exec.native_account::set_map_item + # => [OLD_PSM_PUBLIC_KEY] + + dropw + # => [] + + # Store new scheme id as [scheme_id, 0, 0, 0] in the single-entry map. + loc_load.0 + # => [scheme_id] + + push.0.0.0 movup.3 + # => [NEW_PSM_SCHEME_ID_WORD] + + push.PSM_MAP_KEY + # => [PSM_MAP_KEY, NEW_PSM_SCHEME_ID_WORD] + + push.PSM_SCHEME_ID_SLOT[0..2] + # => [psm_scheme_slot_prefix, psm_scheme_slot_suffix, PSM_MAP_KEY, NEW_PSM_SCHEME_ID_WORD] + + exec.native_account::set_map_item + # => [OLD_PSM_SCHEME_ID_WORD] + + dropw + # => [] +end + +#! Conditionally verifies a private state manager signature. +#! +#! Inputs: [MSG] +#! Outputs: [] +#! +#! Panics if: +#! - `update_psm_public_key` is called together with another non-auth account procedure. +#! - `update_psm_public_key` was not called and a valid PSM signature is missing or invalid. +#! +#! Invocation: exec +pub proc verify_signature(msg: word) + procref.update_psm_public_key + # => [UPDATE_PSM_PUBLIC_KEY_ROOT, MSG] + + exec.native_account::was_procedure_called + # => [was_update_psm_public_key_called, MSG] + + if.true + exec.tx_policy::assert_only_one_non_auth_procedure_called + # => [MSG] + + exec.tx_policy::assert_no_input_or_output_notes + # => [MSG] + + dropw + # => [] + else + push.1 + # => [1, MSG] + + push.PSM_PUBLIC_KEYS_SLOT[0..2] + # => [psm_pubkeys_slot_prefix, psm_pubkeys_slot_suffix, 1, MSG] + + push.PSM_SCHEME_ID_SLOT[0..2] + # => [psm_scheme_slot_prefix, psm_scheme_slot_suffix, psm_pubkeys_slot_prefix, psm_pubkeys_slot_suffix, 1, MSG] + + exec.signature::verify_signatures + # => [num_verified_signatures, MSG] + + neq.1 + # => [is_not_exactly_one, MSG] + + if.true + emit.AUTH_UNAUTHORIZED_EVENT + push.0 assert.err=ERR_INVALID_PSM_SIGNATURE + end + # => [MSG] + + dropw + # => [] + end +end diff --git a/crates/miden-standards/asm/standards/auth/tx_policy.masm b/crates/miden-standards/asm/standards/auth/tx_policy.masm new file mode 100644 index 0000000000..76da300070 --- /dev/null +++ b/crates/miden-standards/asm/standards/auth/tx_policy.masm @@ -0,0 +1,80 @@ +use miden::protocol::active_account +use miden::protocol::native_account +use miden::protocol::tx + +const ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE = "procedure must be called alone" +const ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES = "transaction must not include input or output notes" + +#! Asserts that exactly one non-auth account procedure was called in the current transaction. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +@locals(1) # non-auth called proc count +pub proc assert_only_one_non_auth_procedure_called + push.0 + loc_store.0 + # => [] + + exec.active_account::get_num_procedures + # => [num_procedures] + + dup neq.0 + # => [should_continue, num_procedures] + while.true + sub.1 dup + exec.active_account::get_procedure_root dupw + # => [PROC_ROOT, PROC_ROOT] + + exec.native_account::was_procedure_called + # => [was_called, PROC_ROOT] + + if.true + dropw + # => [proc_index] + + # The auth procedure is always at procedure index 0. + dup neq.0 + # => [is_not_auth_proc, proc_index] + + if.true + loc_load.0 add.1 loc_store.0 + # => [proc_index] + end + else + dropw + # => [proc_index] + end + + dup neq.0 + # => [should_continue, proc_index] + end + + drop + # => [] + + loc_load.0 eq.1 + assert.err=ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE + # => [] +end + +#! Asserts that the current transaction does not consume input notes or create output notes. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +pub proc assert_no_input_or_output_notes + exec.tx::get_num_input_notes + # => [num_input_notes] + + assertz.err=ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES + # => [] + + exec.tx::get_num_output_notes + # => [num_output_notes] + + assertz.err=ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES + # => [] +end diff --git a/crates/miden-standards/src/account/auth/mod.rs b/crates/miden-standards/src/account/auth/mod.rs index c1c3a9791c..e999fab153 100644 --- a/crates/miden-standards/src/account/auth/mod.rs +++ b/crates/miden-standards/src/account/auth/mod.rs @@ -9,3 +9,6 @@ pub use singlesig_acl::{AuthSingleSigAcl, AuthSingleSigAclConfig}; mod multisig; pub use multisig::{AuthMultisig, AuthMultisigConfig}; + +mod multisig_psm; +pub use multisig_psm::{AuthMultisigPsm, AuthMultisigPsmConfig, PsmConfig}; diff --git a/crates/miden-standards/src/account/auth/multisig.rs b/crates/miden-standards/src/account/auth/multisig.rs index ef84292a47..4d76e3a4aa 100644 --- a/crates/miden-standards/src/account/auth/multisig.rs +++ b/crates/miden-standards/src/account/auth/multisig.rs @@ -23,6 +23,9 @@ use miden_protocol::utils::sync::LazyLock; use crate::account::components::multisig_library; +// CONSTANTS +// ================================================================================================ + static THRESHOLD_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::auth::multisig::threshold_config") .expect("storage slot name should be valid") @@ -123,19 +126,13 @@ impl AuthMultisigConfig { } } -/// An [`AccountComponent`] implementing a multisig based on ECDSA signatures. +/// An [`AccountComponent`] implementing a multisig authentication. /// /// It enforces a threshold of approver signatures for every transaction, with optional -/// per-procedure thresholds overrides. Non-uniform thresholds (especially a threshold of one) -/// should be used with caution for private multisig accounts, as a single approver could withhold -/// the new state from other approvers, effectively locking them out. -/// -/// The storage layout is: -/// - Slot 0(value): [threshold, num_approvers, 0, 0] -/// - Slot 1(map): A map with approver public keys (index -> pubkey) -/// - Slot 2(map): A map with approver scheme ids (index -> scheme_id) -/// - Slot 3(map): A map which stores executed transactions -/// - Slot 4(map): A map which stores procedure thresholds (PROC_ROOT -> threshold) +/// per-procedure threshold overrides. Non-uniform thresholds (especially a threshold of one) +/// should be used with caution for private multisig accounts, without Private State Manager (PSM), +/// a single approver may advance state and withhold updates from other approvers, effectively +/// locking them out. /// /// This component supports all account types. #[derive(Debug)] @@ -314,6 +311,9 @@ impl From for AccountComponent { } } +// TESTS +// ================================================================================================ + #[cfg(test)] mod tests { use alloc::string::ToString; diff --git a/crates/miden-standards/src/account/auth/multisig_psm.rs b/crates/miden-standards/src/account/auth/multisig_psm.rs new file mode 100644 index 0000000000..77008949d5 --- /dev/null +++ b/crates/miden-standards/src/account/auth/multisig_psm.rs @@ -0,0 +1,566 @@ +use alloc::vec::Vec; + +use miden_protocol::Word; +use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; +use miden_protocol::account::component::{ + AccountComponentMetadata, + SchemaType, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + AccountComponent, + StorageMap, + StorageMapKey, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::errors::AccountError; +use miden_protocol::utils::sync::LazyLock; + +use super::multisig::{AuthMultisig, AuthMultisigConfig}; +use crate::account::components::multisig_psm_library; + +// CONSTANTS +// ================================================================================================ + +static PSM_PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::psm::pub_key") + .expect("storage slot name should be valid") +}); + +static PSM_SCHEME_ID_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::auth::psm::scheme") + .expect("storage slot name should be valid") +}); + +// MULTISIG AUTHENTICATION COMPONENT +// ================================================================================================ + +/// Configuration for [`AuthMultisigPsm`] component. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthMultisigPsmConfig { + multisig: AuthMultisigConfig, + psm_config: PsmConfig, +} + +/// Public configuration for the private state manager signer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PsmConfig { + pub_key: PublicKeyCommitment, + auth_scheme: AuthScheme, +} + +impl PsmConfig { + pub fn new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self { + Self { pub_key, auth_scheme } + } + + pub fn pub_key(&self) -> PublicKeyCommitment { + self.pub_key + } + + pub fn auth_scheme(&self) -> AuthScheme { + self.auth_scheme + } + + fn public_key_slot() -> &'static StorageSlotName { + &PSM_PUBKEY_SLOT_NAME + } + + fn scheme_id_slot() -> &'static StorageSlotName { + &PSM_SCHEME_ID_SLOT_NAME + } + + fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::public_key_slot().clone(), + StorageSlotSchema::map( + "Private state manager public keys", + SchemaType::u32(), + SchemaType::pub_key(), + ), + ) + } + + fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::scheme_id_slot().clone(), + StorageSlotSchema::map( + "Private state manager scheme IDs", + SchemaType::u32(), + SchemaType::auth_scheme(), + ), + ) + } + + fn into_component_parts(self) -> (Vec, Vec<(StorageSlotName, StorageSlotSchema)>) { + let mut storage_slots = Vec::with_capacity(2); + + // Private state manager public key slot (map: [0, 0, 0, 0] -> pubkey) + let psm_public_key_entries = + [(StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from(self.pub_key))]; + storage_slots.push(StorageSlot::with_map( + Self::public_key_slot().clone(), + StorageMap::with_entries(psm_public_key_entries).unwrap(), + )); + + // Private state manager scheme IDs slot (map: [0, 0, 0, 0] -> [scheme_id, 0, 0, 0]) + let psm_scheme_id_entries = [( + StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), + Word::from([self.auth_scheme as u32, 0, 0, 0]), + )]; + storage_slots.push(StorageSlot::with_map( + Self::scheme_id_slot().clone(), + StorageMap::with_entries(psm_scheme_id_entries).unwrap(), + )); + + let slot_metadata = vec![Self::public_key_slot_schema(), Self::auth_scheme_slot_schema()]; + + (storage_slots, slot_metadata) + } +} + +impl AuthMultisigPsmConfig { + /// Creates a new configuration with the given approvers, default threshold and PSM signer. + /// + /// The `default_threshold` must be at least 1 and at most the number of approvers. + /// The private state manager public key must be different from all approver public keys. + pub fn new( + approvers: Vec<(PublicKeyCommitment, AuthScheme)>, + default_threshold: u32, + psm_config: PsmConfig, + ) -> Result { + let multisig = AuthMultisigConfig::new(approvers, default_threshold)?; + if multisig + .approvers() + .iter() + .any(|(approver, _)| *approver == psm_config.pub_key()) + { + return Err(AccountError::other( + "private state manager public key must be different from approvers", + )); + } + + Ok(Self { multisig, psm_config }) + } + + /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and + /// at most the number of approvers. + pub fn with_proc_thresholds( + mut self, + proc_thresholds: Vec<(Word, u32)>, + ) -> Result { + self.multisig = self.multisig.with_proc_thresholds(proc_thresholds)?; + Ok(self) + } + + pub fn approvers(&self) -> &[(PublicKeyCommitment, AuthScheme)] { + self.multisig.approvers() + } + + pub fn default_threshold(&self) -> u32 { + self.multisig.default_threshold() + } + + pub fn proc_thresholds(&self) -> &[(Word, u32)] { + self.multisig.proc_thresholds() + } + + pub fn psm_config(&self) -> PsmConfig { + self.psm_config + } + + fn into_parts(self) -> (AuthMultisigConfig, PsmConfig) { + (self.multisig, self.psm_config) + } +} + +/// An [`AccountComponent`] implementing a multisig authentication with a private state manager. +/// +/// It enforces a threshold of approver signatures for every transaction, with optional +/// per-procedure threshold overrides. With Private State Manager (PSM) is configured, +/// multisig authorization is combined with PSM authorization, so operations require both +/// multisig approval and a valid PSM signature. This substantially mitigates low-threshold +/// state-withholding scenarios since the PSM is expected to forward state updates to other +/// approvers. +/// +/// This component supports all account types. +#[derive(Debug)] +pub struct AuthMultisigPsm { + multisig: AuthMultisig, + psm_config: PsmConfig, +} + +impl AuthMultisigPsm { + /// The name of the component. + pub const NAME: &'static str = "miden::auth::multisig_psm"; + + /// Creates a new [`AuthMultisigPsm`] component from the provided configuration. + pub fn new(config: AuthMultisigPsmConfig) -> Result { + let (multisig_config, psm_config) = config.into_parts(); + Ok(Self { + multisig: AuthMultisig::new(multisig_config)?, + psm_config, + }) + } + + /// Returns the [`StorageSlotName`] where the threshold configuration is stored. + pub fn threshold_config_slot() -> &'static StorageSlotName { + AuthMultisig::threshold_config_slot() + } + + /// Returns the [`StorageSlotName`] where the approver public keys are stored. + pub fn approver_public_keys_slot() -> &'static StorageSlotName { + AuthMultisig::approver_public_keys_slot() + } + + // Returns the [`StorageSlotName`] where the approver scheme IDs are stored. + pub fn approver_scheme_ids_slot() -> &'static StorageSlotName { + AuthMultisig::approver_scheme_ids_slot() + } + + /// Returns the [`StorageSlotName`] where the executed transactions are stored. + pub fn executed_transactions_slot() -> &'static StorageSlotName { + AuthMultisig::executed_transactions_slot() + } + + /// Returns the [`StorageSlotName`] where the procedure thresholds are stored. + pub fn procedure_thresholds_slot() -> &'static StorageSlotName { + AuthMultisig::procedure_thresholds_slot() + } + + /// Returns the [`StorageSlotName`] where the private state manager public key is stored. + pub fn psm_public_key_slot() -> &'static StorageSlotName { + PsmConfig::public_key_slot() + } + + /// Returns the [`StorageSlotName`] where the private state manager scheme IDs are stored. + pub fn psm_scheme_id_slot() -> &'static StorageSlotName { + PsmConfig::scheme_id_slot() + } + + /// Returns the storage slot schema for the threshold configuration slot. + pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + AuthMultisig::threshold_config_slot_schema() + } + + /// Returns the storage slot schema for the approver public keys slot. + pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + AuthMultisig::approver_public_keys_slot_schema() + } + + // Returns the storage slot schema for the approver scheme IDs slot. + pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + AuthMultisig::approver_auth_scheme_slot_schema() + } + + /// Returns the storage slot schema for the executed transactions slot. + pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + AuthMultisig::executed_transactions_slot_schema() + } + + /// Returns the storage slot schema for the procedure thresholds slot. + pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + AuthMultisig::procedure_thresholds_slot_schema() + } + + /// Returns the storage slot schema for the private state manager public key slot. + pub fn psm_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + PsmConfig::public_key_slot_schema() + } + + /// Returns the storage slot schema for the private state manager scheme IDs slot. + pub fn psm_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + PsmConfig::auth_scheme_slot_schema() + } +} + +impl From for AccountComponent { + fn from(multisig: AuthMultisigPsm) -> Self { + let AuthMultisigPsm { multisig, psm_config } = multisig; + let multisig_component = AccountComponent::from(multisig); + let (psm_slots, psm_slot_metadata) = psm_config.into_component_parts(); + + let mut storage_slots = multisig_component.storage_slots().to_vec(); + storage_slots.extend(psm_slots); + + let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component + .storage_schema() + .iter() + .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone())) + .collect(); + slot_schemas.extend(psm_slot_metadata); + + let storage_schema = + StorageSchema::new(slot_schemas).expect("storage schema should be valid"); + + let metadata = AccountComponentMetadata::new( + AuthMultisigPsm::NAME, + multisig_component.supported_types().clone(), + ) + .with_description(multisig_component.metadata().description()) + .with_version(multisig_component.metadata().version().clone()) + .with_storage_schema(storage_schema); + + AccountComponent::new(multisig_psm_library(), storage_slots, metadata).expect( + "Multisig auth component should satisfy the requirements of a valid account component", + ) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; + use miden_protocol::account::auth::AuthSecretKey; + + use super::*; + use crate::account::wallets::BasicWallet; + + /// Test multisig component setup with various configurations + #[test] + fn test_multisig_component_setup() { + // Create test secret keys + let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2(); + let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2(); + let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2(); + let psm_key = AuthSecretKey::new_ecdsa_k256_keccak(); + + // Create approvers list for multisig config + let approvers = vec![ + (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), + (sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()), + ]; + + let threshold = 2u32; + + // Create multisig component + let multisig_component = AuthMultisigPsm::new( + AuthMultisigPsmConfig::new( + approvers.clone(), + threshold, + PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + ) + .expect("invalid multisig config"), + ) + .expect("multisig component creation failed"); + + // Build account with multisig component + let account = AccountBuilder::new([0; 32]) + .with_auth_component(multisig_component) + .with_component(BasicWallet) + .build() + .expect("account building failed"); + + // Verify config slot: [threshold, num_approvers, 0, 0] + let config_slot = account + .storage() + .get_item(AuthMultisigPsm::threshold_config_slot()) + .expect("config storage slot access failed"); + assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); + + // Verify approver pub keys slot + for (i, (expected_pub_key, _)) in approvers.iter().enumerate() { + let stored_pub_key = account + .storage() + .get_map_item( + AuthMultisigPsm::approver_public_keys_slot(), + Word::from([i as u32, 0, 0, 0]), + ) + .expect("approver public key storage map access failed"); + assert_eq!(stored_pub_key, Word::from(*expected_pub_key)); + } + + // Verify approver scheme IDs slot + for (i, (_, expected_auth_scheme)) in approvers.iter().enumerate() { + let stored_scheme_id = account + .storage() + .get_map_item( + AuthMultisigPsm::approver_scheme_ids_slot(), + Word::from([i as u32, 0, 0, 0]), + ) + .expect("approver scheme ID storage map access failed"); + assert_eq!(stored_scheme_id, Word::from([*expected_auth_scheme as u32, 0, 0, 0])); + } + + // Verify private state manager signer is configured. + let psm_public_key = account + .storage() + .get_map_item(AuthMultisigPsm::psm_public_key_slot(), Word::from([0u32, 0, 0, 0])) + .expect("private state manager public key storage map access failed"); + assert_eq!(psm_public_key, Word::from(psm_key.public_key().to_commitment())); + + let psm_scheme_id = account + .storage() + .get_map_item(AuthMultisigPsm::psm_scheme_id_slot(), Word::from([0u32, 0, 0, 0])) + .expect("private state manager scheme ID storage map access failed"); + assert_eq!(psm_scheme_id, Word::from([psm_key.auth_scheme() as u32, 0, 0, 0])); + } + + /// Test multisig component with minimum threshold (1 of 1) + #[test] + fn test_multisig_component_minimum_threshold() { + let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(); + let psm_key = AuthSecretKey::new_falcon512_poseidon2(); + let approvers = vec![(pub_key, AuthScheme::EcdsaK256Keccak)]; + let threshold = 1u32; + + let multisig_component = AuthMultisigPsm::new( + AuthMultisigPsmConfig::new( + approvers.clone(), + threshold, + PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + ) + .expect("invalid multisig config"), + ) + .expect("multisig component creation failed"); + + let account = AccountBuilder::new([0; 32]) + .with_auth_component(multisig_component) + .with_component(BasicWallet) + .build() + .expect("account building failed"); + + // Verify storage layout + let config_slot = account + .storage() + .get_item(AuthMultisigPsm::threshold_config_slot()) + .expect("config storage slot access failed"); + assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0])); + + let stored_pub_key = account + .storage() + .get_map_item(AuthMultisigPsm::approver_public_keys_slot(), Word::from([0u32, 0, 0, 0])) + .expect("approver pub keys storage map access failed"); + assert_eq!(stored_pub_key, Word::from(pub_key)); + + let stored_scheme_id = account + .storage() + .get_map_item(AuthMultisigPsm::approver_scheme_ids_slot(), Word::from([0u32, 0, 0, 0])) + .expect("approver scheme IDs storage map access failed"); + assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])); + } + + /// Test multisig component setup with a private state manager. + #[test] + fn test_multisig_component_with_psm() { + let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2(); + let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2(); + let psm_key = AuthSecretKey::new_ecdsa_k256_keccak(); + + let approvers = vec![ + (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), + ]; + + let multisig_component = AuthMultisigPsm::new( + AuthMultisigPsmConfig::new( + approvers, + 2, + PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + ) + .expect("invalid multisig config"), + ) + .expect("multisig component creation failed"); + + let account = AccountBuilder::new([0; 32]) + .with_auth_component(multisig_component) + .with_component(BasicWallet) + .build() + .expect("account building failed"); + + let psm_public_key = account + .storage() + .get_map_item(AuthMultisigPsm::psm_public_key_slot(), Word::from([0u32, 0, 0, 0])) + .expect("private state manager public key storage map access failed"); + assert_eq!(psm_public_key, Word::from(psm_key.public_key().to_commitment())); + + let psm_scheme_id = account + .storage() + .get_map_item(AuthMultisigPsm::psm_scheme_id_slot(), Word::from([0u32, 0, 0, 0])) + .expect("private state manager scheme ID storage map access failed"); + assert_eq!(psm_scheme_id, Word::from([psm_key.auth_scheme() as u32, 0, 0, 0])); + } + + /// Test multisig component error cases + #[test] + fn test_multisig_component_error_cases() { + let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(); + let psm_key = AuthSecretKey::new_falcon512_poseidon2(); + let approvers = vec![(pub_key, AuthScheme::EcdsaK256Keccak)]; + + // Test threshold > number of approvers (should fail) + let result = AuthMultisigPsmConfig::new( + approvers, + 2, + PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + ); + + assert!( + result + .unwrap_err() + .to_string() + .contains("threshold cannot be greater than number of approvers") + ); + } + + /// Test multisig component with duplicate approvers (should fail) + #[test] + fn test_multisig_component_duplicate_approvers() { + // Create secret keys for approvers + let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak(); + let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak(); + let psm_key = AuthSecretKey::new_falcon512_poseidon2(); + + // Create approvers list with duplicate public keys + let approvers = vec![ + (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), + ]; + + let result = AuthMultisigPsmConfig::new( + approvers, + 2, + PsmConfig::new(psm_key.public_key().to_commitment(), psm_key.auth_scheme()), + ); + assert!( + result + .unwrap_err() + .to_string() + .contains("duplicate approver public keys are not allowed") + ); + } + + /// Test multisig component rejects a private state manager key which is already an approver. + #[test] + fn test_multisig_component_psm_not_approver() { + let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak(); + let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak(); + + let approvers = vec![ + (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()), + ]; + + let result = AuthMultisigPsmConfig::new( + approvers, + 2, + PsmConfig::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()), + ); + + assert!( + result + .unwrap_err() + .to_string() + .contains("private state manager public key must be different from approvers") + ); + } +} diff --git a/crates/miden-standards/src/account/auth/singlesig.rs b/crates/miden-standards/src/account/auth/singlesig.rs index 9581bfbf40..6e7ca5dc12 100644 --- a/crates/miden-standards/src/account/auth/singlesig.rs +++ b/crates/miden-standards/src/account/auth/singlesig.rs @@ -11,6 +11,9 @@ use miden_protocol::utils::sync::LazyLock; use crate::account::components::singlesig_library; +// CONSTANTS +// ================================================================================================ + static PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::auth::singlesig::pub_key") .expect("storage slot name should be valid") diff --git a/crates/miden-standards/src/account/auth/singlesig_acl.rs b/crates/miden-standards/src/account/auth/singlesig_acl.rs index 2da3f212c5..5792c179bc 100644 --- a/crates/miden-standards/src/account/auth/singlesig_acl.rs +++ b/crates/miden-standards/src/account/auth/singlesig_acl.rs @@ -23,6 +23,9 @@ use miden_protocol::{Felt, Word}; use crate::account::components::singlesig_acl_library; +// CONSTANTS +// ================================================================================================ + static PUBKEY_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::auth::singlesig_acl::pub_key") .expect("storage slot name should be valid") @@ -302,6 +305,9 @@ impl From for AccountComponent { } } +// TESTS +// ================================================================================================ + #[cfg(test)] mod tests { use miden_protocol::Word; diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index edcbea2b4c..f7783a46db 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -48,6 +48,15 @@ static MULTISIG_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Multisig library is well-formed") }); +/// Initialize the Multisig PSM library only once. +static MULTISIG_PSM_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/auth/multisig_psm.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Multisig PSM library is well-formed") +}); + // Initialize the NoAuth library only once. static NO_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = @@ -123,6 +132,11 @@ pub fn multisig_library() -> Library { MULTISIG_LIBRARY.clone() } +/// Returns the Multisig PSM Library. +pub fn multisig_psm_library() -> Library { + MULTISIG_PSM_LIBRARY.clone() +} + /// Returns the NoAuth Library. pub fn no_auth_library() -> Library { NO_AUTH_LIBRARY.clone() @@ -140,6 +154,7 @@ pub enum StandardAccountComponent { AuthSingleSig, AuthSingleSigAcl, AuthMultisig, + AuthMultisigPsm, AuthNoAuth, } @@ -153,6 +168,7 @@ impl StandardAccountComponent { Self::AuthSingleSig => SINGLESIG_LIBRARY.as_ref(), Self::AuthSingleSigAcl => SINGLESIG_ACL_LIBRARY.as_ref(), Self::AuthMultisig => MULTISIG_LIBRARY.as_ref(), + Self::AuthMultisigPsm => MULTISIG_PSM_LIBRARY.as_ref(), Self::AuthNoAuth => NO_AUTH_LIBRARY.as_ref(), }; @@ -205,6 +221,9 @@ impl StandardAccountComponent { Self::AuthMultisig => { component_interface_vec.push(AccountComponentInterface::AuthMultisig) }, + Self::AuthMultisigPsm => { + component_interface_vec.push(AccountComponentInterface::AuthMultisigPsm) + }, Self::AuthNoAuth => { component_interface_vec.push(AccountComponentInterface::AuthNoAuth) }, @@ -223,6 +242,7 @@ impl StandardAccountComponent { Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::AuthSingleSig.extract_component(procedures_set, component_interface_vec); Self::AuthSingleSigAcl.extract_component(procedures_set, component_interface_vec); + Self::AuthMultisigPsm.extract_component(procedures_set, component_interface_vec); Self::AuthMultisig.extract_component(procedures_set, component_interface_vec); Self::AuthNoAuth.extract_component(procedures_set, component_interface_vec); } diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 8a00b0c1a3..51615b7151 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -7,7 +7,7 @@ use miden_protocol::note::PartialNote; use miden_protocol::{Felt, Word}; use crate::AuthMethod; -use crate::account::auth::{AuthMultisig, AuthSingleSig, AuthSingleSigAcl}; +use crate::account::auth::{AuthMultisig, AuthMultisigPsm, AuthSingleSig, AuthSingleSigAcl}; use crate::account::interface::AccountInterfaceError; // ACCOUNT COMPONENT INTERFACE @@ -33,6 +33,9 @@ pub enum AccountComponentInterface { /// Exposes procedures from the /// [`AuthMultisig`][crate::account::auth::AuthMultisig] module. AuthMultisig, + /// Exposes procedures from the + /// [`AuthMultisigPsm`][crate::account::auth::AuthMultisigPsm] module. + AuthMultisigPsm, /// Exposes procedures from the [`NoAuth`][crate::account::auth::NoAuth] module. /// /// This authentication scheme provides no cryptographic authentication and only increments @@ -61,6 +64,7 @@ impl AccountComponentInterface { AccountComponentInterface::AuthSingleSig => "SingleSig".to_string(), AccountComponentInterface::AuthSingleSigAcl => "SingleSig ACL".to_string(), AccountComponentInterface::AuthMultisig => "Multisig".to_string(), + AccountComponentInterface::AuthMultisigPsm => "Multisig PSM".to_string(), AccountComponentInterface::AuthNoAuth => "No Auth".to_string(), AccountComponentInterface::Custom(proc_root_vec) => { let result = proc_root_vec @@ -82,6 +86,7 @@ impl AccountComponentInterface { AccountComponentInterface::AuthSingleSig | AccountComponentInterface::AuthSingleSigAcl | AccountComponentInterface::AuthMultisig + | AccountComponentInterface::AuthMultisigPsm | AccountComponentInterface::AuthNoAuth ) } @@ -107,6 +112,14 @@ impl AccountComponentInterface { AuthMultisig::approver_scheme_ids_slot(), )] }, + AccountComponentInterface::AuthMultisigPsm => { + vec![extract_multisig_auth_method( + storage, + AuthMultisigPsm::threshold_config_slot(), + AuthMultisigPsm::approver_public_keys_slot(), + AuthMultisigPsm::approver_scheme_ids_slot(), + )] + }, AccountComponentInterface::AuthNoAuth => vec![AuthMethod::NoAuth], _ => vec![], // Non-auth components return empty vector } diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index ab7e6f879d..f23b1414a7 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -14,6 +14,7 @@ use crate::account::components::{ basic_fungible_faucet_library, basic_wallet_library, multisig_library, + multisig_psm_library, network_fungible_faucet_library, no_auth_library, singlesig_acl_library, @@ -112,6 +113,10 @@ impl AccountInterfaceExt for AccountInterface { component_proc_digests .extend(multisig_library().mast_forest().procedure_digests()); }, + AccountComponentInterface::AuthMultisigPsm => { + component_proc_digests + .extend(multisig_psm_library().mast_forest().procedure_digests()); + }, AccountComponentInterface::AuthNoAuth => { component_proc_digests .extend(no_auth_library().mast_forest().procedure_digests()); diff --git a/crates/miden-testing/src/mock_chain/auth.rs b/crates/miden-testing/src/mock_chain/auth.rs index a389046d18..5b7f06b06a 100644 --- a/crates/miden-testing/src/mock_chain/auth.rs +++ b/crates/miden-testing/src/mock_chain/auth.rs @@ -9,9 +9,12 @@ use miden_protocol::testing::noop_auth_component::NoopAuthComponent; use miden_standards::account::auth::{ AuthMultisig, AuthMultisigConfig, + AuthMultisigPsm, + AuthMultisigPsmConfig, AuthSingleSig, AuthSingleSigAcl, AuthSingleSigAclConfig, + PsmConfig, }; use miden_standards::testing::account_component::{ ConditionalAuthComponent, @@ -31,7 +34,15 @@ pub enum Auth { /// Multisig Multisig { threshold: u32, - approvers: Vec<(Word, AuthScheme)>, + approvers: Vec<(PublicKeyCommitment, AuthScheme)>, + proc_threshold_map: Vec<(Word, u32)>, + }, + + /// Multisig with a private state manager. + MultisigPsm { + threshold: u32, + approvers: Vec<(PublicKeyCommitment, AuthScheme)>, + psm_config: PsmConfig, proc_threshold_map: Vec<(Word, u32)>, }, @@ -77,14 +88,7 @@ impl Auth { (component, Some(authenticator)) }, Auth::Multisig { threshold, approvers, proc_threshold_map } => { - let approvers = approvers - .iter() - .map(|(pub_key, auth_scheme)| { - (PublicKeyCommitment::from(*pub_key), *auth_scheme) - }) - .collect(); - - let config = AuthMultisigConfig::new(approvers, *threshold) + let config = AuthMultisigConfig::new(approvers.clone(), *threshold) .and_then(|cfg| cfg.with_proc_thresholds(proc_threshold_map.clone())) .expect("invalid multisig config"); let component = @@ -92,6 +96,21 @@ impl Auth { (component, None) }, + Auth::MultisigPsm { + threshold, + approvers, + psm_config, + proc_threshold_map, + } => { + let config = AuthMultisigPsmConfig::new(approvers.clone(), *threshold, *psm_config) + .and_then(|cfg| cfg.with_proc_thresholds(proc_threshold_map.clone())) + .expect("invalid multisig psm config"); + let component = AuthMultisigPsm::new(config) + .expect("multisig psm component creation failed") + .into(); + + (component, None) + }, Auth::Acl { auth_trigger_procedures, allow_unauthorized_output_notes, diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 1d579d3218..3b30c58f32 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -116,12 +116,12 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // CREATE BRIDGE ADMIN ACCOUNT (not used in this test, but distinct from GER manager) // -------------------------------------------------------------------------------------------- let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE GER MANAGER ACCOUNT (sends the UPDATE_GER note) // -------------------------------------------------------------------------------------------- let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE BRIDGE ACCOUNT // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index ce2b417b2a..b065acfaf4 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -104,11 +104,11 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE GER MANAGER ACCOUNT (not used in this test, but distinct from admin) let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; let mut bridge_account = create_existing_bridge_account( builder.rng_mut().draw_word(), @@ -310,11 +310,11 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> // CREATE BRIDGE ADMIN ACCOUNT let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE GER MANAGER ACCOUNT (not used in this test, but distinct from admin) let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE BRIDGE ACCOUNT (empty faucet registry — no faucets registered) // -------------------------------------------------------------------------------------------- @@ -408,11 +408,11 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // Create a bridge admin account let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // Create a GER manager account (not used in this test, but distinct from admin) let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // Create a bridge account (includes a `bridge` component) let bridge_account = create_existing_bridge_account( @@ -424,7 +424,7 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // Create a user account that will create and consume the B2AGG note let mut user_account = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE B2AGG NOTE WITH USER ACCOUNT AS SENDER // -------------------------------------------------------------------------------------------- @@ -519,11 +519,11 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { // Create a bridge admin account let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // Create a GER manager account (not used in this test, but distinct from admin) let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // Create a bridge account as the designated TARGET for the B2AGG note let bridge_account = create_existing_bridge_account( @@ -535,7 +535,7 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { // Create a user account as the SENDER of the B2AGG note let sender_account = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // Create a "malicious" account with a bridge interface let malicious_account = create_existing_bridge_account( diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index b9f7dcfbbc..d56643ebb2 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -27,11 +27,11 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { // CREATE BRIDGE ADMIN ACCOUNT (note sender) let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE GER MANAGER ACCOUNT (not used in this test, but distinct from admin) let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE BRIDGE ACCOUNT (starts with empty faucet registry) let bridge_account = create_existing_bridge_account( diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 910046db96..2070f66b3e 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -58,12 +58,12 @@ async fn update_ger_note_updates_storage() -> anyhow::Result<()> { // CREATE BRIDGE ADMIN ACCOUNT (not used in this test, but distinct from GER manager) // -------------------------------------------------------------------------------------------- let bridge_admin = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE GER MANAGER ACCOUNT (note sender) // -------------------------------------------------------------------------------------------- let ger_manager = - builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; + builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2 })?; // CREATE BRIDGE ACCOUNT // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-testing/tests/auth/hybrid_multisig.rs b/crates/miden-testing/tests/auth/hybrid_multisig.rs index f2be9dd7da..a1594175d7 100644 --- a/crates/miden-testing/tests/auth/hybrid_multisig.rs +++ b/crates/miden-testing/tests/auth/hybrid_multisig.rs @@ -19,14 +19,12 @@ use miden_protocol::vm::AdviceMap; use miden_protocol::{Felt, Hasher, Word}; use miden_standards::account::auth::AuthMultisig; use miden_standards::account::components::multisig_library; -use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; -use miden_standards::errors::standards::ERR_TX_ALREADY_EXECUTED; use miden_standards::note::P2idNote; use miden_standards::testing::account_interface::get_public_keys_from_account; use miden_testing::utils::create_spawn_note; -use miden_testing::{Auth, MockChainBuilder, assert_transaction_executor_error}; +use miden_testing::{Auth, MockChainBuilder}; use miden_tx::TransactionExecutorError; use miden_tx::auth::{BasicAuthenticator, SigningInputs, TransactionAuthenticator}; use rand::SeedableRng; @@ -89,7 +87,7 @@ fn create_multisig_account( ) -> anyhow::Result { let approvers = approvers .iter() - .map(|(pub_key, auth_scheme)| (pub_key.to_commitment().into(), *auth_scheme)) + .map(|(pub_key, auth_scheme)| (pub_key.to_commitment(), *auth_scheme)) .collect(); let multisig_account = AccountBuilder::new([0; 32]) @@ -285,89 +283,6 @@ async fn test_multisig_2_of_4_all_signer_combinations() -> anyhow::Result<()> { Ok(()) } -/// Tests multisig replay protection to prevent transaction re-execution. -/// -/// This test verifies that a 2-of-3 multisig account properly prevents replay attacks -/// by rejecting attempts to execute the same transaction twice. The first execution -/// should succeed with valid signatures, but the second attempt with identical -/// parameters should fail with ERR_TX_ALREADY_EXECUTED. -/// -/// **Roles:** -/// - 3 Approvers (2 signers required) -/// - 1 Multisig Contract -#[tokio::test] -async fn test_multisig_replay_protection() -> anyhow::Result<()> { - // Setup keys and authenticators (3 approvers, but only 2 signers) - let (_secret_keys, auth_schemes, public_keys, authenticators) = - setup_keys_and_authenticators(3, 2)?; - - let approvers = public_keys - .iter() - .zip(auth_schemes.iter()) - .map(|(pk, scheme)| (pk.clone(), *scheme)) - .collect::>(); - - // Create 2/3 multisig account - let multisig_account = create_multisig_account(2, &approvers, 20, vec![])?; - - let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) - .unwrap() - .build() - .unwrap(); - - let salt = Word::from([Felt::new(3); 4]); - - // Execute transaction without signatures first to get tx summary - let tx_context_init = mock_chain - .build_tx_context(multisig_account.id(), &[], &[])? - .auth_args(salt) - .build()?; - - let tx_summary = match tx_context_init.execute().await.unwrap_err() { - TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, - error => panic!("expected abort with tx effects: {error:?}"), - }; - - // Get signatures from 2 of the 3 approvers - let msg = tx_summary.as_ref().to_commitment(); - let tx_summary = SigningInputs::TransactionSummary(tx_summary); - - let sig_1 = authenticators[0] - .get_signature(public_keys[0].to_commitment(), &tx_summary) - .await?; - let sig_2 = authenticators[1] - .get_signature(public_keys[1].to_commitment(), &tx_summary) - .await?; - - // Execute transaction with signatures - should succeed (first execution) - let tx_context_execute = mock_chain - .build_tx_context(multisig_account.id(), &[], &[])? - .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) - .add_signature(public_keys[1].to_commitment(), msg, sig_2.clone()) - .auth_args(salt) - .build()?; - - let executed_tx = tx_context_execute.execute().await.expect("First transaction should succeed"); - - // Apply the transaction to the mock chain - mock_chain.add_pending_executed_transaction(&executed_tx)?; - mock_chain.prove_next_block()?; - - // Attempt to execute the same transaction again - should fail due to replay protection - let tx_context_replay = mock_chain - .build_tx_context(multisig_account.id(), &[], &[])? - .add_signature(public_keys[0].to_commitment(), msg, sig_1) - .add_signature(public_keys[1].to_commitment(), msg, sig_2) - .auth_args(salt) - .build()?; - - // This should fail due to replay protection - let result = tx_context_replay.execute().await; - assert_transaction_executor_error!(result, ERR_TX_ALREADY_EXECUTED); - - Ok(()) -} - /// Tests multisig signer update functionality. /// /// This test verifies that a multisig account can: @@ -994,168 +909,3 @@ async fn test_multisig_new_approvers_cannot_sign_before_update() -> anyhow::Resu Ok(()) } - -/// Tests that 1-of-2 approvers can consume a note but 2-of-2 are required to send a note. -/// -/// This test verifies that a multisig account with 2 approvers and threshold 2, but a procedure -/// threshold of 1 for note consumption, can: -/// 1. Consume a note when only one approver signs the transaction -/// 2. Send a note only when both approvers sign the transaction (default threshold) -#[tokio::test] -async fn test_multisig_proc_threshold_overrides() -> anyhow::Result<()> { - // Setup keys and authenticators - let (_secret_keys, auth_schemes, public_keys, authenticators) = - setup_keys_and_authenticators(2, 2)?; - - let proc_threshold_map = vec![(BasicWallet::receive_asset_digest(), 1)]; - - let approvers = public_keys - .iter() - .zip(auth_schemes.iter()) - .map(|(pk, scheme)| (pk.clone(), *scheme)) - .collect::>(); - - // Create multisig account - let multisig_starting_balance = 10u64; - let mut multisig_account = - create_multisig_account(2, &approvers, multisig_starting_balance, proc_threshold_map)?; - - // SECTION 1: Test note consumption with 1 signature - // ================================================================================ - - // 1. create a mock note from some random account - let mut mock_chain_builder = - MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); - - let note = mock_chain_builder.add_p2id_note( - multisig_account.id(), - multisig_account.id(), - &[FungibleAsset::mock(1)], - NoteType::Public, - )?; - - let mut mock_chain = mock_chain_builder.build()?; - - // 2. consume without signatures - let salt = Word::from([Felt::new(1); 4]); - let tx_context = mock_chain - .build_tx_context(multisig_account.id(), &[note.id()], &[])? - .auth_args(salt) - .build()?; - - let tx_summary = match tx_context.execute().await.unwrap_err() { - TransactionExecutorError::Unauthorized(tx_summary) => tx_summary, - error => panic!("expected abort with tx summary: {error:?}"), - }; - - // 3. get signature from one approver - let msg = tx_summary.as_ref().to_commitment(); - let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary.clone()); - let sig = authenticators[0] - .get_signature(public_keys[0].to_commitment(), &tx_summary_signing) - .await?; - - // 4. execute with signature - let tx_result = mock_chain - .build_tx_context(multisig_account.id(), &[note.id()], &[])? - .add_signature(public_keys[0].to_commitment(), msg, sig) - .auth_args(salt) - .build()? - .execute() - .await; - - assert!(tx_result.is_ok(), "Note consumption with 1 signature should succeed"); - - // Apply the transaction to the account - multisig_account.apply_delta(tx_result.as_ref().unwrap().account_delta())?; - mock_chain.add_pending_executed_transaction(&tx_result.unwrap())?; - mock_chain.prove_next_block()?; - - // SECTION 2: Test note sending requires 2 signatures - // ================================================================================ - - let salt2 = Word::from([Felt::new(2); 4]); - - // Create output note to send 5 units from the account - let output_note = P2idNote::create( - multisig_account.id(), - ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), - vec![FungibleAsset::mock(5)], - NoteType::Public, - Default::default(), - &mut RpoRandomCoin::new(Word::from([Felt::new(42); 4])), - )?; - let multisig_account_interface = AccountInterface::from_account(&multisig_account); - let send_note_transaction_script = - multisig_account_interface.build_send_notes_script(&[output_note.clone().into()], None)?; - - // Execute transaction without signatures to get tx summary - let tx_context_init = mock_chain - .build_tx_context(multisig_account.id(), &[], &[])? - .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) - .tx_script(send_note_transaction_script.clone()) - .auth_args(salt2) - .build()?; - - let tx_summary2 = match tx_context_init.execute().await.unwrap_err() { - TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, - error => panic!("expected abort with tx effects: {error:?}"), - }; - // Get signature from only ONE approver - let msg2 = tx_summary2.as_ref().to_commitment(); - let tx_summary2_signing = SigningInputs::TransactionSummary(tx_summary2.clone()); - - let sig_1 = authenticators[0] - .get_signature(public_keys[0].to_commitment(), &tx_summary2_signing) - .await?; - - // Try to execute with only 1 signature - should FAIL - let tx_context_one_sig = mock_chain - .build_tx_context(multisig_account.id(), &[], &[])? - .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) - .add_signature(public_keys[0].to_commitment(), msg2, sig_1) - .tx_script(send_note_transaction_script.clone()) - .auth_args(salt2) - .build()?; - - let result = tx_context_one_sig.execute().await; - match result { - Err(TransactionExecutorError::Unauthorized(_)) => { - // Expected: transaction should fail with insufficient signatures - }, - _ => panic!( - "Transaction should fail with Unauthorized error when only 1 signature provided for note sending" - ), - } - - // Now get signatures from BOTH approvers - let sig_1 = authenticators[0] - .get_signature(public_keys[0].to_commitment(), &tx_summary2_signing) - .await?; - let sig_2 = authenticators[1] - .get_signature(public_keys[1].to_commitment(), &tx_summary2_signing) - .await?; - - // Execute with 2 signatures - should SUCCEED - let result = mock_chain - .build_tx_context(multisig_account.id(), &[], &[])? - .extend_expected_output_notes(vec![OutputNote::Full(output_note)]) - .add_signature(public_keys[0].to_commitment(), msg2, sig_1) - .add_signature(public_keys[1].to_commitment(), msg2, sig_2) - .auth_args(salt2) - .tx_script(send_note_transaction_script) - .build()? - .execute() - .await; - - assert!(result.is_ok(), "Transaction should succeed with 2 signatures for note sending"); - - // Apply the transaction to the account - multisig_account.apply_delta(result.as_ref().unwrap().account_delta())?; - mock_chain.add_pending_executed_transaction(&result.unwrap())?; - mock_chain.prove_next_block()?; - - assert_eq!(multisig_account.vault().get_balance(FungibleAsset::mock_issuer())?, 6); - - Ok(()) -} diff --git a/crates/miden-testing/tests/auth/mod.rs b/crates/miden-testing/tests/auth/mod.rs index d7536fd219..33d6f35bde 100644 --- a/crates/miden-testing/tests/auth/mod.rs +++ b/crates/miden-testing/tests/auth/mod.rs @@ -3,3 +3,5 @@ mod singlesig_acl; mod multisig; mod hybrid_multisig; + +mod multisig_psm; diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index 734cce269a..0690accafc 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -22,7 +22,10 @@ use miden_standards::account::components::multisig_library; use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; -use miden_standards::errors::standards::ERR_TX_ALREADY_EXECUTED; +use miden_standards::errors::standards::{ + ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS, + ERR_TX_ALREADY_EXECUTED, +}; use miden_standards::note::P2idNote; use miden_standards::testing::account_interface::get_public_keys_from_account; use miden_testing::utils::create_spawn_note; @@ -87,7 +90,7 @@ fn create_multisig_account( ) -> anyhow::Result { let approvers = approvers .iter() - .map(|(pub_key, auth_scheme)| (pub_key.to_commitment().into(), *auth_scheme)) + .map(|(pub_key, auth_scheme)| (pub_key.to_commitment(), *auth_scheme)) .collect(); let multisig_account = AccountBuilder::new([0; 32]) @@ -864,6 +867,78 @@ async fn test_multisig_update_signers_remove_owner( Ok(()) } +/// Tests that signer updates are rejected when stored procedure threshold overrides would become +/// unreachable for the new signer set. +#[rstest] +#[case::ecdsa(AuthScheme::EcdsaK256Keccak)] +#[case::falcon(AuthScheme::Falcon512Poseidon2)] +#[tokio::test] +async fn test_multisig_update_signers_rejects_unreachable_proc_thresholds( + #[case] auth_scheme: AuthScheme, +) -> anyhow::Result<()> { + let (_secret_keys, auth_schemes, public_keys, _authenticators) = + setup_keys_and_authenticators_with_scheme(3, 2, auth_scheme)?; + + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + // Configure a procedure override that is valid for the initial signer set (3-of-3), + // but invalid after updating to 2 signers. + let multisig_account = + create_multisig_account(2, &approvers, 10, vec![(BasicWallet::receive_asset_digest(), 3)])?; + + let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) + .unwrap() + .build() + .unwrap(); + + let new_public_keys = &public_keys[0..2]; + let threshold = 2u64; + let num_of_approvers = 2u64; + + let mut config_and_pubkeys_vector = + vec![Felt::new(threshold), Felt::new(num_of_approvers), Felt::new(0), Felt::new(0)]; + + for public_key in new_public_keys.iter().rev() { + let key_word: Word = public_key.to_commitment().into(); + config_and_pubkeys_vector.extend_from_slice(key_word.as_elements()); + config_and_pubkeys_vector.extend_from_slice(&[ + Felt::new(auth_scheme as u64), + Felt::new(0), + Felt::new(0), + Felt::new(0), + ]); + } + + let multisig_config_hash = Hasher::hash_elements(&config_and_pubkeys_vector); + let mut advice_map = AdviceMap::default(); + advice_map.insert(multisig_config_hash, config_and_pubkeys_vector); + + let tx_script = CodeBuilder::default() + .with_dynamically_linked_library(multisig_library())? + .compile_tx_script("begin\n call.::multisig::update_signers_and_threshold\nend")?; + + let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; + let salt = Word::from([Felt::new(8); 4]); + + let result = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(tx_script) + .tx_script_args(multisig_config_hash) + .extend_advice_inputs(advice_inputs) + .auth_args(salt) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS); + + Ok(()) +} + /// Tests that newly added approvers cannot sign transactions before the signer update is executed. /// /// This is a regression test to ensure that unauthorized parties cannot add their own public keys @@ -1186,3 +1261,251 @@ async fn test_multisig_proc_threshold_overrides( Ok(()) } + +/// Tests setting a per-procedure threshold override and clearing it via `proc_threshold == 0`. +#[rstest] +#[case::ecdsa(AuthScheme::EcdsaK256Keccak)] +#[case::falcon(AuthScheme::Falcon512Poseidon2)] +#[tokio::test] +async fn test_multisig_set_procedure_threshold( + #[case] auth_scheme: AuthScheme, +) -> anyhow::Result<()> { + let (_secret_keys, auth_schemes, public_keys, authenticators) = + setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?; + + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + let mut multisig_account = create_multisig_account(2, &approvers, 10, vec![])?; + let mut mock_chain_builder = + MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); + let one_sig_note = mock_chain_builder.add_p2id_note( + multisig_account.id(), + multisig_account.id(), + &[FungibleAsset::mock(1)], + NoteType::Public, + )?; + let clear_check_note = mock_chain_builder.add_p2id_note( + multisig_account.id(), + multisig_account.id(), + &[FungibleAsset::mock(1)], + NoteType::Public, + )?; + let mut mock_chain = mock_chain_builder.build().unwrap(); + let proc_root = BasicWallet::receive_asset_digest(); + + let set_script_code = format!( + r#" + begin + push.{proc_root} + push.1 + call.::multisig::set_procedure_threshold + dropw + drop + end + "# + ); + let set_script = CodeBuilder::default() + .with_dynamically_linked_library(multisig_library())? + .compile_tx_script(set_script_code)?; + + // 1) Set override to 1 (requires default 2 signatures). + let set_salt = Word::from([Felt::new(50); 4]); + + let set_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(set_script.clone()) + .auth_args(set_salt) + .build()?; + let set_summary = match set_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => panic!("expected abort with tx effects: {error:?}"), + }; + let set_msg = set_summary.as_ref().to_commitment(); + let set_summary = SigningInputs::TransactionSummary(set_summary); + let set_sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &set_summary) + .await?; + let set_sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &set_summary) + .await?; + + let set_tx = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(set_script) + .add_signature(public_keys[0].to_commitment(), set_msg, set_sig_1) + .add_signature(public_keys[1].to_commitment(), set_msg, set_sig_2) + .auth_args(set_salt) + .build()? + .execute() + .await?; + + multisig_account.apply_delta(set_tx.account_delta())?; + mock_chain.add_pending_executed_transaction(&set_tx)?; + mock_chain.prove_next_block()?; + + // 2) Verify receive_asset can now execute with one signature. + let one_sig_salt = Word::from([Felt::new(51); 4]); + + let one_sig_init = mock_chain + .build_tx_context(multisig_account.id(), &[one_sig_note.id()], &[])? + .auth_args(one_sig_salt) + .build()?; + let one_sig_summary = match one_sig_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => panic!("expected abort with tx effects: {error:?}"), + }; + let one_sig_msg = one_sig_summary.as_ref().to_commitment(); + let one_sig_summary = SigningInputs::TransactionSummary(one_sig_summary); + let one_sig = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &one_sig_summary) + .await?; + + let one_sig_tx = mock_chain + .build_tx_context(multisig_account.id(), &[one_sig_note.id()], &[])? + .add_signature(public_keys[0].to_commitment(), one_sig_msg, one_sig) + .auth_args(one_sig_salt) + .build()? + .execute() + .await + .expect("override=1 should allow receive_asset with one signature"); + multisig_account.apply_delta(one_sig_tx.account_delta())?; + mock_chain.add_pending_executed_transaction(&one_sig_tx)?; + mock_chain.prove_next_block()?; + + // 3) Clear override by setting threshold to zero. + let clear_script_code = format!( + r#" + begin + push.{proc_root} + push.0 + call.::multisig::set_procedure_threshold + dropw + drop + end + "# + ); + let clear_script = CodeBuilder::default() + .with_dynamically_linked_library(multisig_library())? + .compile_tx_script(clear_script_code)?; + let clear_salt = Word::from([Felt::new(52); 4]); + + let clear_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(clear_script.clone()) + .auth_args(clear_salt) + .build()?; + let clear_summary = match clear_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => panic!("expected abort with tx effects: {error:?}"), + }; + let clear_msg = clear_summary.as_ref().to_commitment(); + let clear_summary = SigningInputs::TransactionSummary(clear_summary); + let clear_sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &clear_summary) + .await?; + let clear_sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &clear_summary) + .await?; + + let clear_tx = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(clear_script) + .add_signature(public_keys[0].to_commitment(), clear_msg, clear_sig_1) + .add_signature(public_keys[1].to_commitment(), clear_msg, clear_sig_2) + .auth_args(clear_salt) + .build()? + .execute() + .await?; + + multisig_account.apply_delta(clear_tx.account_delta())?; + mock_chain.add_pending_executed_transaction(&clear_tx)?; + mock_chain.prove_next_block()?; + + // 4) After clear, one signature should no longer be sufficient for receive_asset. + let clear_check_salt = Word::from([Felt::new(53); 4]); + + let clear_check_init = mock_chain + .build_tx_context(multisig_account.id(), &[clear_check_note.id()], &[])? + .auth_args(clear_check_salt) + .build()?; + let clear_check_summary = match clear_check_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => panic!("expected abort with tx effects: {error:?}"), + }; + let clear_check_msg = clear_check_summary.as_ref().to_commitment(); + let clear_check_summary = SigningInputs::TransactionSummary(clear_check_summary); + let clear_check_sig = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &clear_check_summary) + .await?; + + let clear_check_result = mock_chain + .build_tx_context(multisig_account.id(), &[clear_check_note.id()], &[])? + .add_signature(public_keys[0].to_commitment(), clear_check_msg, clear_check_sig) + .auth_args(clear_check_salt) + .build()? + .execute() + .await; + + assert!( + matches!(clear_check_result, Err(TransactionExecutorError::Unauthorized(_))), + "override cleared via threshold=0 should restore default threshold requirements" + ); + + Ok(()) +} + +/// Tests setting an override threshold above num_approvers is rejected. +#[rstest] +#[case::ecdsa(AuthScheme::EcdsaK256Keccak)] +#[case::falcon(AuthScheme::Falcon512Poseidon2)] +#[tokio::test] +async fn test_multisig_set_procedure_threshold_rejects_exceeding_approvers( + #[case] auth_scheme: AuthScheme, +) -> anyhow::Result<()> { + let (_secret_keys, auth_schemes, public_keys, _authenticators) = + setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?; + + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + let multisig_account = create_multisig_account(2, &approvers, 10, vec![])?; + let proc_root = BasicWallet::receive_asset_digest(); + + let script_code = format!( + r#" + begin + push.{proc_root} + push.3 + call.::multisig::set_procedure_threshold + end + "# + ); + let script = CodeBuilder::default() + .with_dynamically_linked_library(multisig_library())? + .compile_tx_script(script_code)?; + + let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) + .unwrap() + .build() + .unwrap(); + let salt = Word::from([Felt::new(54); 4]); + + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(script.clone()) + .auth_args(salt) + .build()?; + + let result = tx_context_init.execute().await; + + assert_transaction_executor_error!(result, ERR_PROC_THRESHOLD_EXCEEDS_NUM_APPROVERS); + + Ok(()) +} diff --git a/crates/miden-testing/tests/auth/multisig_psm.rs b/crates/miden-testing/tests/auth/multisig_psm.rs new file mode 100644 index 0000000000..2e50c688c5 --- /dev/null +++ b/crates/miden-testing/tests/auth/multisig_psm.rs @@ -0,0 +1,531 @@ +use miden_protocol::account::auth::{AuthScheme, AuthSecretKey, PublicKey}; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountId, + AccountStorageMode, + AccountType, +}; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, +}; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, Word}; +use miden_standards::account::auth::{AuthMultisigPsm, AuthMultisigPsmConfig, PsmConfig}; +use miden_standards::account::components::multisig_psm_library; +use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::{ + ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE, + ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES, +}; +use miden_testing::{MockChainBuilder, assert_transaction_executor_error}; +use miden_tx::TransactionExecutorError; +use miden_tx::auth::{BasicAuthenticator, SigningInputs, TransactionAuthenticator}; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use rstest::rstest; + +// ================================================================================================ +// HELPER FUNCTIONS +// ================================================================================================ + +type MultisigTestSetup = + (Vec, Vec, Vec, Vec); + +/// Sets up secret keys, public keys, and authenticators for multisig testing for the given scheme. +fn setup_keys_and_authenticators_with_scheme( + num_approvers: usize, + threshold: usize, + auth_scheme: AuthScheme, +) -> anyhow::Result { + let seed: [u8; 32] = rand::random(); + let mut rng = ChaCha20Rng::from_seed(seed); + + let mut secret_keys = Vec::new(); + let mut auth_schemes = Vec::new(); + let mut public_keys = Vec::new(); + let mut authenticators = Vec::new(); + + for _ in 0..num_approvers { + let sec_key = match auth_scheme { + AuthScheme::EcdsaK256Keccak => AuthSecretKey::new_ecdsa_k256_keccak_with_rng(&mut rng), + AuthScheme::Falcon512Poseidon2 => { + AuthSecretKey::new_falcon512_poseidon2_with_rng(&mut rng) + }, + _ => anyhow::bail!("unsupported auth scheme for this test: {auth_scheme:?}"), + }; + let pub_key = sec_key.public_key(); + + secret_keys.push(sec_key); + auth_schemes.push(auth_scheme); + public_keys.push(pub_key); + } + + // Create authenticators for required signers + for secret_key in secret_keys.iter().take(threshold) { + let authenticator = BasicAuthenticator::new(core::slice::from_ref(secret_key)); + authenticators.push(authenticator); + } + + Ok((secret_keys, auth_schemes, public_keys, authenticators)) +} + +/// Creates a multisig account configured with a private state manager signer. +fn create_multisig_account_with_psm( + threshold: u32, + approvers: &[(PublicKey, AuthScheme)], + psm: PsmConfig, + asset_amount: u64, + proc_threshold_map: Vec<(Word, u32)>, +) -> anyhow::Result { + let approvers = approvers + .iter() + .map(|(pub_key, auth_scheme)| (pub_key.to_commitment(), *auth_scheme)) + .collect(); + + let config = AuthMultisigPsmConfig::new(approvers, threshold, psm)? + .with_proc_thresholds(proc_threshold_map)?; + + let multisig_account = AccountBuilder::new([0; 32]) + .with_auth_component(AuthMultisigPsm::new(config)?) + .with_component(BasicWallet) + .account_type(AccountType::RegularAccountUpdatableCode) + .storage_mode(AccountStorageMode::Public) + .with_assets(vec![FungibleAsset::mock(asset_amount)]) + .build_existing()?; + + Ok(multisig_account) +} + +// ================================================================================================ +// TESTS +// ================================================================================================ + +/// Tests that multisig authentication requires an additional PSM signature when +/// configured. +#[rstest] +#[case::ecdsa(AuthScheme::EcdsaK256Keccak)] +#[case::falcon(AuthScheme::Falcon512Poseidon2)] +#[tokio::test] +async fn test_multisig_psm_signature_required( + #[case] auth_scheme: AuthScheme, +) -> anyhow::Result<()> { + let (_secret_keys, auth_schemes, public_keys, authenticators) = + setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?; + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + let psm_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let psm_public_key = psm_secret_key.public_key(); + let psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&psm_secret_key)); + + let mut multisig_account = create_multisig_account_with_psm( + 2, + &approvers, + PsmConfig::new(psm_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), + 10, + vec![], + )?; + + let output_note_asset = FungibleAsset::mock(0); + let mut mock_chain_builder = + MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); + + let output_note = mock_chain_builder.add_p2id_note( + multisig_account.id(), + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE.try_into().unwrap(), + &[output_note_asset], + NoteType::Public, + )?; + let input_note = mock_chain_builder.add_spawn_note([&output_note])?; + let mut mock_chain = mock_chain_builder.build().unwrap(); + + let salt = Word::from([Felt::new(777); 4]); + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[input_note.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .auth_args(salt) + .build()?; + + let tx_summary = match tx_context_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => anyhow::bail!("expected abort with tx effects: {error}"), + }; + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary); + + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary_signing) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) + .await?; + + // Missing PSM signature must fail. + let without_psm_result = mock_chain + .build_tx_context(multisig_account.id(), &[input_note.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) + .add_signature(public_keys[1].to_commitment(), msg, sig_2.clone()) + .auth_args(salt) + .build()? + .execute() + .await; + assert!(matches!(without_psm_result, Err(TransactionExecutorError::Unauthorized(_)))); + + let psm_signature = psm_authenticator + .get_signature(psm_public_key.to_commitment(), &tx_summary_signing) + .await?; + + // With PSM signature the transaction should succeed. + let tx_context_execute = mock_chain + .build_tx_context(multisig_account.id(), &[input_note.id()], &[])? + .extend_expected_output_notes(vec![OutputNote::Full(output_note)]) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) + .add_signature(psm_public_key.to_commitment(), msg, psm_signature) + .auth_args(salt) + .build()? + .execute() + .await?; + + multisig_account.apply_delta(tx_context_execute.account_delta())?; + + mock_chain.add_pending_executed_transaction(&tx_context_execute)?; + mock_chain.prove_next_block()?; + + assert_eq!( + multisig_account + .vault() + .get_balance(AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?)?, + 10 - output_note_asset.unwrap_fungible().amount() + ); + + Ok(()) +} + +/// Tests that the PSM public key can be updated and then enforced. +#[rstest] +#[case::ecdsa(AuthScheme::EcdsaK256Keccak)] +#[case::falcon(AuthScheme::Falcon512Poseidon2)] +#[tokio::test] +async fn test_multisig_update_psm_public_key( + #[case] auth_scheme: AuthScheme, +) -> anyhow::Result<()> { + let (_secret_keys, auth_schemes, public_keys, authenticators) = + setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?; + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + let old_psm_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let old_psm_public_key = old_psm_secret_key.public_key(); + let old_psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&old_psm_secret_key)); + + let new_psm_secret_key = AuthSecretKey::new_falcon512_poseidon2(); + let new_psm_public_key = new_psm_secret_key.public_key(); + let new_psm_auth_scheme = new_psm_secret_key.auth_scheme(); + let new_psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&new_psm_secret_key)); + + let multisig_account = create_multisig_account_with_psm( + 2, + &approvers, + PsmConfig::new(old_psm_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), + 10, + vec![], + )?; + + let mut mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) + .unwrap() + .build() + .unwrap(); + + let new_psm_key_word: Word = new_psm_public_key.to_commitment().into(); + let new_psm_scheme_id = new_psm_auth_scheme as u32; + let update_psm_script = CodeBuilder::new() + .with_dynamically_linked_library(multisig_psm_library())? + .compile_tx_script(format!( + "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::multisig_psm::update_psm_public_key\n drop\n dropw\nend" + ))?; + + let update_salt = Word::from([Felt::new(991); 4]); + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(update_psm_script.clone()) + .auth_args(update_salt) + .build()?; + + let tx_summary = match tx_context_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => anyhow::bail!("expected abort with tx effects: {error}"), + }; + + let update_msg = tx_summary.as_ref().to_commitment(); + let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary); + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary_signing) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) + .await?; + + // PSM key rotation intentionally skips PSM signature for this update tx. + let update_psm_tx = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(update_psm_script) + .add_signature(public_keys[0].to_commitment(), update_msg, sig_1) + .add_signature(public_keys[1].to_commitment(), update_msg, sig_2) + .auth_args(update_salt) + .build()? + .execute() + .await?; + + let mut updated_multisig_account = multisig_account.clone(); + updated_multisig_account.apply_delta(update_psm_tx.account_delta())?; + let updated_psm_public_key = updated_multisig_account + .storage() + .get_map_item(AuthMultisigPsm::psm_public_key_slot(), Word::empty())?; + assert_eq!(updated_psm_public_key, Word::from(new_psm_public_key.to_commitment())); + let updated_psm_scheme_id = updated_multisig_account + .storage() + .get_map_item(AuthMultisigPsm::psm_scheme_id_slot(), Word::from([0u32, 0, 0, 0]))?; + assert_eq!( + updated_psm_scheme_id, + Word::from([new_psm_auth_scheme as u32, 0u32, 0u32, 0u32]) + ); + + mock_chain.add_pending_executed_transaction(&update_psm_tx)?; + mock_chain.prove_next_block()?; + + // Build one tx summary after key update. Old PSM must fail and new PSM must pass on this same + // transaction. + let next_salt = Word::from([Felt::new(992); 4]); + let tx_context_init_next = mock_chain + .build_tx_context(updated_multisig_account.id(), &[], &[])? + .auth_args(next_salt) + .build()?; + + let tx_summary_next = match tx_context_init_next.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => anyhow::bail!("expected abort with tx effects: {error}"), + }; + let next_msg = tx_summary_next.as_ref().to_commitment(); + let tx_summary_next_signing = SigningInputs::TransactionSummary(tx_summary_next); + + let next_sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary_next_signing) + .await?; + let next_sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary_next_signing) + .await?; + let old_psm_sig_next = old_psm_authenticator + .get_signature(old_psm_public_key.to_commitment(), &tx_summary_next_signing) + .await?; + let new_psm_sig_next = new_psm_authenticator + .get_signature(new_psm_public_key.to_commitment(), &tx_summary_next_signing) + .await?; + + // Old PSM signature must fail after key update. + let with_old_psm_result = mock_chain + .build_tx_context(updated_multisig_account.id(), &[], &[])? + .add_signature(public_keys[0].to_commitment(), next_msg, next_sig_1.clone()) + .add_signature(public_keys[1].to_commitment(), next_msg, next_sig_2.clone()) + .add_signature(old_psm_public_key.to_commitment(), next_msg, old_psm_sig_next) + .auth_args(next_salt) + .build()? + .execute() + .await; + assert!(matches!(with_old_psm_result, Err(TransactionExecutorError::Unauthorized(_)))); + + // New PSM signature must pass. + mock_chain + .build_tx_context(updated_multisig_account.id(), &[], &[])? + .add_signature(public_keys[0].to_commitment(), next_msg, next_sig_1) + .add_signature(public_keys[1].to_commitment(), next_msg, next_sig_2) + .add_signature(new_psm_public_key.to_commitment(), next_msg, new_psm_sig_next) + .auth_args(next_salt) + .build()? + .execute() + .await?; + + Ok(()) +} + +/// Tests that `update_psm_public_key` must be the only account action in the transaction. +#[rstest] +#[case::ecdsa(AuthScheme::EcdsaK256Keccak)] +#[case::falcon(AuthScheme::Falcon512Poseidon2)] +#[tokio::test] +async fn test_multisig_update_psm_public_key_must_be_called_alone( + #[case] auth_scheme: AuthScheme, +) -> anyhow::Result<()> { + let (_secret_keys, auth_schemes, public_keys, authenticators) = + setup_keys_and_authenticators_with_scheme(2, 2, auth_scheme)?; + let approvers = public_keys + .iter() + .zip(auth_schemes.iter()) + .map(|(pk, scheme)| (pk.clone(), *scheme)) + .collect::>(); + + let old_psm_secret_key = AuthSecretKey::new_ecdsa_k256_keccak(); + let old_psm_public_key = old_psm_secret_key.public_key(); + let old_psm_authenticator = BasicAuthenticator::new(core::slice::from_ref(&old_psm_secret_key)); + + let new_psm_secret_key = AuthSecretKey::new_falcon512_poseidon2(); + let new_psm_public_key = new_psm_secret_key.public_key(); + let new_psm_auth_scheme = new_psm_secret_key.auth_scheme(); + + let multisig_account = create_multisig_account_with_psm( + 2, + &approvers, + PsmConfig::new(old_psm_public_key.to_commitment(), AuthScheme::EcdsaK256Keccak), + 10, + vec![], + )?; + + let new_psm_key_word: Word = new_psm_public_key.to_commitment().into(); + let new_psm_scheme_id = new_psm_auth_scheme as u32; + let update_psm_script = CodeBuilder::new() + .with_dynamically_linked_library(multisig_psm_library())? + .compile_tx_script(format!( + "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::multisig_psm::update_psm_public_key\n drop\n dropw\nend" + ))?; + + let mut mock_chain_builder = + MockChainBuilder::with_accounts([multisig_account.clone()]).unwrap(); + let receive_asset_note = mock_chain_builder.add_p2id_note( + multisig_account.id(), + multisig_account.id(), + &[FungibleAsset::mock(1)], + NoteType::Public, + )?; + let mock_chain = mock_chain_builder.build().unwrap(); + + let salt = Word::from([Felt::new(993); 4]); + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[receive_asset_note.id()], &[])? + .tx_script(update_psm_script.clone()) + .auth_args(salt) + .build()?; + + let tx_summary = match tx_context_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => anyhow::bail!("expected abort with tx effects: {error}"), + }; + + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary); + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary_signing) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) + .await?; + + let without_psm_result = mock_chain + .build_tx_context(multisig_account.id(), &[receive_asset_note.id()], &[])? + .tx_script(update_psm_script.clone()) + .add_signature(public_keys[0].to_commitment(), msg, sig_1.clone()) + .add_signature(public_keys[1].to_commitment(), msg, sig_2.clone()) + .auth_args(salt) + .build()? + .execute() + .await; + assert_transaction_executor_error!(without_psm_result, ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE); + + let old_psm_signature = old_psm_authenticator + .get_signature(old_psm_public_key.to_commitment(), &tx_summary_signing) + .await?; + + let with_psm_result = mock_chain + .build_tx_context(multisig_account.id(), &[receive_asset_note.id()], &[])? + .tx_script(update_psm_script) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) + .add_signature(old_psm_public_key.to_commitment(), msg, old_psm_signature) + .auth_args(salt) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(with_psm_result, ERR_AUTH_PROCEDURE_MUST_BE_CALLED_ALONE); + + // Also reject rotation transactions that touch notes even when no other account procedure is + // called. + let note_script = CodeBuilder::default().compile_note_script("begin nop end")?; + let note_serial_num = Word::from([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]); + let note_recipient = + NoteRecipient::new(note_serial_num, note_script.clone(), NoteStorage::default()); + let output_note = Note::new( + NoteAssets::new(vec![])?, + NoteMetadata::new(multisig_account.id(), NoteType::Public), + note_recipient, + ); + + let new_psm_key_word: Word = new_psm_public_key.to_commitment().into(); + let new_psm_scheme_id = new_psm_auth_scheme as u32; + let update_psm_with_output_script = CodeBuilder::new() + .with_dynamically_linked_library(multisig_psm_library())? + .compile_tx_script(format!( + "use miden::protocol::output_note\nbegin\n push.{recipient}\n push.{note_type}\n push.{tag}\n exec.output_note::create\n swapdw\n dropw\n dropw\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::multisig_psm::update_psm_public_key\n drop\n dropw\nend", + recipient = output_note.recipient().digest(), + note_type = NoteType::Public as u8, + tag = Felt::from(output_note.metadata().tag()), + ))?; + + let mock_chain = MockChainBuilder::with_accounts([multisig_account.clone()]) + .unwrap() + .build() + .unwrap(); + + let salt = Word::from([Felt::new(994); 4]); + let tx_context_init = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(update_psm_with_output_script.clone()) + .add_note_script(note_script.clone()) + .extend_expected_output_notes(vec![OutputNote::Full(output_note.clone())]) + .auth_args(salt) + .build()?; + + let tx_summary = match tx_context_init.execute().await.unwrap_err() { + TransactionExecutorError::Unauthorized(tx_effects) => tx_effects, + error => anyhow::bail!("expected abort with tx effects: {error}"), + }; + + let msg = tx_summary.as_ref().to_commitment(); + let tx_summary_signing = SigningInputs::TransactionSummary(tx_summary); + let sig_1 = authenticators[0] + .get_signature(public_keys[0].to_commitment(), &tx_summary_signing) + .await?; + let sig_2 = authenticators[1] + .get_signature(public_keys[1].to_commitment(), &tx_summary_signing) + .await?; + + let result = mock_chain + .build_tx_context(multisig_account.id(), &[], &[])? + .tx_script(update_psm_with_output_script) + .add_note_script(note_script) + .extend_expected_output_notes(vec![OutputNote::Full(output_note)]) + .add_signature(public_keys[0].to_commitment(), msg, sig_1) + .add_signature(public_keys[1].to_commitment(), msg, sig_2) + .auth_args(salt) + .build()? + .execute() + .await; + + assert_transaction_executor_error!( + result, + ERR_AUTH_TRANSACTION_MUST_NOT_INCLUDE_INPUT_OR_OUTPUT_NOTES + ); + + Ok(()) +} From ac4637c717ae03ebf08eb1c8209c3b0d6763a554 Mon Sep 17 00:00:00 2001 From: Himess <95512809+Himess@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:21:32 +0300 Subject: [PATCH 25/56] chore: remove `ProvenTransactionBuilder` in favor of `ProvenTransaction::new` (#2567) --- CHANGELOG.md | 1 + .../src/batch/proposed_batch.rs | 19 +- crates/miden-protocol/src/transaction/mod.rs | 7 +- .../src/transaction/proven_tx.rs | 244 ++++++------------ .../kernel_tests/batch/proven_tx_builder.rs | 29 ++- .../src/kernel_tests/block/header_errors.rs | 19 +- crates/miden-tx/src/prover/mod.rs | 45 ++-- 7 files changed, 155 insertions(+), 209 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbc97a7e9..30e14e8831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). - Fixed MASM inline comment casing to adhere to commenting conventions ([#2398](https://github.com/0xMiden/miden-base/pull/2398)). +- [BREAKING] Removed `ProvenTransactionBuilder` in favor of `ProvenTransaction::new()` constructor ([#2567](https://github.com/0xMiden/miden-base/pull/2567)). - Removed redundant note storage item count from advice map ([#2376](https://github.com/0xMiden/miden-base/pull/2376)). - Moved `NoteExecutionHint` to `miden-standards` ([#2378](https://github.com/0xMiden/miden-base/pull/2378)). - Added `miden::protocol::auth` module with public auth event constants ([#2377](https://github.com/0xMiden/miden-base/pull/2377)). diff --git a/crates/miden-protocol/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs index 394f723c57..07ab32eb16 100644 --- a/crates/miden-protocol/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -438,9 +438,15 @@ mod tests { use super::*; use crate::Word; + use crate::account::delta::AccountUpdateDetails; use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; use crate::asset::FungibleAsset; - use crate::transaction::ProvenTransactionBuilder; + use crate::transaction::{ + InputNoteCommitment, + ProvenOutputNote, + ProvenTransaction, + TxAccountUpdate, + }; #[test] fn proposed_batch_serialization() -> anyhow::Result<()> { @@ -481,18 +487,25 @@ mod tests { let expiration_block_num = reference_block_header.block_num() + 1; let proof = ExecutionProof::new_dummy(); - let tx = ProvenTransactionBuilder::new( + let account_update = TxAccountUpdate::new( account_id, initial_account_commitment, final_account_commitment, account_delta_commitment, + AccountUpdateDetails::Private, + ) + .context("failed to build account update")?; + + let tx = ProvenTransaction::new( + account_update, + Vec::::new(), + Vec::::new(), block_num, block_ref, FungibleAsset::mock(100).unwrap_fungible(), expiration_block_num, proof, ) - .build() .context("failed to build proven transaction")?; let batch = ProposedBatch::new( diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index c35d948c56..04da8757f0 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -29,12 +29,7 @@ pub use outputs::{ TransactionOutputs, }; pub use partial_blockchain::PartialBlockchain; -pub use proven_tx::{ - InputNoteCommitment, - ProvenTransaction, - ProvenTransactionBuilder, - TxAccountUpdate, -}; +pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate}; pub use transaction_id::TransactionId; pub use tx_args::{TransactionArgs, TransactionScript}; pub use tx_header::TransactionHeader; diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index 4a9b53303b..bb434a0631 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -71,6 +71,70 @@ pub struct ProvenTransaction { } impl ProvenTransaction { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + + /// Creates a new [ProvenTransaction] from the specified components. + /// + /// # Errors + /// + /// Returns an error if: + /// - The total number of input notes is greater than + /// [`MAX_INPUT_NOTES_PER_TX`](crate::constants::MAX_INPUT_NOTES_PER_TX). + /// - The vector of input notes contains duplicates. + /// - The total number of output notes is greater than + /// [`MAX_OUTPUT_NOTES_PER_TX`](crate::constants::MAX_OUTPUT_NOTES_PER_TX). + /// - The vector of output notes contains duplicates. + /// - The transaction is empty, which is the case if the account state is unchanged or the + /// number of input notes is zero. + /// - The commitment computed on the actual account delta contained in [`TxAccountUpdate`] does + /// not match its declared account delta commitment. + pub fn new( + account_update: TxAccountUpdate, + input_notes: impl IntoIterator>, + output_notes: impl IntoIterator>, + ref_block_num: BlockNumber, + ref_block_commitment: Word, + fee: FungibleAsset, + expiration_block_num: BlockNumber, + proof: ExecutionProof, + ) -> Result { + let input_notes: Vec = + input_notes.into_iter().map(Into::into).collect(); + let output_notes: Vec = + output_notes.into_iter().map(Into::into).collect(); + + let input_notes = + InputNotes::new(input_notes).map_err(ProvenTransactionError::InputNotesError)?; + let output_notes = ProvenOutputNotes::new(output_notes) + .map_err(ProvenTransactionError::OutputNotesError)?; + + let id = TransactionId::new( + account_update.initial_state_commitment(), + account_update.final_state_commitment(), + input_notes.commitment(), + output_notes.commitment(), + fee, + ); + + let proven_transaction = Self { + id, + account_update, + input_notes, + output_notes, + ref_block_num, + ref_block_commitment, + fee, + expiration_block_num, + proof, + }; + + proven_transaction.validate() + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + /// Returns unique identifier of this transaction. pub fn id(&self) -> TransactionId { self.id @@ -239,172 +303,6 @@ impl Deserializable for ProvenTransaction { } } -// PROVEN TRANSACTION BUILDER -// ================================================================================================ - -/// Builder for a proven transaction. -#[derive(Clone, Debug)] -pub struct ProvenTransactionBuilder { - /// ID of the account that the transaction was executed against. - account_id: AccountId, - - /// The commitment of the account before the transaction was executed. - initial_account_commitment: Word, - - /// The commitment of the account after the transaction was executed. - final_account_commitment: Word, - - /// The commitment of the account delta produced by the transaction. - account_delta_commitment: Word, - - /// State changes to the account due to the transaction. - account_update_details: AccountUpdateDetails, - - /// List of [InputNoteCommitment]s of all consumed notes by the transaction. - input_notes: Vec, - - /// List of [`ProvenOutputNote`]s of all notes created by the transaction. - output_notes: Vec, - - /// [`BlockNumber`] of the transaction's reference block. - ref_block_num: BlockNumber, - - /// Block digest of the transaction's reference block. - ref_block_commitment: Word, - - /// The fee of the transaction. - fee: FungibleAsset, - - /// The block number by which the transaction will expire, as defined by the executed scripts. - expiration_block_num: BlockNumber, - - /// A STARK proof that attests to the correct execution of the transaction. - proof: ExecutionProof, -} - -impl ProvenTransactionBuilder { - // CONSTRUCTOR - // -------------------------------------------------------------------------------------------- - - /// Returns a [ProvenTransactionBuilder] used to build a [ProvenTransaction]. - #[allow(clippy::too_many_arguments)] - pub fn new( - account_id: AccountId, - initial_account_commitment: Word, - final_account_commitment: Word, - account_delta_commitment: Word, - ref_block_num: BlockNumber, - ref_block_commitment: Word, - fee: FungibleAsset, - expiration_block_num: BlockNumber, - proof: ExecutionProof, - ) -> Self { - Self { - account_id, - initial_account_commitment, - final_account_commitment, - account_delta_commitment, - account_update_details: AccountUpdateDetails::Private, - input_notes: Vec::new(), - output_notes: Vec::new(), - ref_block_num, - ref_block_commitment, - fee, - expiration_block_num, - proof, - } - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Sets the account's update details. - pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self { - self.account_update_details = details; - self - } - - /// Add notes consumed by the transaction. - pub fn add_input_notes(mut self, notes: I) -> Self - where - I: IntoIterator, - T: Into, - { - self.input_notes.extend(notes.into_iter().map(|note| note.into())); - self - } - - /// Add notes produced by the transaction. - pub fn add_output_notes(mut self, notes: T) -> Self - where - T: IntoIterator, - { - self.output_notes.extend(notes); - self - } - - /// Builds the [`ProvenTransaction`]. - /// - /// # Errors - /// - /// Returns an error if: - /// - The total number of input notes is greater than - /// [`MAX_INPUT_NOTES_PER_TX`](crate::constants::MAX_INPUT_NOTES_PER_TX). - /// - The vector of input notes contains duplicates. - /// - The total number of output notes is greater than - /// [`MAX_OUTPUT_NOTES_PER_TX`](crate::constants::MAX_OUTPUT_NOTES_PER_TX). - /// - The vector of output notes contains duplicates. - /// - The transaction is empty, which is the case if the account state is unchanged or the - /// number of input notes is zero. - /// - The commitment computed on the actual account delta contained in [`TxAccountUpdate`] does - /// not match its declared account delta commitment. - /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`]. - /// - The transaction was executed against a _new_ account with public state and its account ID - /// does not match the ID in the account update. - /// - The transaction was executed against a _new_ account with public state and its commitment - /// does not match the final state commitment of the account update. - /// - The transaction creates a _new_ account with public state and the update is of type - /// [`AccountUpdateDetails::Delta`] but the account delta is not a full state delta. - /// - The transaction was executed against a private account and the account update is _not_ of - /// type [`AccountUpdateDetails::Private`]. - /// - The transaction was executed against an account with public state and the update is of - /// type [`AccountUpdateDetails::Private`]. - pub fn build(self) -> Result { - let input_notes = - InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?; - let output_notes = ProvenOutputNotes::new(self.output_notes) - .map_err(ProvenTransactionError::OutputNotesError)?; - let id = TransactionId::new( - self.initial_account_commitment, - self.final_account_commitment, - input_notes.commitment(), - output_notes.commitment(), - self.fee, - ); - let account_update = TxAccountUpdate::new( - self.account_id, - self.initial_account_commitment, - self.final_account_commitment, - self.account_delta_commitment, - self.account_update_details, - )?; - - let proven_transaction = ProvenTransaction { - id, - account_update, - input_notes, - output_notes, - ref_block_num: self.ref_block_num, - ref_block_commitment: self.ref_block_commitment, - fee: self.fee, - expiration_block_num: self.expiration_block_num, - proof: self.proof, - }; - - proven_transaction.validate() - } -} - // TRANSACTION ACCOUNT UPDATE // ================================================================================================ @@ -686,6 +584,7 @@ impl Deserializable for InputNoteCommitment { #[cfg(test)] mod tests { use alloc::collections::BTreeMap; + use alloc::vec::Vec; use anyhow::Context; use miden_crypto::rand::test_utils::rand_value; @@ -715,7 +614,7 @@ mod tests { }; use crate::testing::add_component::AddComponent; use crate::testing::noop_auth_component::NoopAuthComponent; - use crate::transaction::{ProvenTransactionBuilder, TxAccountUpdate}; + use crate::transaction::{InputNoteCommitment, ProvenOutputNote, TxAccountUpdate}; use crate::utils::serde::{Deserializable, Serializable}; use crate::{ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, LexicographicWord, ONE, Word}; @@ -817,18 +716,25 @@ mod tests { let expiration_block_num = BlockNumber::from(2); let proof = ExecutionProof::new_dummy(); - let tx = ProvenTransactionBuilder::new( + let account_update = TxAccountUpdate::new( account_id, initial_account_commitment, final_account_commitment, account_delta_commitment, + AccountUpdateDetails::Private, + ) + .context("failed to build account update")?; + + let tx = ProvenTransaction::new( + account_update, + Vec::::new(), + Vec::::new(), ref_block_num, ref_block_commitment, FungibleAsset::mock(42).unwrap_fungible(), expiration_block_num, proof, ) - .build() .context("failed to build proven transaction")?; let deserialized = ProvenTransaction::read_from_bytes(&tx.to_bytes()).unwrap(); diff --git a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs index da8bc3f9ea..951cd2f905 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs @@ -3,15 +3,17 @@ use alloc::vec::Vec; use anyhow::Context; use miden_protocol::Word; use miden_protocol::account::AccountId; +use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::asset::FungibleAsset; use miden_protocol::block::BlockNumber; use miden_protocol::crypto::merkle::SparseMerklePath; use miden_protocol::note::{Note, NoteInclusionProof, Nullifier}; use miden_protocol::transaction::{ InputNote, + InputNoteCommitment, ProvenOutputNote, ProvenTransaction, - ProvenTransactionBuilder, + TxAccountUpdate, }; use miden_protocol::vm::ExecutionProof; @@ -102,21 +104,36 @@ impl MockProvenTxBuilder { /// Builds the [`ProvenTransaction`] and returns potential errors. pub fn build(self) -> anyhow::Result { - ProvenTransactionBuilder::new( + let mut input_note_commitments: Vec = self + .input_notes + .unwrap_or_default() + .into_iter() + .map(InputNoteCommitment::from) + .collect(); + + // Add nullifiers as input note commitments + input_note_commitments + .extend(self.nullifiers.unwrap_or_default().into_iter().map(InputNoteCommitment::from)); + + let account_update = TxAccountUpdate::new( self.account_id, self.initial_account_commitment, self.final_account_commitment, Word::empty(), + AccountUpdateDetails::Private, + ) + .context("failed to build account update")?; + + ProvenTransaction::new( + account_update, + input_note_commitments, + self.output_notes.unwrap_or_default(), BlockNumber::from(0), self.ref_block_commitment.unwrap_or_default(), self.fee, self.expiration_block_num, ExecutionProof::new_dummy(), ) - .add_input_notes(self.input_notes.unwrap_or_default()) - .add_input_notes(self.nullifiers.unwrap_or_default()) - .add_output_notes(self.output_notes.unwrap_or_default()) - .build() .context("failed to build proven transaction") } } diff --git a/crates/miden-testing/src/kernel_tests/block/header_errors.rs b/crates/miden-testing/src/kernel_tests/block/header_errors.rs index 78cca44f3f..1f5340c4ab 100644 --- a/crates/miden-testing/src/kernel_tests/block/header_errors.rs +++ b/crates/miden-testing/src/kernel_tests/block/header_errors.rs @@ -17,7 +17,12 @@ use miden_protocol::batch::ProvenBatch; use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock}; use miden_protocol::errors::{AccountTreeError, NullifierTreeError, ProposedBlockError}; use miden_protocol::note::NoteType; -use miden_protocol::transaction::ProvenTransactionBuilder; +use miden_protocol::transaction::{ + InputNoteCommitment, + ProvenOutputNote, + ProvenTransaction, + TxAccountUpdate, +}; use miden_protocol::vm::ExecutionProof; use miden_standards::testing::account_component::{IncrNonceAuthComponent, MockAccountComponent}; use miden_standards::testing::mock_account::MockAccountExt; @@ -383,19 +388,25 @@ async fn block_building_fails_on_creating_account_with_duplicate_account_id_pref let [tx0, tx1] = [(id0, [0, 0, 0, 1u32]), (id1, [0, 0, 0, 2u32])].map(|(id, final_state_comm)| { - ProvenTransactionBuilder::new( + let account_update = TxAccountUpdate::new( id, Word::empty(), Word::from(final_state_comm), Word::empty(), + AccountUpdateDetails::Private, + ) + .context("failed to build account update") + .unwrap(); + ProvenTransaction::new( + account_update, + Vec::::new(), + Vec::::new(), genesis_block.block_num(), genesis_block.commitment(), FungibleAsset::mock(500).unwrap_fungible(), BlockNumber::from(u32::MAX), ExecutionProof::new_dummy(), ) - .account_update_details(AccountUpdateDetails::Private) - .build() .context("failed to build proven transaction") .unwrap() }); diff --git a/crates/miden-tx/src/prover/mod.rs b/crates/miden-tx/src/prover/mod.rs index 3ebd574b3b..3268aad1e1 100644 --- a/crates/miden-tx/src/prover/mod.rs +++ b/crates/miden-tx/src/prover/mod.rs @@ -9,10 +9,10 @@ use miden_protocol::transaction::{ InputNote, InputNotes, ProvenTransaction, - ProvenTransactionBuilder, TransactionInputs, TransactionKernel, TransactionOutputs, + TxAccountUpdate, }; pub use miden_prover::ProvingOptions; use miden_prover::{ExecutionProof, Word, prove}; @@ -66,36 +66,39 @@ impl LocalTransactionProver { // since it is the output of the transaction and so is needed for proof verification. let pre_fee_delta_commitment: Word = pre_fee_account_delta.to_commitment(); - let builder = ProvenTransactionBuilder::new( + // The full transaction delta is the pre fee delta with the fee asset removed. + let mut post_fee_account_delta = pre_fee_account_delta; + post_fee_account_delta + .vault_mut() + .remove_asset(Asset::from(tx_outputs.fee)) + .map_err(TransactionProverError::RemoveFeeAssetFromDelta)?; + + let account_update_details = if account.has_public_state() { + AccountUpdateDetails::Delta(post_fee_account_delta) + } else { + AccountUpdateDetails::Private + }; + + let account_update = TxAccountUpdate::new( account.id(), account.initial_commitment(), tx_outputs.account.to_commitment(), pre_fee_delta_commitment, + account_update_details, + ) + .map_err(TransactionProverError::ProvenTransactionBuildFailed)?; + + ProvenTransaction::new( + account_update, + input_notes.iter(), + output_notes, ref_block_num, ref_block_commitment, tx_outputs.fee, tx_outputs.expiration_block_num, proof, ) - .add_input_notes(input_notes) - .add_output_notes(output_notes); - - // The full transaction delta is the pre fee delta with the fee asset removed. - let mut post_fee_account_delta = pre_fee_account_delta; - post_fee_account_delta - .vault_mut() - .remove_asset(Asset::from(tx_outputs.fee)) - .map_err(TransactionProverError::RemoveFeeAssetFromDelta)?; - - let builder = match account.has_public_state() { - true => { - let account_update_details = AccountUpdateDetails::Delta(post_fee_account_delta); - builder.account_update_details(account_update_details) - }, - false => builder, - }; - - builder.build().map_err(TransactionProverError::ProvenTransactionBuildFailed) + .map_err(TransactionProverError::ProvenTransactionBuildFailed) } pub async fn prove( From 0adc24b85499d9607f5da16204e412c8d7f2f090 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 09:13:24 -0300 Subject: [PATCH 26/56] feat: implement `Ownable2Step` (#2292) * feat: implement two-step ownership management for account components - Add `ownable2step.masm` to provide two-step ownership functionality, requiring explicit acceptance of ownership transfers. - Create Rust module for `Ownable2Step` struct to manage ownership state and storage layout. - Introduce error handling for ownership operations in `standards.rs`. - Develop comprehensive tests for ownership transfer, acceptance, and renouncement scenarios in `ownable2step.rs`. * feat: add Ownable2Step account component for two-step ownership transfer * fix: correct ownership word layout and update transfer ownership logic in ownable2step * Refactor ownership management to use Ownable2Step pattern - Updated the access control mechanism to implement a two-step ownership transfer pattern in the ownable2step module. - Modified the storage layout to accommodate the new ownership structure, reversing the order of owner and nominated owner fields. - Refactored the network fungible faucet to utilize the new Ownable2Step for ownership management. - Adjusted related tests to validate the new ownership transfer logic and ensure correct behavior when transferring and accepting ownership. - Updated error handling to reflect changes in ownership management, including new error messages for ownership transfer scenarios. * refactor: update ownership word layout and adjust related logic in Ownable2Step * refactor: rename owner and nominated_owner to get_owner and get_nominated_owner for consistency * refactor: streamline ownership management in tests by utilizing Ownable2Step according to philip * fix: `make format` --------- Co-authored-by: Bobbin Threadbare <43513081+bobbinth@users.noreply.github.com> Co-authored-by: Philipp Gackstatter --- CHANGELOG.md | 1 + .../faucets/network_fungible_faucet.masm | 3 + .../asm/standards/access/ownable2step.masm | 369 +++++++++++++ .../standards/faucets/network_fungible.masm | 47 +- .../miden-standards/src/account/access/mod.rs | 3 + .../src/account/access/ownable2step.rs | 150 ++++++ .../src/account/faucets/mod.rs | 4 + .../src/account/faucets/network_fungible.rs | 88 ++-- crates/miden-standards/src/account/mod.rs | 1 + crates/miden-testing/tests/scripts/faucet.rs | 124 ++--- crates/miden-testing/tests/scripts/mod.rs | 1 + .../tests/scripts/ownable2step.rs | 491 ++++++++++++++++++ 12 files changed, 1142 insertions(+), 140 deletions(-) create mode 100644 crates/miden-standards/asm/standards/access/ownable2step.masm create mode 100644 crates/miden-standards/src/account/access/mod.rs create mode 100644 crates/miden-standards/src/account/access/ownable2step.rs create mode 100644 crates/miden-testing/tests/scripts/ownable2step.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e14e8831..d66ddd5552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Resolve standard note scripts directly in `TransactionExecutorHost` instead of querying the data store ([#2417](https://github.com/0xMiden/miden-base/pull/2417)). - Added `DEFAULT_TAG` constant to `miden::standards::note_tag` MASM module ([#2482](https://github.com/0xMiden/miden-base/pull/2482)). - Added `NoteExecutionHint` variant constants (`NONE`, `ALWAYS`, `AFTER_BLOCK`, `ON_BLOCK_SLOT`) to `miden::standards::note::execution_hint` MASM module ([#2493](https://github.com/0xMiden/miden-base/pull/2493)). +- Added `Ownable2Step` account component with two-step ownership transfer (`transfer_ownership`, `accept_ownership`, `renounce_ownership`) and `owner`, `nominated_owner` procedures ([#2292](https://github.com/0xMiden/miden-base/pull/2292)). - Added PSM authentication procedures and integrated them into `AuthMultisig` ([#2527](https://github.com/0xMiden/protocol/pull/2527)). - Added `CodeBuilder::with_warnings_as_errors()` to promote assembler warning diagnostics to errors ([#2558](https://github.com/0xMiden/protocol/pull/2558)). - Added `MockChain::add_pending_batch()` to allow submitting user batches directly ([#2565](https://github.com/0xMiden/protocol/pull/2565)). diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index 1743a47d5a..e6effc5ef5 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -4,7 +4,10 @@ pub use ::miden::standards::faucets::network_fungible::distribute pub use ::miden::standards::faucets::network_fungible::burn +pub use ::miden::standards::faucets::network_fungible::get_owner +pub use ::miden::standards::faucets::network_fungible::get_nominated_owner pub use ::miden::standards::faucets::network_fungible::transfer_ownership +pub use ::miden::standards::faucets::network_fungible::accept_ownership pub use ::miden::standards::faucets::network_fungible::renounce_ownership # Metadata — all from metadata::fungible diff --git a/crates/miden-standards/asm/standards/access/ownable2step.masm b/crates/miden-standards/asm/standards/access/ownable2step.masm new file mode 100644 index 0000000000..d4b7bcffbd --- /dev/null +++ b/crates/miden-standards/asm/standards/access/ownable2step.masm @@ -0,0 +1,369 @@ +# miden::standards::access::ownable2step +# +# Provides two-step ownership management functionality for account components. +# This module can be imported and used by any component that needs owner controls. +# +# Unlike a single-step ownership transfer, this module requires the new owner to explicitly +# accept the transfer before it takes effect. This prevents accidental transfers to incorrect +# addresses, which would permanently lock the component. +# +# The transfer flow is: +# 1. The current owner calls `transfer_ownership` to nominate a new owner. +# 2. The nominated account calls `accept_ownership` to complete the transfer. +# 3. Optionally, the current owner can call `transfer_ownership` with their own address +# to cancel the nominated transfer. +# +# Storage layout (single slot): +# Word: [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] +# word[0] word[1] word[2] word[3] + +use miden::protocol::active_account +use miden::protocol::account_id +use miden::protocol::active_note +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================ + +# Ownership config value representing renounced ownership (all zeros). +const RENOUNCED_OWNERSHIP_CONFIG = [0, 0, 0, 0] + +# The slot in this component's storage layout where the owner configuration is stored. +# Contains both the current owner and the nominated owner in a single word. +const OWNER_CONFIG_SLOT = word("miden::standards::access::ownable2step::owner_config") + +# ERRORS +# ================================================================================================ + +const ERR_SENDER_NOT_OWNER = "note sender is not the owner" +const ERR_SENDER_NOT_NOMINATED_OWNER = "note sender is not the nominated owner" +const ERR_NO_NOMINATED_OWNER = "no nominated ownership transfer exists" + +# LOCAL MEMORY ADDRESSES +# ================================================================================================ + +# transfer_ownership locals +const NEW_OWNER_SUFFIX_LOC = 0 +const NEW_OWNER_PREFIX_LOC = 1 +const OWNER_SUFFIX_LOC = 2 +const OWNER_PREFIX_LOC = 3 + +# INTERNAL PROCEDURES +# ================================================================================================ + +#! Returns the full ownership word from storage. +#! +#! Inputs: [] +#! Outputs: [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] +#! +#! Where: +#! - owner_{suffix, prefix} are the suffix and prefix felts of the current owner account ID. +#! - nominated_owner_{suffix, prefix} are the suffix and prefix felts of the nominated +#! owner account ID. +proc load_ownership_info + push.OWNER_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] +end + +#! Writes the ownership word to storage and drops the old value. +#! +#! Inputs: [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] +#! Outputs: [] +proc save_ownership_info + push.OWNER_CONFIG_SLOT[0..2] + # => [slot_suffix, slot_prefix, owner_suffix, owner_prefix, + # nominated_owner_suffix, nominated_owner_prefix] + + exec.native_account::set_item + # => [OLD_OWNERSHIP_WORD] + + dropw + # => [] +end + +#! Returns the owner account ID from storage. +#! +#! Inputs: [] +#! Outputs: [owner_suffix, owner_prefix] +#! +#! Where: +#! - owner_{suffix, prefix} are the suffix and prefix felts of the owner account ID. +proc get_owner_internal + exec.load_ownership_info + # => [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] + + movup.2 drop + # => [owner_suffix, owner_prefix, nominated_owner_prefix] + + movup.2 drop + # => [owner_suffix, owner_prefix] +end + +#! Returns the nominated owner account ID from storage. +#! +#! Inputs: [] +#! Outputs: [nominated_owner_suffix, nominated_owner_prefix] +#! +#! Where: +#! - nominated_owner_{suffix, prefix} are the suffix and prefix felts of the nominated +#! owner account ID. +proc get_nominated_owner_internal + exec.load_ownership_info + # => [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] + + drop drop + # => [nominated_owner_suffix, nominated_owner_prefix] +end + +#! Checks if the given account ID is the owner of this component. +#! +#! Inputs: [account_id_suffix, account_id_prefix] +#! Outputs: [is_owner] +#! +#! Where: +#! - is_owner is 1 if the account is the owner, 0 otherwise. +proc is_owner_internal + exec.get_owner_internal + # => [owner_suffix, owner_prefix, account_id_suffix, account_id_prefix] + + exec.account_id::is_equal + # => [is_owner] +end + +#! Checks if the given account ID is the nominated owner of this component. +#! +#! Inputs: [account_id_suffix, account_id_prefix] +#! Outputs: [is_nominated_owner] +#! +#! Where: +#! - account_id_{suffix, prefix} are the suffix and prefix felts of the account ID to check. +#! - is_nominated_owner is 1 if the account is the nominated owner, 0 otherwise. +proc is_nominated_owner_internal + exec.get_nominated_owner_internal + # => [nominated_owner_suffix, nominated_owner_prefix, account_id_suffix, account_id_prefix] + + exec.account_id::is_equal + # => [is_nominated_owner] +end + +#! Checks if the note sender is the owner and panics if not. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: exec +proc assert_sender_is_owner_internal + exec.active_note::get_sender + # => [sender_suffix, sender_prefix] + + exec.is_owner_internal + # => [is_owner] + + assert.err=ERR_SENDER_NOT_OWNER + # => [] +end + +# PUBLIC INTERFACE +# ================================================================================================ + +#! Checks if the note sender is the owner and panics if not. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: call +pub proc assert_sender_is_owner + exec.assert_sender_is_owner_internal + # => [pad(16)] +end + +#! Returns the owner account ID. +#! +#! Inputs: [pad(16)] +#! Outputs: [owner_suffix, owner_prefix, pad(14)] +#! +#! Where: +#! - owner_{suffix, prefix} are the suffix and prefix felts of the owner account ID. +#! +#! Invocation: call +pub proc get_owner + exec.get_owner_internal + # => [owner_suffix, owner_prefix, pad(16)] + + movup.2 drop movup.2 drop + # => [owner_suffix, owner_prefix, pad(14)] +end + +#! Returns the nominated owner account ID. +#! +#! Inputs: [pad(16)] +#! Outputs: [nominated_owner_suffix, nominated_owner_prefix, pad(14)] +#! +#! Where: +#! - nominated_owner_{suffix, prefix} are the suffix and prefix felts of the nominated +#! owner account ID. Both are zero if no nominated transfer exists. +#! +#! Invocation: call +pub proc get_nominated_owner + exec.get_nominated_owner_internal + # => [nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + movup.2 drop movup.2 drop + # => [nominated_owner_suffix, nominated_owner_prefix, pad(14)] +end + +#! Initiates a two-step ownership transfer by setting the nominated owner. +#! +#! The current owner remains in control until the nominated owner calls `accept_ownership`. +#! Can only be called by the current owner. +#! +#! If the new owner is the current owner, any nominated transfer is cancelled and the +#! nominated owner field is cleared. +#! +#! Inputs: [new_owner_suffix, new_owner_prefix, pad(14)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Locals: +#! 0: new_owner_suffix +#! 1: new_owner_prefix +#! 2: owner_suffix +#! 3: owner_prefix +#! +#! Invocation: call +@locals(4) +pub proc transfer_ownership + exec.assert_sender_is_owner_internal + # => [new_owner_suffix, new_owner_prefix, pad(14)] + + dup.1 dup.1 exec.account_id::validate + # => [new_owner_suffix, new_owner_prefix, pad(14)] + + loc_store.NEW_OWNER_SUFFIX_LOC + # => [new_owner_prefix, pad(14)] + + loc_store.NEW_OWNER_PREFIX_LOC + # => [pad(14)] + + exec.get_owner_internal + # => [owner_suffix, owner_prefix, pad(14)] + + loc_store.OWNER_SUFFIX_LOC + # => [owner_prefix, pad(13)] + + loc_store.OWNER_PREFIX_LOC + # => [pad(12)] + + # Check if new_owner == owner (cancel case). + loc_load.NEW_OWNER_PREFIX_LOC loc_load.NEW_OWNER_SUFFIX_LOC + # => [new_owner_suffix, new_owner_prefix, pad(12)] + + loc_load.OWNER_PREFIX_LOC loc_load.OWNER_SUFFIX_LOC + # => [owner_suffix, owner_prefix, new_owner_suffix, new_owner_prefix, pad(12)] + + exec.account_id::is_equal + # => [is_self_transfer, pad(12)] + + if.true + # Cancel ownership transfer and clear nominated owner. + # Stack for save: [owner_suffix, owner_prefix, nominated_suffix=0, nominated_prefix=0] + loc_load.OWNER_PREFIX_LOC loc_load.OWNER_SUFFIX_LOC + # => [owner_suffix, owner_prefix, pad(12)] + + push.0.0 movup.3 movup.3 + # => [owner_suffix, owner_prefix, 0, 0, pad(12)] + else + # Transfer ownership by setting nominated = new_owner. + # Stack for save: [owner_suffix, owner_prefix, new_owner_suffix, new_owner_prefix] + loc_load.NEW_OWNER_PREFIX_LOC loc_load.NEW_OWNER_SUFFIX_LOC + # => [new_owner_suffix, new_owner_prefix, pad(12)] + + loc_load.OWNER_PREFIX_LOC loc_load.OWNER_SUFFIX_LOC + # => [owner_suffix, owner_prefix, new_owner_suffix, new_owner_prefix, pad(12)] + end + + exec.save_ownership_info + # => [pad(12)] +end + +#! Accepts a nominated ownership transfer. The nominated owner becomes the new owner +#! and the nominated owner field is cleared. +#! +#! Can only be called by the nominated owner. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - there is no nominated ownership transfer (nominated owner is zero). +#! - the note sender is not the nominated owner. +#! +#! Invocation: call +pub proc accept_ownership + exec.get_nominated_owner_internal + # => [nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + # Check that a nominated transfer exists (nominated owner is not zero). + dup.1 eq.0 dup.1 eq.0 and + # => [is_zero, nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + assertz.err=ERR_NO_NOMINATED_OWNER + # => [nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + exec.active_note::get_sender + # => [sender_suffix, sender_prefix, nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + dup.3 dup.3 + exec.account_id::is_equal + # => [is_sender_nominated_owner, nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + assert.err=ERR_SENDER_NOT_NOMINATED_OWNER + # => [nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + # Build new ownership word: nominated becomes owner, clear nominated. + # Stack for save: [owner_suffix, owner_prefix, nominated_suffix=0, nominated_prefix=0] + push.0.0 + # => [0, 0, nominated_owner_suffix, nominated_owner_prefix, pad(16)] + + # Reorder: move nominated (now new owner) to owner position + movup.3 movup.3 + # => [nominated_owner_suffix, nominated_owner_prefix, 0, 0, pad(16)] + + exec.save_ownership_info + # => [pad(16)] +end + +#! Renounces ownership, leaving the component without an owner. +#! +#! Can only be called by the current owner. Clears both the owner and any nominated owner. +#! +#! Important Note! +#! This feature allows the owner to relinquish administrative privileges, a common pattern +#! after an initial stage with centralized administration is over. Once ownership is renounced, +#! the component becomes permanently ownerless and cannot be managed by any account. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the owner. +#! +#! Invocation: call +pub proc renounce_ownership + exec.assert_sender_is_owner_internal + # => [pad(16)] + + push.RENOUNCED_OWNERSHIP_CONFIG + # => [0, 0, 0, 0, pad(16)] + + exec.save_ownership_info + # => [pad(16)] +end diff --git a/crates/miden-standards/asm/standards/faucets/network_fungible.masm b/crates/miden-standards/asm/standards/faucets/network_fungible.masm index ed37c3ce3d..74aed661e2 100644 --- a/crates/miden-standards/asm/standards/faucets/network_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/network_fungible.masm @@ -1,48 +1,17 @@ use miden::standards::faucets -use miden::standards::access::ownable +use miden::standards::access::ownable2step # PUBLIC INTERFACE # ================================================================================================ -# OWNER MANAGEMENT +# OWNER MANAGEMENT (re-exported from ownable2step) # ------------------------------------------------------------------------------------------------ -#! Returns the owner AccountId. -#! -#! Inputs: [] -#! Outputs: [owner_suffix, owner_prefix, pad(14)] -#! -#! Invocation: call -pub use ownable::get_owner - -#! Transfers ownership to a new account. -#! -#! Can only be called by the current owner. -#! -#! Inputs: [new_owner_suffix, new_owner_prefix, pad(14)] -#! Outputs: [pad(16)] -#! -#! Where: -#! - new_owner_{suffix, prefix} are the suffix and prefix felts of the new owner AccountId. -#! -#! Panics if: -#! - the note sender is not the owner. -#! -#! Invocation: call -pub use ownable::transfer_ownership - -#! Renounces ownership, leaving the component without an owner. -#! -#! Can only be called by the current owner. -#! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] -#! -#! Panics if: -#! - the note sender is not the owner. -#! -#! Invocation: call -pub use ownable::renounce_ownership +pub use ownable2step::get_owner +pub use ownable2step::get_nominated_owner +pub use ownable2step::transfer_ownership +pub use ownable2step::accept_ownership +pub use ownable2step::renounce_ownership # ASSET DISTRIBUTION # ------------------------------------------------------------------------------------------------ @@ -68,7 +37,7 @@ pub use ownable::renounce_ownership #! #! Invocation: call pub proc distribute - exec.ownable::verify_owner + exec.ownable2step::assert_sender_is_owner # => [amount, tag, aux, note_type, execution_hint, RECIPIENT, pad(7)] exec.faucets::distribute diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs new file mode 100644 index 0000000000..6e14ad58bf --- /dev/null +++ b/crates/miden-standards/src/account/access/mod.rs @@ -0,0 +1,3 @@ +pub mod ownable2step; + +pub use ownable2step::{Ownable2Step, Ownable2StepError}; diff --git a/crates/miden-standards/src/account/access/ownable2step.rs b/crates/miden-standards/src/account/access/ownable2step.rs new file mode 100644 index 0000000000..ec3668a7a4 --- /dev/null +++ b/crates/miden-standards/src/account/access/ownable2step.rs @@ -0,0 +1,150 @@ +use miden_protocol::account::component::{FeltSchema, StorageSlotSchema}; +use miden_protocol::account::{AccountId, AccountStorage, StorageSlot, StorageSlotName}; +use miden_protocol::errors::AccountIdError; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; + +static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::ownable2step::owner_config") + .expect("storage slot name should be valid") +}); + +/// Two-step ownership management for account components. +/// +/// This struct holds the current owner and any nominated (pending) owner. A nominated owner +/// must explicitly accept the transfer before it takes effect, preventing accidental transfers +/// to incorrect addresses. +/// +/// ## Storage Layout +/// +/// The ownership data is stored in a single word: +/// +/// ```text +/// Word: [owner_suffix, owner_prefix, nominated_owner_suffix, nominated_owner_prefix] +/// word[0] word[1] word[2] word[3] +/// ``` +pub struct Ownable2Step { + /// The current owner of the component. `None` when ownership has been renounced. + owner: Option, + nominated_owner: Option, +} + +impl Ownable2Step { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`Ownable2Step`] with the given owner and no nominated owner. + pub fn new(owner: AccountId) -> Self { + Self { + owner: Some(owner), + nominated_owner: None, + } + } + + /// Reads ownership data from account storage, validating any non-zero account IDs. + /// + /// Returns an error if either owner or nominated owner contains an invalid (but non-zero) + /// account ID. + pub fn try_from_storage(storage: &AccountStorage) -> Result { + let word: Word = storage + .get_item(Self::slot_name()) + .map_err(Ownable2StepError::StorageLookupFailed)?; + + Self::try_from_word(word) + } + + /// Reconstructs an [`Ownable2Step`] from a raw storage word. + /// + /// Format: `[owner_suffix, owner_prefix, nominated_suffix, nominated_prefix]` + pub fn try_from_word(word: Word) -> Result { + let owner = account_id_from_felt_pair(word[0], word[1]) + .map_err(Ownable2StepError::InvalidOwnerId)?; + + let nominated_owner = account_id_from_felt_pair(word[2], word[3]) + .map_err(Ownable2StepError::InvalidNominatedOwnerId)?; + + Ok(Self { owner, nominated_owner }) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where ownership data is stored. + pub fn slot_name() -> &'static StorageSlotName { + &OWNER_CONFIG_SLOT_NAME + } + + /// Returns the storage slot schema for the ownership configuration slot. + pub fn slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::slot_name().clone(), + StorageSlotSchema::value( + "Ownership data (owner and nominated owner)", + [ + FeltSchema::felt("owner_suffix"), + FeltSchema::felt("owner_prefix"), + FeltSchema::felt("nominated_suffix"), + FeltSchema::felt("nominated_prefix"), + ], + ), + ) + } + + /// Returns the current owner, or `None` if ownership has been renounced. + pub fn owner(&self) -> Option { + self.owner + } + + /// Returns the nominated owner, or `None` if no transfer is in progress. + pub fn nominated_owner(&self) -> Option { + self.nominated_owner + } + + /// Converts this ownership data into a [`StorageSlot`]. + pub fn to_storage_slot(&self) -> StorageSlot { + StorageSlot::with_value(Self::slot_name().clone(), self.to_word()) + } + + /// Converts this ownership data into a raw [`Word`]. + pub fn to_word(&self) -> Word { + let (owner_suffix, owner_prefix) = match self.owner { + Some(id) => (id.suffix(), id.prefix().as_felt()), + None => (Felt::ZERO, Felt::ZERO), + }; + let (nominated_suffix, nominated_prefix) = match self.nominated_owner { + Some(id) => (id.suffix(), id.prefix().as_felt()), + None => (Felt::ZERO, Felt::ZERO), + }; + [owner_suffix, owner_prefix, nominated_suffix, nominated_prefix].into() + } +} + +// OWNABLE2STEP ERROR +// ================================================================================================ + +/// Errors that can occur when reading [`Ownable2Step`] data from storage. +#[derive(Debug, thiserror::Error)] +pub enum Ownable2StepError { + #[error("failed to read ownership slot from storage")] + StorageLookupFailed(#[source] miden_protocol::errors::AccountError), + #[error("invalid owner account ID in storage")] + InvalidOwnerId(#[source] AccountIdError), + #[error("invalid nominated owner account ID in storage")] + InvalidNominatedOwnerId(#[source] AccountIdError), +} + +// HELPERS +// ================================================================================================ + +/// Constructs an `Option` from a suffix/prefix felt pair. +/// Returns `Ok(None)` when both felts are zero (renounced / no nomination). +fn account_id_from_felt_pair( + suffix: Felt, + prefix: Felt, +) -> Result, AccountIdError> { + if suffix == Felt::ZERO && prefix == Felt::ZERO { + Ok(None) + } else { + AccountId::try_from_elements(suffix, prefix).map(Some) + } +} diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 65c902ea88..9ac35b9dba 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -4,6 +4,8 @@ use miden_protocol::account::StorageSlotName; use miden_protocol::errors::{AccountError, TokenSymbolError}; use thiserror::Error; +use crate::account::access::Ownable2StepError; + mod basic_fungible; mod network_fungible; mod token_metadata; @@ -50,4 +52,6 @@ pub enum FungibleFaucetError { AccountError(#[source] AccountError), #[error("account is not a fungible faucet account")] NotAFungibleFaucetAccount, + #[error("failed to read ownership data from storage")] + OwnershipError(#[source] Ownable2StepError), } diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 880b97a25d..af5347ed46 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -13,12 +13,12 @@ use miden_protocol::account::{ AccountStorage, AccountStorageMode, AccountType, - StorageSlot, StorageSlotName, }; use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; +<<<<<<< HEAD use super::{ Description, ExternalLink, @@ -27,14 +27,23 @@ use super::{ LogoURI, TokenName, }; +======= +use super::{FungibleFaucetError, TokenMetadata}; +use crate::account::access::Ownable2Step; +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::procedure_digest; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; +<<<<<<< HEAD use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::account::metadata::TokenMetadata as TokenMetadataInfo; use crate::procedure_digest; +======= +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ @@ -66,16 +75,24 @@ procedure_digest!( /// authentication while `burn` does not require authentication and can be called by anyone. /// Thus, this component must be combined with a component providing authentication. /// +/// Ownership is managed via a two-step transfer pattern ([`Ownable2Step`]). The current owner +/// must first nominate a new owner, who then accepts the transfer. +/// /// ## Storage Layout /// /// - [`Self::metadata_slot`]: Fungible faucet metadata. -/// - [`Self::owner_config_slot`]: The owner account of this network faucet. +/// - [`Ownable2Step::slot_name`]: The owner and nominated owner of this network faucet. /// /// [builder]: crate::code_builder::CodeBuilder pub struct NetworkFungibleFaucet { +<<<<<<< HEAD metadata: FungibleTokenMetadata, owner_account_id: AccountId, info: Option, +======= + metadata: TokenMetadata, + ownership: Ownable2Step, +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } impl NetworkFungibleFaucet { @@ -114,6 +131,7 @@ impl NetworkFungibleFaucet { logo_uri: Option, external_link: Option, ) -> Result { +<<<<<<< HEAD let metadata = FungibleTokenMetadata::new( symbol, decimals, @@ -124,12 +142,18 @@ impl NetworkFungibleFaucet { external_link, )?; Ok(Self { metadata, owner_account_id, info: None }) +======= + let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; + let ownership = Ownable2Step::new(owner_account_id); + Ok(Self { metadata, ownership }) +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } /// Creates a new [`NetworkFungibleFaucet`] component from the given [`FungibleTokenMetadata`]. /// /// This is a convenience constructor that allows creating a faucet from pre-validated /// metadata. +<<<<<<< HEAD pub fn from_metadata(metadata: FungibleTokenMetadata, owner_account_id: AccountId) -> Self { Self { metadata, owner_account_id, info: None } } @@ -139,6 +163,11 @@ impl NetworkFungibleFaucet { pub fn with_info(mut self, info: TokenMetadataInfo) -> Self { self.info = Some(info); self +======= + pub fn from_metadata(metadata: TokenMetadata, owner_account_id: AccountId) -> Self { + let ownership = Ownable2Step::new(owner_account_id); + Self { metadata, ownership } +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account @@ -169,14 +198,11 @@ impl NetworkFungibleFaucet { // Read token metadata from storage let metadata = FungibleTokenMetadata::try_from(storage)?; - // obtain owner account ID from the next storage slot - let owner_account_id_word: Word = storage - .get_item(NetworkFungibleFaucet::owner_config_slot()) - .map_err(|err| FungibleFaucetError::StorageLookupFailed { - slot_name: NetworkFungibleFaucet::owner_config_slot().clone(), - source: err, - })?; + // Read ownership data from storage + let ownership = + Ownable2Step::try_from_storage(storage).map_err(FungibleFaucetError::OwnershipError)?; +<<<<<<< HEAD // Convert Word back to AccountId // Storage format: [0, 0, suffix, prefix] let prefix = owner_account_id_word[3]; @@ -184,6 +210,9 @@ impl NetworkFungibleFaucet { let owner_account_id = AccountId::new_unchecked([prefix, suffix]); Ok(Self { metadata, owner_account_id, info: None }) +======= + Ok(Self { metadata, ownership }) +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } // PUBLIC ACCESSORS @@ -195,12 +224,15 @@ impl NetworkFungibleFaucet { FungibleTokenMetadata::metadata_slot() } +<<<<<<< HEAD /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s owner configuration is /// stored (slot 1). pub fn owner_config_slot() -> &'static StorageSlotName { crate::account::metadata::owner_config_slot() } +======= +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) /// Returns the storage slot schema for the metadata slot. pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); @@ -218,22 +250,6 @@ impl NetworkFungibleFaucet { ) } - /// Returns the storage slot schema for the owner configuration slot. - pub fn owner_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - ( - Self::owner_config_slot().clone(), - StorageSlotSchema::value( - "Owner account configuration", - [ - FeltSchema::new_void(), - FeltSchema::new_void(), - FeltSchema::felt("owner_suffix"), - FeltSchema::felt("owner_prefix"), - ], - ), - ) - } - /// Returns the token metadata. pub fn metadata(&self) -> &FungibleTokenMetadata { &self.metadata @@ -264,9 +280,19 @@ impl NetworkFungibleFaucet { self.metadata.token_supply() } - /// Returns the owner account ID of the faucet. - pub fn owner_account_id(&self) -> AccountId { - self.owner_account_id + /// Returns the owner account ID of the faucet, or `None` if ownership has been renounced. + pub fn owner_account_id(&self) -> Option { + self.ownership.owner() + } + + /// Returns the nominated owner account ID, or `None` if no transfer is in progress. + pub fn nominated_owner(&self) -> Option { + self.ownership.nominated_owner() + } + + /// Returns the ownership data of the faucet. + pub fn ownership(&self) -> &Ownable2Step { + &self.ownership } /// Returns the digest of the `distribute` account procedure. @@ -297,6 +323,7 @@ impl NetworkFungibleFaucet { impl From for AccountComponent { fn from(network_faucet: NetworkFungibleFaucet) -> Self { let metadata_slot = network_faucet.metadata.into(); +<<<<<<< HEAD let owner_account_id_word: Word = [ Felt::new(0), @@ -310,6 +337,9 @@ impl From for AccountComponent { NetworkFungibleFaucet::owner_config_slot().clone(), owner_account_id_word, ); +======= + let owner_slot = network_faucet.ownership.to_storage_slot(); +>>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) let mut slots = vec![metadata_slot, owner_slot]; if let Some(info) = &network_faucet.info { @@ -318,7 +348,7 @@ impl From for AccountComponent { let storage_schema = StorageSchema::new([ NetworkFungibleFaucet::metadata_slot_schema(), - NetworkFungibleFaucet::owner_config_slot_schema(), + Ownable2Step::slot_schema(), ]) .expect("storage schema should be valid"); diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index 56e4dbe720..0c11b7f2c1 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -1,5 +1,6 @@ use super::auth_method::AuthMethod; +pub mod access; pub mod auth; pub mod components; pub mod faucets; diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 4170bc0da1..7127d9bfe3 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -28,6 +28,7 @@ use miden_protocol::note::{ use miden_protocol::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER; use miden_protocol::transaction::{ExecutedTransaction, OutputNote}; use miden_protocol::{Felt, Word}; +use miden_standards::account::access::Ownable2Step; use miden_standards::account::faucets::{ BasicFungibleFaucet, FungibleTokenMetadata, @@ -568,15 +569,16 @@ async fn network_faucet_mint() -> anyhow::Result<()> { let actual_max_supply = FungibleTokenMetadata::try_from(faucet.storage())?.max_supply(); assert_eq!(actual_max_supply.as_canonical_u64(), max_supply); - // Check that the creator account ID is stored in slot 2 (second storage slot of the component) - // The owner_account_id is stored as Word [0, 0, suffix, prefix] - let stored_owner_id = - faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot()).unwrap(); - assert_eq!(stored_owner_id[3], faucet_owner_account_id.prefix().as_felt()); + // Check that the creator account ID is stored in the ownership slot. + // Word: [owner_suffix, owner_prefix, nominated_suffix, nominated_prefix] + let stored_owner_id = faucet.storage().get_item(Ownable2Step::slot_name()).unwrap(); assert_eq!( - stored_owner_id[2], + stored_owner_id[0], Felt::new(faucet_owner_account_id.suffix().as_canonical_u64()) ); + assert_eq!(stored_owner_id[1], faucet_owner_account_id.prefix().as_felt()); + assert_eq!(stored_owner_id[2], Felt::new(0)); // no nominated owner + assert_eq!(stored_owner_id[3], Felt::new(0)); // Check that the faucet's token supply has been correctly initialized. // The already issued amount should be 50. @@ -782,18 +784,20 @@ async fn test_network_faucet_owner_storage() -> anyhow::Result<()> { let _mock_chain = builder.build()?; // Verify owner is stored correctly - let stored_owner = faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot())?; + let stored_owner = faucet.storage().get_item(Ownable2Step::slot_name())?; - // Storage format: [0, 0, suffix, prefix] - assert_eq!(stored_owner[3], owner_account_id.prefix().as_felt()); - assert_eq!(stored_owner[2], Felt::new(owner_account_id.suffix().as_canonical_u64())); - assert_eq!(stored_owner[1], Felt::new(0)); - assert_eq!(stored_owner[0], Felt::new(0)); + // Word: [owner_suffix, owner_prefix, nominated_suffix, nominated_prefix] + assert_eq!(stored_owner[0], Felt::new(owner_account_id.suffix().as_canonical_u64())); + assert_eq!(stored_owner[1], owner_account_id.prefix().as_felt()); + assert_eq!(stored_owner[2], Felt::new(0)); // no nominated owner + assert_eq!(stored_owner[3], Felt::new(0)); Ok(()) } -/// Tests that transfer_ownership updates the owner correctly. +/// Tests that two-step transfer_ownership updates the owner correctly. +/// Step 1: Owner nominates a new owner via transfer_ownership. +/// Step 2: Nominated owner accepts via accept_ownership. #[tokio::test] async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -842,7 +846,7 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { &mut rng, )?; - // Action: Create transfer_ownership note script + // Step 1: Create transfer_ownership note script to nominate new owner let transfer_note_script_code = format!( r#" use miden::standards::faucets::network_fungible->network_faucet @@ -860,8 +864,6 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { ); let source_manager = Arc::new(DefaultSourceManager::default()); - let transfer_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(transfer_note_script_code.clone())?; // Create the transfer note and add it to the builder so it exists on-chain let mut rng = RpoRandomCoin::new([Felt::from(200u32); 4].into()); @@ -884,10 +886,9 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { let executed_transaction = tx_context.execute().await?; assert_eq!(executed_transaction.output_notes().num_notes(), 1); - // Action: Execute transfer_ownership via note script + // Execute transfer_ownership via note script (nominates new owner) let tx_context = mock_chain .build_tx_context(faucet.id(), &[transfer_note.id()], &[])? - .add_note_script(transfer_note_script.clone()) .with_source_manager(source_manager.clone()) .build()?; let executed_transaction = tx_context.execute().await?; @@ -896,48 +897,44 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { mock_chain.add_pending_executed_transaction(&executed_transaction)?; mock_chain.prove_next_block()?; - // Apply the delta to the faucet account to reflect the ownership change let mut updated_faucet = faucet.clone(); updated_faucet.apply_delta(executed_transaction.account_delta())?; - // Validation 1: Try to mint using the old owner - should fail - let mut rng = RpoRandomCoin::new([Felt::from(300u32); 4].into()); - let mint_note_old_owner = MintNote::create( - updated_faucet.id(), - initial_owner_account_id, - mint_inputs.clone(), - NoteAttachment::default(), - &mut rng, - )?; - - // Use the note as an unauthenticated note (full note object) - it will be created in this - // transaction - let tx_context = mock_chain - .build_tx_context(updated_faucet.id(), &[], &[mint_note_old_owner])? - .build()?; - let result = tx_context.execute().await; + // Step 2: Accept ownership as the nominated owner + let accept_note_script_code = r#" + use miden::standards::faucets::network_fungible->network_faucet - // The distribute function uses ERR_ONLY_OWNER, which is "note sender is not the owner" - let expected_error = ERR_SENDER_NOT_OWNER; - assert_transaction_executor_error!(result, expected_error); + begin + repeat.16 push.0 end + call.network_faucet::accept_ownership + dropw dropw dropw dropw + end + "#; - // Validation 2: Try to mint using the new owner - should succeed let mut rng = RpoRandomCoin::new([Felt::from(400u32); 4].into()); - let mint_note_new_owner = MintNote::create( - updated_faucet.id(), - new_owner_account_id, - mint_inputs, - NoteAttachment::default(), - &mut rng, - )?; + let accept_note = NoteBuilder::new(new_owner_account_id, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([55, 66, 77, 88u32])) + .code(accept_note_script_code) + .build()?; let tx_context = mock_chain - .build_tx_context(updated_faucet.id(), &[], &[mint_note_new_owner])? + .build_tx_context(updated_faucet.clone(), &[], slice::from_ref(&accept_note))? + .with_source_manager(source_manager.clone()) .build()?; let executed_transaction = tx_context.execute().await?; - // Verify that minting succeeded - assert_eq!(executed_transaction.output_notes().num_notes(), 1); + let mut final_faucet = updated_faucet.clone(); + final_faucet.apply_delta(executed_transaction.account_delta())?; + + // Verify that owner changed to new_owner and nominated was cleared + // Word: [owner_suffix, owner_prefix, nominated_suffix, nominated_prefix] + let stored_owner = final_faucet.storage().get_item(Ownable2Step::slot_name())?; + assert_eq!(stored_owner[0], Felt::new(new_owner_account_id.suffix().as_canonical_u64())); + assert_eq!(stored_owner[1], new_owner_account_id.prefix().as_felt()); + assert_eq!(stored_owner[2], Felt::new(0)); // nominated cleared + assert_eq!(stored_owner[3], Felt::new(0)); Ok(()) } @@ -989,8 +986,6 @@ async fn test_network_faucet_only_owner_can_transfer() -> anyhow::Result<()> { ); let source_manager = Arc::new(DefaultSourceManager::default()); - let transfer_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(transfer_note_script_code.clone())?; // Create a note from NON-OWNER that tries to transfer ownership let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); @@ -1003,14 +998,11 @@ async fn test_network_faucet_only_owner_can_transfer() -> anyhow::Result<()> { let tx_context = mock_chain .build_tx_context(faucet.id(), &[], &[transfer_note])? - .add_note_script(transfer_note_script.clone()) .with_source_manager(source_manager.clone()) .build()?; let result = tx_context.execute().await; - // Verify that the transaction failed with ERR_ONLY_OWNER - let expected_error = ERR_SENDER_NOT_OWNER; - assert_transaction_executor_error!(result, expected_error); + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); Ok(()) } @@ -1037,10 +1029,9 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("NET", 1000, owner_account_id, Some(50))?; // Check stored value before renouncing - let stored_owner_before = - faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot())?; - assert_eq!(stored_owner_before[3], owner_account_id.prefix().as_felt()); - assert_eq!(stored_owner_before[2], Felt::new(owner_account_id.suffix().as_canonical_u64())); + let stored_owner_before = faucet.storage().get_item(Ownable2Step::slot_name())?; + assert_eq!(stored_owner_before[0], Felt::new(owner_account_id.suffix().as_canonical_u64())); + assert_eq!(stored_owner_before[1], owner_account_id.prefix().as_felt()); // Create renounce_ownership note script let renounce_note_script_code = r#" @@ -1054,8 +1045,6 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { "#; let source_manager = Arc::new(DefaultSourceManager::default()); - let renounce_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(renounce_note_script_code)?; // Create transfer note script (will be used after renounce) let transfer_note_script_code = format!( @@ -1074,9 +1063,6 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { new_owner_suffix = Felt::new(new_owner_account_id.suffix().as_canonical_u64()), ); - let transfer_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(transfer_note_script_code.clone())?; - let mut rng = RpoRandomCoin::new([Felt::from(200u32); 4].into()); let renounce_note = NoteBuilder::new(owner_account_id, &mut rng) .note_type(NoteType::Private) @@ -1101,7 +1087,6 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { // Execute renounce_ownership let tx_context = mock_chain .build_tx_context(faucet.id(), &[renounce_note.id()], &[])? - .add_note_script(renounce_note_script.clone()) .with_source_manager(source_manager.clone()) .build()?; let executed_transaction = tx_context.execute().await?; @@ -1113,27 +1098,22 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { updated_faucet.apply_delta(executed_transaction.account_delta())?; // Check stored value after renouncing - should be zero - let stored_owner_after = - updated_faucet.storage().get_item(NetworkFungibleFaucet::owner_config_slot())?; + let stored_owner_after = updated_faucet.storage().get_item(Ownable2Step::slot_name())?; assert_eq!(stored_owner_after[0], Felt::new(0)); assert_eq!(stored_owner_after[1], Felt::new(0)); assert_eq!(stored_owner_after[2], Felt::new(0)); assert_eq!(stored_owner_after[3], Felt::new(0)); // Try to transfer ownership - should fail because there's no owner - // The transfer note was already added to the builder, so we need to prove another block - // to make it available on-chain after the renounce transaction mock_chain.prove_next_block()?; let tx_context = mock_chain .build_tx_context(updated_faucet.id(), &[transfer_note.id()], &[])? - .add_note_script(transfer_note_script.clone()) .with_source_manager(source_manager.clone()) .build()?; let result = tx_context.execute().await; - let expected_error = ERR_SENDER_NOT_OWNER; - assert_transaction_executor_error!(result, expected_error); + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); Ok(()) } diff --git a/crates/miden-testing/tests/scripts/mod.rs b/crates/miden-testing/tests/scripts/mod.rs index 58bf4152ad..8d15402744 100644 --- a/crates/miden-testing/tests/scripts/mod.rs +++ b/crates/miden-testing/tests/scripts/mod.rs @@ -1,5 +1,6 @@ mod faucet; mod fee; +mod ownable2step; mod p2id; mod p2ide; mod send_note; diff --git a/crates/miden-testing/tests/scripts/ownable2step.rs b/crates/miden-testing/tests/scripts/ownable2step.rs new file mode 100644 index 0000000000..b265ba9449 --- /dev/null +++ b/crates/miden-testing/tests/scripts/ownable2step.rs @@ -0,0 +1,491 @@ +extern crate alloc; + +use alloc::sync::Arc; + +use miden_processor::crypto::random::RpoRandomCoin; +use miden_protocol::Felt; +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountComponent, + AccountId, + AccountStorageMode, + AccountType, + StorageSlot, +}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::assembly::debuginfo::SourceManagerSync; +use miden_protocol::note::Note; +use miden_protocol::testing::account_id::AccountIdBuilder; +use miden_protocol::transaction::OutputNote; +use miden_standards::account::access::Ownable2Step; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::{ + ERR_NO_NOMINATED_OWNER, + ERR_SENDER_NOT_NOMINATED_OWNER, + ERR_SENDER_NOT_OWNER, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; + +// HELPERS +// ================================================================================================ + +fn create_ownable_account( + owner: AccountId, + initial_storage: Vec, +) -> anyhow::Result { + let component_code = r#" + use miden::standards::access::ownable2step + pub use ownable2step::get_owner + pub use ownable2step::get_nominated_owner + pub use ownable2step::transfer_ownership + pub use ownable2step::accept_ownership + pub use ownable2step::renounce_ownership + "#; + let component_code_obj = + CodeBuilder::default().compile_component_code("test::ownable", component_code)?; + + let mut storage_slots = initial_storage; + storage_slots.push(Ownable2Step::new(owner).to_storage_slot()); + + let account = AccountBuilder::new([1; 32]) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(Auth::IncrNonce) + .with_component({ + let metadata = AccountComponentMetadata::new("test::ownable", AccountType::all()); + AccountComponent::new(component_code_obj, storage_slots, metadata)? + }) + .build_existing()?; + Ok(account) +} + +fn get_owner_from_storage(account: &Account) -> anyhow::Result> { + let ownable = Ownable2Step::try_from_storage(account.storage())?; + Ok(ownable.owner()) +} + +fn get_nominated_owner_from_storage(account: &Account) -> anyhow::Result> { + let ownable = Ownable2Step::try_from_storage(account.storage())?; + Ok(ownable.nominated_owner()) +} + +fn create_transfer_note( + sender: AccountId, + new_owner: AccountId, + rng: &mut RpoRandomCoin, + source_manager: Arc, +) -> anyhow::Result { + let script = format!( + r#" + use miden::standards::access::ownable2step->test_account + begin + repeat.14 push.0 end + push.{new_owner_prefix} + push.{new_owner_suffix} + call.test_account::transfer_ownership + dropw dropw dropw dropw + end + "#, + new_owner_prefix = new_owner.prefix().as_felt(), + new_owner_suffix = Felt::new(new_owner.suffix().as_canonical_u64()), + ); + + let note = NoteBuilder::new(sender, rng) + .source_manager(source_manager) + .code(script) + .build()?; + + Ok(note) +} + +fn create_accept_note( + sender: AccountId, + rng: &mut RpoRandomCoin, + source_manager: Arc, +) -> anyhow::Result { + let script = r#" + use miden::standards::access::ownable2step->test_account + begin + repeat.16 push.0 end + call.test_account::accept_ownership + dropw dropw dropw dropw + end + "#; + + let note = NoteBuilder::new(sender, rng) + .source_manager(source_manager) + .code(script) + .build()?; + + Ok(note) +} + +fn create_renounce_note( + sender: AccountId, + rng: &mut RpoRandomCoin, + source_manager: Arc, +) -> anyhow::Result { + let script = r#" + use miden::standards::access::ownable2step->test_account + begin + repeat.16 push.0 end + call.test_account::renounce_ownership + dropw dropw dropw dropw + end + "#; + + let note = NoteBuilder::new(sender, rng) + .source_manager(source_manager) + .code(script) + .build()?; + + Ok(note) +} + +// TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_transfer_ownership_only_owner() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + let non_owner = AccountIdBuilder::new().build_with_seed([2; 32]); + let new_owner = AccountIdBuilder::new().build_with_seed([3; 32]); + + let account = create_ownable_account(owner, vec![])?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let note = create_transfer_note(non_owner, new_owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[note.id()], &[])? + .with_source_manager(source_manager) + .build()?; + let result = tx.execute().await; + + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + Ok(()) +} + +#[tokio::test] +async fn test_complete_ownership_transfer() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + let new_owner = AccountIdBuilder::new().build_with_seed([2; 32]); + + let account = create_ownable_account(owner, vec![])?; + + // Step 1: transfer ownership + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let transfer_note = + create_transfer_note(owner, new_owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(transfer_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[transfer_note.id()], &[])? + .with_source_manager(Arc::clone(&source_manager)) + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + // Verify intermediate state: owner unchanged, nominated set + assert_eq!(get_owner_from_storage(&updated)?, Some(owner)); + assert_eq!(get_nominated_owner_from_storage(&updated)?, Some(new_owner)); + + // Commit step 1 to the chain + mock_chain.add_pending_executed_transaction(&executed)?; + mock_chain.prove_next_block()?; + + // Step 2: accept ownership + let mut rng2 = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let accept_note = create_accept_note(new_owner, &mut rng2, Arc::clone(&source_manager))?; + + let tx2 = mock_chain + .build_tx_context(updated.clone(), &[], std::slice::from_ref(&accept_note))? + .with_source_manager(source_manager) + .build()?; + let executed2 = tx2.execute().await?; + + let mut final_account = updated.clone(); + final_account.apply_delta(executed2.account_delta())?; + + assert_eq!(get_owner_from_storage(&final_account)?, Some(new_owner)); + assert_eq!(get_nominated_owner_from_storage(&final_account)?, None); + Ok(()) +} + +#[tokio::test] +async fn test_accept_ownership_only_nominated_owner() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + let new_owner = AccountIdBuilder::new().build_with_seed([2; 32]); + let wrong = AccountIdBuilder::new().build_with_seed([3; 32]); + + let account = create_ownable_account(owner, vec![])?; + + // Step 1: transfer + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let transfer_note = + create_transfer_note(owner, new_owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(transfer_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[transfer_note.id()], &[])? + .with_source_manager(Arc::clone(&source_manager)) + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + // Commit step 1 to the chain + mock_chain.add_pending_executed_transaction(&executed)?; + mock_chain.prove_next_block()?; + + // Step 2: wrong account tries accept + let mut rng2 = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let accept_note = create_accept_note(wrong, &mut rng2, Arc::clone(&source_manager))?; + + let tx2 = mock_chain + .build_tx_context(updated.clone(), &[], std::slice::from_ref(&accept_note))? + .with_source_manager(source_manager) + .build()?; + let result = tx2.execute().await; + + assert_transaction_executor_error!(result, ERR_SENDER_NOT_NOMINATED_OWNER); + Ok(()) +} + +#[tokio::test] +async fn test_accept_ownership_no_nominated() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + + let account = create_ownable_account(owner, vec![])?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let accept_note = create_accept_note(owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(accept_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[accept_note.id()], &[])? + .with_source_manager(source_manager) + .build()?; + let result = tx.execute().await; + + assert_transaction_executor_error!(result, ERR_NO_NOMINATED_OWNER); + Ok(()) +} + +#[tokio::test] +async fn test_cancel_transfer() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + let new_owner = AccountIdBuilder::new().build_with_seed([2; 32]); + + let account = create_ownable_account(owner, vec![])?; + + // Step 1: transfer + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let transfer_note = + create_transfer_note(owner, new_owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(transfer_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[transfer_note.id()], &[])? + .with_source_manager(Arc::clone(&source_manager)) + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + // Commit step 1 to the chain + mock_chain.add_pending_executed_transaction(&executed)?; + mock_chain.prove_next_block()?; + + // Step 2: cancel by transferring to self (owner) + let mut rng2 = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let cancel_note = create_transfer_note(owner, owner, &mut rng2, Arc::clone(&source_manager))?; + + let tx2 = mock_chain + .build_tx_context(updated.clone(), &[], std::slice::from_ref(&cancel_note))? + .with_source_manager(source_manager) + .build()?; + let executed2 = tx2.execute().await?; + + let mut final_account = updated.clone(); + final_account.apply_delta(executed2.account_delta())?; + + assert_eq!(get_nominated_owner_from_storage(&final_account)?, None); + assert_eq!(get_owner_from_storage(&final_account)?, Some(owner)); + Ok(()) +} + +/// Tests that an owner can transfer to themselves when no nominated transfer exists. +/// This is a no-op but should succeed without errors. +#[tokio::test] +async fn test_transfer_to_self_no_nominated() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + + let account = create_ownable_account(owner, vec![])?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let note = create_transfer_note(owner, owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[note.id()], &[])? + .with_source_manager(source_manager) + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + assert_eq!(get_owner_from_storage(&updated)?, Some(owner)); + assert_eq!(get_nominated_owner_from_storage(&updated)?, None); + Ok(()) +} + +#[tokio::test] +async fn test_renounce_ownership() -> anyhow::Result<()> { + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + let new_owner = AccountIdBuilder::new().build_with_seed([2; 32]); + + let account = create_ownable_account(owner, vec![])?; + + // Step 1: transfer (to have nominated) + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let transfer_note = + create_transfer_note(owner, new_owner, &mut rng, Arc::clone(&source_manager))?; + + builder.add_output_note(OutputNote::Full(transfer_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[transfer_note.id()], &[])? + .with_source_manager(Arc::clone(&source_manager)) + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + // Commit step 1 to the chain + mock_chain.add_pending_executed_transaction(&executed)?; + mock_chain.prove_next_block()?; + + // Step 2: renounce + let mut rng2 = RpoRandomCoin::new([Felt::from(200u32); 4].into()); + let renounce_note = create_renounce_note(owner, &mut rng2, Arc::clone(&source_manager))?; + + let tx2 = mock_chain + .build_tx_context(updated.clone(), &[], std::slice::from_ref(&renounce_note))? + .with_source_manager(source_manager) + .build()?; + let executed2 = tx2.execute().await?; + + let mut final_account = updated.clone(); + final_account.apply_delta(executed2.account_delta())?; + + assert_eq!(get_owner_from_storage(&final_account)?, None); + assert_eq!(get_nominated_owner_from_storage(&final_account)?, None); + Ok(()) +} + +/// Tests that transfer_ownership fails when the new owner account ID is invalid. +/// An invalid account ID has its suffix's lower 8 bits set to a non-zero value. +#[tokio::test] +async fn test_transfer_ownership_fails_with_invalid_account_id() -> anyhow::Result<()> { + use miden_protocol::errors::protocol::ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO; + + let owner = AccountIdBuilder::new().build_with_seed([1; 32]); + + let account = create_ownable_account(owner, vec![])?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + let invalid_prefix = owner.prefix().as_felt(); + let invalid_suffix = Felt::new(1); + + let script = format!( + r#" + use miden::standards::access::ownable2step->test_account + begin + repeat.14 push.0 end + push.{invalid_suffix} + push.{invalid_prefix} + call.test_account::transfer_ownership + dropw dropw dropw dropw + end + "#, + ); + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let mut rng = RpoRandomCoin::new([Felt::from(100u32); 4].into()); + let note = NoteBuilder::new(owner, &mut rng) + .source_manager(Arc::clone(&source_manager)) + .code(script) + .build()?; + + builder.add_output_note(OutputNote::Full(note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let tx = mock_chain + .build_tx_context(account.id(), &[note.id()], &[])? + .with_source_manager(source_manager) + .build()?; + let result = tx.execute().await; + + assert_transaction_executor_error!( + result, + ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO + ); + Ok(()) +} From cf79614086e494239ddaf24fd858af34e7dcc4ca Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 10:31:07 -0300 Subject: [PATCH 27/56] feat: add token metadata support to NetworkFungibleFaucet --- .../faucets/network_fungible_faucet.masm | 1 - .../src/account/faucets/network_fungible.rs | 90 +++++++------------ .../src/mock_chain/chain_builder.rs | 5 +- 3 files changed, 31 insertions(+), 65 deletions(-) diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index e6effc5ef5..6ff900b881 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -11,7 +11,6 @@ pub use ::miden::standards::faucets::network_fungible::accept_ownership pub use ::miden::standards::faucets::network_fungible::renounce_ownership # Metadata — all from metadata::fungible -pub use ::miden::standards::metadata::fungible::get_owner pub use ::miden::standards::metadata::fungible::get_name pub use ::miden::standards::metadata::fungible::get_mutability_config pub use ::miden::standards::metadata::fungible::is_max_supply_mutable diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index af5347ed46..cab6fd9172 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -13,12 +13,12 @@ use miden_protocol::account::{ AccountStorage, AccountStorageMode, AccountType, + StorageSlot, StorageSlotName, }; use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; -<<<<<<< HEAD use super::{ Description, ExternalLink, @@ -27,23 +27,14 @@ use super::{ LogoURI, TokenName, }; -======= -use super::{FungibleFaucetError, TokenMetadata}; -use crate::account::access::Ownable2Step; ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; +use crate::account::metadata::TokenMetadata as TokenMetadataInfo; use crate::procedure_digest; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; -<<<<<<< HEAD -use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::TokenMetadata as TokenMetadataInfo; -use crate::procedure_digest; -======= ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ @@ -75,24 +66,16 @@ procedure_digest!( /// authentication while `burn` does not require authentication and can be called by anyone. /// Thus, this component must be combined with a component providing authentication. /// -/// Ownership is managed via a two-step transfer pattern ([`Ownable2Step`]). The current owner -/// must first nominate a new owner, who then accepts the transfer. -/// /// ## Storage Layout /// /// - [`Self::metadata_slot`]: Fungible faucet metadata. -/// - [`Ownable2Step::slot_name`]: The owner and nominated owner of this network faucet. +/// - [`Self::owner_config_slot`]: The owner account of this network faucet. /// /// [builder]: crate::code_builder::CodeBuilder pub struct NetworkFungibleFaucet { -<<<<<<< HEAD metadata: FungibleTokenMetadata, owner_account_id: AccountId, info: Option, -======= - metadata: TokenMetadata, - ownership: Ownable2Step, ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } impl NetworkFungibleFaucet { @@ -131,7 +114,6 @@ impl NetworkFungibleFaucet { logo_uri: Option, external_link: Option, ) -> Result { -<<<<<<< HEAD let metadata = FungibleTokenMetadata::new( symbol, decimals, @@ -142,18 +124,12 @@ impl NetworkFungibleFaucet { external_link, )?; Ok(Self { metadata, owner_account_id, info: None }) -======= - let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - let ownership = Ownable2Step::new(owner_account_id); - Ok(Self { metadata, ownership }) ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } /// Creates a new [`NetworkFungibleFaucet`] component from the given [`FungibleTokenMetadata`]. /// /// This is a convenience constructor that allows creating a faucet from pre-validated /// metadata. -<<<<<<< HEAD pub fn from_metadata(metadata: FungibleTokenMetadata, owner_account_id: AccountId) -> Self { Self { metadata, owner_account_id, info: None } } @@ -163,11 +139,6 @@ impl NetworkFungibleFaucet { pub fn with_info(mut self, info: TokenMetadataInfo) -> Self { self.info = Some(info); self -======= - pub fn from_metadata(metadata: TokenMetadata, owner_account_id: AccountId) -> Self { - let ownership = Ownable2Step::new(owner_account_id); - Self { metadata, ownership } ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account @@ -198,11 +169,14 @@ impl NetworkFungibleFaucet { // Read token metadata from storage let metadata = FungibleTokenMetadata::try_from(storage)?; - // Read ownership data from storage - let ownership = - Ownable2Step::try_from_storage(storage).map_err(FungibleFaucetError::OwnershipError)?; + // obtain owner account ID from the next storage slot + let owner_account_id_word: Word = storage + .get_item(NetworkFungibleFaucet::owner_config_slot()) + .map_err(|err| FungibleFaucetError::StorageLookupFailed { + slot_name: NetworkFungibleFaucet::owner_config_slot().clone(), + source: err, + })?; -<<<<<<< HEAD // Convert Word back to AccountId // Storage format: [0, 0, suffix, prefix] let prefix = owner_account_id_word[3]; @@ -210,9 +184,6 @@ impl NetworkFungibleFaucet { let owner_account_id = AccountId::new_unchecked([prefix, suffix]); Ok(Self { metadata, owner_account_id, info: None }) -======= - Ok(Self { metadata, ownership }) ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) } // PUBLIC ACCESSORS @@ -224,15 +195,12 @@ impl NetworkFungibleFaucet { FungibleTokenMetadata::metadata_slot() } -<<<<<<< HEAD /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s owner configuration is /// stored (slot 1). pub fn owner_config_slot() -> &'static StorageSlotName { crate::account::metadata::owner_config_slot() } -======= ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) /// Returns the storage slot schema for the metadata slot. pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); @@ -250,6 +218,22 @@ impl NetworkFungibleFaucet { ) } + /// Returns the storage slot schema for the owner configuration slot. + pub fn owner_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::owner_config_slot().clone(), + StorageSlotSchema::value( + "Owner account configuration", + [ + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::felt("owner_suffix"), + FeltSchema::felt("owner_prefix"), + ], + ), + ) + } + /// Returns the token metadata. pub fn metadata(&self) -> &FungibleTokenMetadata { &self.metadata @@ -280,19 +264,9 @@ impl NetworkFungibleFaucet { self.metadata.token_supply() } - /// Returns the owner account ID of the faucet, or `None` if ownership has been renounced. - pub fn owner_account_id(&self) -> Option { - self.ownership.owner() - } - - /// Returns the nominated owner account ID, or `None` if no transfer is in progress. - pub fn nominated_owner(&self) -> Option { - self.ownership.nominated_owner() - } - - /// Returns the ownership data of the faucet. - pub fn ownership(&self) -> &Ownable2Step { - &self.ownership + /// Returns the owner account ID of the faucet. + pub fn owner_account_id(&self) -> AccountId { + self.owner_account_id } /// Returns the digest of the `distribute` account procedure. @@ -323,7 +297,6 @@ impl NetworkFungibleFaucet { impl From for AccountComponent { fn from(network_faucet: NetworkFungibleFaucet) -> Self { let metadata_slot = network_faucet.metadata.into(); -<<<<<<< HEAD let owner_account_id_word: Word = [ Felt::new(0), @@ -337,9 +310,6 @@ impl From for AccountComponent { NetworkFungibleFaucet::owner_config_slot().clone(), owner_account_id_word, ); -======= - let owner_slot = network_faucet.ownership.to_storage_slot(); ->>>>>>> 698fa6fb (feat: implement `Ownable2Step` (#2292)) let mut slots = vec![metadata_slot, owner_slot]; if let Some(info) = &network_faucet.info { @@ -348,7 +318,7 @@ impl From for AccountComponent { let storage_schema = StorageSchema::new([ NetworkFungibleFaucet::metadata_slot_schema(), - Ownable2Step::slot_schema(), + NetworkFungibleFaucet::owner_config_slot_schema(), ]) .expect("storage schema should be valid"); diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 6c38c20add..3c0d7dd16b 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -407,8 +407,6 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let info = TokenMetadataInfo::new().with_name(name.clone()); - let network_faucet = NetworkFungibleFaucet::new( token_symbol, DEFAULT_FAUCET_DECIMALS, @@ -420,8 +418,7 @@ impl MockChainBuilder { None, ) .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")? - .with_info(info); + .context("failed to create network fungible faucet")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) From 68cc2fe5762d2379f8e46050989293af6ddeb88f Mon Sep 17 00:00:00 2001 From: Forostovec Date: Fri, 6 Mar 2026 08:44:26 +0200 Subject: [PATCH 28/56] feat: enforce maximum serialized size for output notes (#2205) --- crates/miden-protocol/src/transaction/mod.rs | 4 +- .../src/transaction/proven_tx.rs | 165 ++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index 04da8757f0..6d472f013b 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -29,7 +29,9 @@ pub use outputs::{ TransactionOutputs, }; pub use partial_blockchain::PartialBlockchain; -pub use proven_tx::{InputNoteCommitment, ProvenTransaction, TxAccountUpdate}; +pub use proven_tx::{ + InputNoteCommitment, ProvenTransaction, ProvenTransactionBuilder, TxAccountUpdate, +}; pub use transaction_id::TransactionId; pub use tx_args::{TransactionArgs, TransactionScript}; pub use tx_header::TransactionHeader; diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index bb434a0631..fb4b338c28 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -303,6 +303,171 @@ impl Deserializable for ProvenTransaction { } } +// PROVEN TRANSACTION BUILDER +// ================================================================================================ + +/// Builder for a proven transaction. +#[derive(Clone, Debug)] +pub struct ProvenTransactionBuilder { + /// ID of the account that the transaction was executed against. + account_id: AccountId, + + /// The commitment of the account before the transaction was executed. + initial_account_commitment: Word, + + /// The commitment of the account after the transaction was executed. + final_account_commitment: Word, + + /// The commitment of the account delta produced by the transaction. + account_delta_commitment: Word, + + /// State changes to the account due to the transaction. + account_update_details: AccountUpdateDetails, + + /// List of [InputNoteCommitment]s of all consumed notes by the transaction. + input_notes: Vec, + + /// List of [`ProvenOutputNote`]s of all notes created by the transaction. + output_notes: Vec, + + /// [`BlockNumber`] of the transaction's reference block. + ref_block_num: BlockNumber, + + /// Block digest of the transaction's reference block. + ref_block_commitment: Word, + + /// The fee of the transaction. + fee: FungibleAsset, + + /// The block number by which the transaction will expire, as defined by the executed scripts. + expiration_block_num: BlockNumber, + + /// A STARK proof that attests to the correct execution of the transaction. + proof: ExecutionProof, +} + +impl ProvenTransactionBuilder { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + + /// Returns a [ProvenTransactionBuilder] used to build a [ProvenTransaction]. + #[allow(clippy::too_many_arguments)] + pub fn new( + account_id: AccountId, + initial_account_commitment: Word, + final_account_commitment: Word, + account_delta_commitment: Word, + ref_block_num: BlockNumber, + ref_block_commitment: Word, + fee: FungibleAsset, + expiration_block_num: BlockNumber, + proof: ExecutionProof, + ) -> Self { + Self { + account_id, + initial_account_commitment, + final_account_commitment, + account_delta_commitment, + account_update_details: AccountUpdateDetails::Private, + input_notes: Vec::new(), + output_notes: Vec::new(), + ref_block_num, + ref_block_commitment, + fee, + expiration_block_num, + proof, + } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Sets the account's update details. + pub fn account_update_details(mut self, details: AccountUpdateDetails) -> Self { + self.account_update_details = details; + self + } + + /// Add notes consumed by the transaction. + pub fn add_input_notes(mut self, notes: I) -> Self + where + I: IntoIterator, + T: Into, + { + self.input_notes.extend(notes.into_iter().map(|note| note.into())); + self + } + + /// Add notes produced by the transaction. + pub fn add_output_notes(mut self, notes: T) -> Self + where + T: IntoIterator, + { + self.output_notes.extend(notes); + self + } + + /// Builds the [`ProvenTransaction`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - The total number of input notes is greater than + /// [`MAX_INPUT_NOTES_PER_TX`](crate::constants::MAX_INPUT_NOTES_PER_TX). + /// - The vector of input notes contains duplicates. + /// - The total number of output notes is greater than + /// [`MAX_OUTPUT_NOTES_PER_TX`](crate::constants::MAX_OUTPUT_NOTES_PER_TX). + /// - The vector of output notes contains duplicates. + /// - The transaction is empty, which is the case if the account state is unchanged or the + /// number of input notes is zero. + /// - The commitment computed on the actual account delta contained in [`TxAccountUpdate`] does + /// not match its declared account delta commitment. + /// - The size of the serialized account update exceeds [`ACCOUNT_UPDATE_MAX_SIZE`]. + /// - The transaction was executed against a _new_ account with public state and its account ID + /// does not match the ID in the account update. + /// - The transaction was executed against a _new_ account with public state and its commitment + /// does not match the final state commitment of the account update. + /// - The transaction creates a _new_ account with public state and the update is of type + /// [`AccountUpdateDetails::Delta`] but the account delta is not a full state delta. + /// - The transaction was executed against a private account and the account update is _not_ of + /// type [`AccountUpdateDetails::Private`]. + /// - The transaction was executed against an account with public state and the update is of + /// type [`AccountUpdateDetails::Private`]. + pub fn build(self) -> Result { + let input_notes = + InputNotes::new(self.input_notes).map_err(ProvenTransactionError::InputNotesError)?; + let output_notes = ProvenOutputNotes::new(self.output_notes) + .map_err(ProvenTransactionError::OutputNotesError)?; + let id = TransactionId::new( + self.initial_account_commitment, + self.final_account_commitment, + input_notes.commitment(), + output_notes.commitment(), + ); + let account_update = TxAccountUpdate::new( + self.account_id, + self.initial_account_commitment, + self.final_account_commitment, + self.account_delta_commitment, + self.account_update_details, + )?; + + let proven_transaction = ProvenTransaction { + id, + account_update, + input_notes, + output_notes, + ref_block_num: self.ref_block_num, + ref_block_commitment: self.ref_block_commitment, + fee: self.fee, + expiration_block_num: self.expiration_block_num, + proof: self.proof, + }; + + proven_transaction.validate() + } +} + // TRANSACTION ACCOUNT UPDATE // ================================================================================================ From 90cf256b1ef7a923bdf7b668a8c50d1052836220 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 09:13:24 -0300 Subject: [PATCH 29/56] feat: implement `Ownable2Step` (#2292) * feat: implement two-step ownership management for account components - Add `ownable2step.masm` to provide two-step ownership functionality, requiring explicit acceptance of ownership transfers. - Create Rust module for `Ownable2Step` struct to manage ownership state and storage layout. - Introduce error handling for ownership operations in `standards.rs`. - Develop comprehensive tests for ownership transfer, acceptance, and renouncement scenarios in `ownable2step.rs`. * feat: add Ownable2Step account component for two-step ownership transfer * fix: correct ownership word layout and update transfer ownership logic in ownable2step * Refactor ownership management to use Ownable2Step pattern - Updated the access control mechanism to implement a two-step ownership transfer pattern in the ownable2step module. - Modified the storage layout to accommodate the new ownership structure, reversing the order of owner and nominated owner fields. - Refactored the network fungible faucet to utilize the new Ownable2Step for ownership management. - Adjusted related tests to validate the new ownership transfer logic and ensure correct behavior when transferring and accepting ownership. - Updated error handling to reflect changes in ownership management, including new error messages for ownership transfer scenarios. * refactor: update ownership word layout and adjust related logic in Ownable2Step * refactor: rename owner and nominated_owner to get_owner and get_nominated_owner for consistency * refactor: streamline ownership management in tests by utilizing Ownable2Step according to philip * fix: `make format` --------- Co-authored-by: Bobbin Threadbare <43513081+bobbinth@users.noreply.github.com> Co-authored-by: Philipp Gackstatter --- crates/miden-protocol/src/transaction/proven_tx.rs | 1 + crates/miden-standards/src/account/faucets/network_fungible.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index fb4b338c28..1f532c0d3f 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -443,6 +443,7 @@ impl ProvenTransactionBuilder { self.final_account_commitment, input_notes.commitment(), output_notes.commitment(), + self.fee, ); let account_update = TxAccountUpdate::new( self.account_id, diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index cab6fd9172..a0f0a53e8d 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -169,7 +169,7 @@ impl NetworkFungibleFaucet { // Read token metadata from storage let metadata = FungibleTokenMetadata::try_from(storage)?; - // obtain owner account ID from the next storage slot + // Obtain owner account ID from storage let owner_account_id_word: Word = storage .get_item(NetworkFungibleFaucet::owner_config_slot()) .map_err(|err| FungibleFaucetError::StorageLookupFailed { From 88c69d4caae7a2e7c6ed793db7536194d160a1e7 Mon Sep 17 00:00:00 2001 From: Himess <95512809+Himess@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:24:30 +0300 Subject: [PATCH 30/56] feat: prefix account components with miden::standards namespace (#2400) --- CHANGELOG.md | 1 + crates/miden-standards/build.rs | 15 ++++++++++++++- .../miden-standards/src/account/auth/multisig.rs | 2 +- .../src/account/auth/multisig_psm.rs | 2 +- .../miden-standards/src/account/auth/no_auth.rs | 2 +- .../miden-standards/src/account/auth/singlesig.rs | 2 +- .../src/account/auth/singlesig_acl.rs | 2 +- .../src/account/faucets/basic_fungible.rs | 8 +++++--- .../src/account/faucets/network_fungible.rs | 8 +++++--- crates/miden-standards/src/account/mod.rs | 14 ++++++++++---- crates/miden-standards/src/account/wallets/mod.rs | 8 +++++--- .../miden-testing/tests/auth/hybrid_multisig.rs | 6 +++--- crates/miden-testing/tests/auth/multisig.rs | 14 +++++++------- crates/miden-testing/tests/auth/multisig_psm.rs | 6 +++--- 14 files changed, 58 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d66ddd5552..c42c8efc36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - Introduced `StorageMapKey` and `StorageMapKeyHash` Word wrappers for type-safe storage map key handling ([#2431](https://github.com/0xMiden/miden-base/pull/2431)). - Restructured `miden-agglayer/asm` directory to separate bridge and faucet into per-component libraries, preventing cross-component procedure exposure ([#2294](https://github.com/0xMiden/miden-base/issues/2294)). - `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. +- Prefixed standard account component names with `miden::standards::components` ([#2400](https://github.com/0xMiden/miden-base/pull/2400)). - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). - Fixed MASM inline comment casing to adhere to commenting conventions ([#2398](https://github.com/0xMiden/miden-base/pull/2398)). diff --git a/crates/miden-standards/build.rs b/crates/miden-standards/build.rs index 477750eec0..d41e453948 100644 --- a/crates/miden-standards/build.rs +++ b/crates/miden-standards/build.rs @@ -15,6 +15,7 @@ const ASM_STANDARDS_DIR: &str = "standards"; const ASM_ACCOUNT_COMPONENTS_DIR: &str = "account_components"; const STANDARDS_LIB_NAMESPACE: &str = "miden::standards"; +const ACCOUNT_COMPONENTS_LIB_NAMESPACE: &str = "miden::standards::components"; const STANDARDS_ERRORS_RS_FILE: &str = "standards_errors.rs"; const STANDARDS_ERRORS_ARRAY_NAME: &str = "STANDARDS_ERRORS"; @@ -107,7 +108,19 @@ fn compile_account_components( let component_source_code = fs::read_to_string(&masm_file_path) .expect("reading the component's MASM source code should succeed"); - let named_source = NamedSource::new(component_name.clone(), component_source_code); + // Build full library path from directory structure: + // e.g. faucets/basic_fungible_faucet.masm -> + // miden::standards::components::faucets::basic_fungible_faucet + let relative_path = masm_file_path + .strip_prefix(source_dir) + .expect("masm file should be inside source dir"); + let mut library_path = ACCOUNT_COMPONENTS_LIB_NAMESPACE.to_owned(); + for component in relative_path.with_extension("").components() { + let part = component.as_os_str().to_str().expect("valid UTF-8"); + library_path.push_str("::"); + library_path.push_str(part); + } + let named_source = NamedSource::new(library_path, component_source_code); let component_library = assembler .clone() diff --git a/crates/miden-standards/src/account/auth/multisig.rs b/crates/miden-standards/src/account/auth/multisig.rs index 4d76e3a4aa..3fb73aef6c 100644 --- a/crates/miden-standards/src/account/auth/multisig.rs +++ b/crates/miden-standards/src/account/auth/multisig.rs @@ -142,7 +142,7 @@ pub struct AuthMultisig { impl AuthMultisig { /// The name of the component. - pub const NAME: &'static str = "miden::auth::multisig"; + pub const NAME: &'static str = "miden::standards::components::auth::multisig"; /// Creates a new [`AuthMultisig`] component from the provided configuration. pub fn new(config: AuthMultisigConfig) -> Result { diff --git a/crates/miden-standards/src/account/auth/multisig_psm.rs b/crates/miden-standards/src/account/auth/multisig_psm.rs index 77008949d5..7c7dd3bcf5 100644 --- a/crates/miden-standards/src/account/auth/multisig_psm.rs +++ b/crates/miden-standards/src/account/auth/multisig_psm.rs @@ -194,7 +194,7 @@ pub struct AuthMultisigPsm { impl AuthMultisigPsm { /// The name of the component. - pub const NAME: &'static str = "miden::auth::multisig_psm"; + pub const NAME: &'static str = "miden::standards::components::auth::multisig_psm"; /// Creates a new [`AuthMultisigPsm`] component from the provided configuration. pub fn new(config: AuthMultisigPsmConfig) -> Result { diff --git a/crates/miden-standards/src/account/auth/no_auth.rs b/crates/miden-standards/src/account/auth/no_auth.rs index f19b884e5c..c8f932710b 100644 --- a/crates/miden-standards/src/account/auth/no_auth.rs +++ b/crates/miden-standards/src/account/auth/no_auth.rs @@ -21,7 +21,7 @@ pub struct NoAuth; impl NoAuth { /// The name of the component. - pub const NAME: &'static str = "miden::auth::no_auth"; + pub const NAME: &'static str = "miden::standards::components::auth::no_auth"; /// Creates a new [`NoAuth`] component. pub fn new() -> Self { diff --git a/crates/miden-standards/src/account/auth/singlesig.rs b/crates/miden-standards/src/account/auth/singlesig.rs index 6e7ca5dc12..c0da16aede 100644 --- a/crates/miden-standards/src/account/auth/singlesig.rs +++ b/crates/miden-standards/src/account/auth/singlesig.rs @@ -45,7 +45,7 @@ pub struct AuthSingleSig { impl AuthSingleSig { /// The name of the component. - pub const NAME: &'static str = "miden::auth::singlesig"; + pub const NAME: &'static str = "miden::standards::components::auth::singlesig"; /// Creates a new [`AuthSingleSig`] component with the given `public_key`. pub fn new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self { diff --git a/crates/miden-standards/src/account/auth/singlesig_acl.rs b/crates/miden-standards/src/account/auth/singlesig_acl.rs index 5792c179bc..2ae95a5d15 100644 --- a/crates/miden-standards/src/account/auth/singlesig_acl.rs +++ b/crates/miden-standards/src/account/auth/singlesig_acl.rs @@ -157,7 +157,7 @@ pub struct AuthSingleSigAcl { impl AuthSingleSigAcl { /// The name of the component. - pub const NAME: &'static str = "miden::auth::singlesig_acl"; + pub const NAME: &'static str = "miden::standards::components::auth::singlesig_acl"; /// Creates a new [`AuthSingleSigAcl`] component with the given `public_key` and /// configuration. /// diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index ab69622e7a..6f9a743298 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -41,6 +41,7 @@ use crate::procedure_digest; // Initialize the digest of the `distribute` procedure of the Basic Fungible Faucet only once. procedure_digest!( BASIC_FUNGIBLE_FAUCET_DISTRIBUTE, + BasicFungibleFaucet::NAME, BasicFungibleFaucet::DISTRIBUTE_PROC_NAME, basic_fungible_faucet_library ); @@ -48,6 +49,7 @@ procedure_digest!( // Initialize the digest of the `burn` procedure of the Basic Fungible Faucet only once. procedure_digest!( BASIC_FUNGIBLE_FAUCET_BURN, + BasicFungibleFaucet::NAME, BasicFungibleFaucet::BURN_PROC_NAME, basic_fungible_faucet_library ); @@ -83,13 +85,13 @@ impl BasicFungibleFaucet { // -------------------------------------------------------------------------------------------- /// The name of the component. - pub const NAME: &'static str = "miden::basic_fungible_faucet"; + pub const NAME: &'static str = "miden::standards::components::faucets::basic_fungible_faucet"; /// The maximum number of decimals supported by the component. pub const MAX_DECIMALS: u8 = FungibleTokenMetadata::MAX_DECIMALS; - const DISTRIBUTE_PROC_NAME: &str = "basic_fungible_faucet::distribute"; - const BURN_PROC_NAME: &str = "basic_fungible_faucet::burn"; + const DISTRIBUTE_PROC_NAME: &str = "distribute"; + const BURN_PROC_NAME: &str = "burn"; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index a0f0a53e8d..4fa126d6b2 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -42,6 +42,7 @@ const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::t // Initialize the digest of the `distribute` procedure of the Network Fungible Faucet only once. procedure_digest!( NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE, + NetworkFungibleFaucet::NAME, NetworkFungibleFaucet::DISTRIBUTE_PROC_NAME, network_fungible_faucet_library ); @@ -49,6 +50,7 @@ procedure_digest!( // Initialize the digest of the `burn` procedure of the Network Fungible Faucet only once. procedure_digest!( NETWORK_FUNGIBLE_FAUCET_BURN, + NetworkFungibleFaucet::NAME, NetworkFungibleFaucet::BURN_PROC_NAME, network_fungible_faucet_library ); @@ -83,13 +85,13 @@ impl NetworkFungibleFaucet { // -------------------------------------------------------------------------------------------- /// The name of the component. - pub const NAME: &'static str = "miden::network_fungible_faucet"; + pub const NAME: &'static str = "miden::standards::components::faucets::network_fungible_faucet"; /// The maximum number of decimals supported by the component. pub const MAX_DECIMALS: u8 = FungibleTokenMetadata::MAX_DECIMALS; - const DISTRIBUTE_PROC_NAME: &str = "network_fungible_faucet::distribute"; - const BURN_PROC_NAME: &str = "network_fungible_faucet::burn"; + const DISTRIBUTE_PROC_NAME: &str = "distribute"; + const BURN_PROC_NAME: &str = "burn"; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index 0c11b7f2c1..ad7b67f1f9 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -15,29 +15,35 @@ pub use metadata::AccountBuilderSchemaCommitmentExt; /// This macro generates a `LazyLock` static variable that lazily initializes /// the digest of a procedure from a library. /// +/// The full procedure path is constructed by concatenating `$component_name` and `$proc_name` +/// with `::` as separator (i.e. `"{component_name}::{proc_name}"`). +/// /// Note: This macro references exported types from `miden_protocol`, so your crate must /// include `miden_protocol` as a dependency. /// /// # Arguments /// * `$name` - The name of the static variable to create -/// * `$proc_name` - The string name of the procedure +/// * `$component_name` - The name of the component (e.g. `BasicWallet::NAME`) +/// * `$proc_name` - The short name of the procedure (e.g. `"receive_asset"`) /// * `$library_fn` - The function that returns the library containing the procedure /// /// # Example /// ```ignore /// procedure_digest!( /// BASIC_WALLET_RECEIVE_ASSET, +/// BasicWallet::NAME, /// BasicWallet::RECEIVE_ASSET_PROC_NAME, /// basic_wallet_library /// ); /// ``` #[macro_export] macro_rules! procedure_digest { - ($name:ident, $proc_name:expr, $library_fn:expr) => { + ($name:ident, $component_name:expr, $proc_name:expr, $library_fn:expr) => { static $name: miden_protocol::utils::sync::LazyLock = miden_protocol::utils::sync::LazyLock::new(|| { - $library_fn().get_procedure_root_by_path($proc_name).unwrap_or_else(|| { - panic!("{} should contain '{}' procedure", stringify!($library_fn), $proc_name) + let full_path = alloc::format!("{}::{}", $component_name, $proc_name); + $library_fn().get_procedure_root_by_path(full_path.as_str()).unwrap_or_else(|| { + panic!("{} should contain '{}' procedure", stringify!($library_fn), full_path) }) }); }; diff --git a/crates/miden-standards/src/account/wallets/mod.rs b/crates/miden-standards/src/account/wallets/mod.rs index 292ca19600..4498df12ec 100644 --- a/crates/miden-standards/src/account/wallets/mod.rs +++ b/crates/miden-standards/src/account/wallets/mod.rs @@ -23,6 +23,7 @@ use crate::procedure_digest; // Initialize the digest of the `receive_asset` procedure of the Basic Wallet only once. procedure_digest!( BASIC_WALLET_RECEIVE_ASSET, + BasicWallet::NAME, BasicWallet::RECEIVE_ASSET_PROC_NAME, basic_wallet_library ); @@ -30,6 +31,7 @@ procedure_digest!( // Initialize the digest of the `move_asset_to_note` procedure of the Basic Wallet only once. procedure_digest!( BASIC_WALLET_MOVE_ASSET_TO_NOTE, + BasicWallet::NAME, BasicWallet::MOVE_ASSET_TO_NOTE_PROC_NAME, basic_wallet_library ); @@ -57,10 +59,10 @@ impl BasicWallet { // -------------------------------------------------------------------------------------------- /// The name of the component. - pub const NAME: &'static str = "miden::basic_wallet"; + pub const NAME: &'static str = "miden::standards::components::wallets::basic_wallet"; - const RECEIVE_ASSET_PROC_NAME: &str = "basic_wallet::receive_asset"; - const MOVE_ASSET_TO_NOTE_PROC_NAME: &str = "basic_wallet::move_asset_to_note"; + const RECEIVE_ASSET_PROC_NAME: &str = "receive_asset"; + const MOVE_ASSET_TO_NOTE_PROC_NAME: &str = "move_asset_to_note"; // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-testing/tests/auth/hybrid_multisig.rs b/crates/miden-testing/tests/auth/hybrid_multisig.rs index a1594175d7..2a80f89bef 100644 --- a/crates/miden-testing/tests/auth/hybrid_multisig.rs +++ b/crates/miden-testing/tests/auth/hybrid_multisig.rs @@ -366,7 +366,7 @@ async fn test_multisig_update_signers() -> anyhow::Result<()> { // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::multisig::update_signers_and_threshold + call.::miden::standards::components::auth::multisig::update_signers_and_threshold end "; @@ -627,7 +627,7 @@ async fn test_multisig_update_signers_remove_owner() -> anyhow::Result<()> { // Create transaction script let tx_script = CodeBuilder::default() .with_dynamically_linked_library(multisig_library())? - .compile_tx_script("begin\n call.::multisig::update_signers_and_threshold\nend")?; + .compile_tx_script("begin\n call.::miden::standards::components::auth::multisig::update_signers_and_threshold\nend")?; let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; @@ -840,7 +840,7 @@ async fn test_multisig_new_approvers_cannot_sign_before_update() -> anyhow::Resu // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::multisig::update_signers_and_threshold + call.::miden::standards::components::auth::multisig::update_signers_and_threshold end "; diff --git a/crates/miden-testing/tests/auth/multisig.rs b/crates/miden-testing/tests/auth/multisig.rs index 0690accafc..97d28d8eb0 100644 --- a/crates/miden-testing/tests/auth/multisig.rs +++ b/crates/miden-testing/tests/auth/multisig.rs @@ -469,7 +469,7 @@ async fn test_multisig_update_signers(#[case] auth_scheme: AuthScheme) -> anyhow // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::multisig::update_signers_and_threshold + call.::miden::standards::components::auth::multisig::update_signers_and_threshold end "; @@ -733,7 +733,7 @@ async fn test_multisig_update_signers_remove_owner( // Create transaction script let tx_script = CodeBuilder::default() .with_dynamically_linked_library(multisig_library())? - .compile_tx_script("begin\n call.::multisig::update_signers_and_threshold\nend")?; + .compile_tx_script("begin\n call.::miden::standards::components::auth::multisig::update_signers_and_threshold\nend")?; let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; @@ -919,7 +919,7 @@ async fn test_multisig_update_signers_rejects_unreachable_proc_thresholds( let tx_script = CodeBuilder::default() .with_dynamically_linked_library(multisig_library())? - .compile_tx_script("begin\n call.::multisig::update_signers_and_threshold\nend")?; + .compile_tx_script("begin\n call.::miden::standards::components::auth::multisig::update_signers_and_threshold\nend")?; let advice_inputs = AdviceInputs { map: advice_map, ..Default::default() }; let salt = Word::from([Felt::new(8); 4]); @@ -1022,7 +1022,7 @@ async fn test_multisig_new_approvers_cannot_sign_before_update( // Create a transaction script that calls the update_signers procedure let tx_script_code = " begin - call.::multisig::update_signers_and_threshold + call.::miden::standards::components::auth::multisig::update_signers_and_threshold end "; @@ -1302,7 +1302,7 @@ async fn test_multisig_set_procedure_threshold( begin push.{proc_root} push.1 - call.::multisig::set_procedure_threshold + call.::miden::standards::components::auth::multisig::set_procedure_threshold dropw drop end @@ -1382,7 +1382,7 @@ async fn test_multisig_set_procedure_threshold( begin push.{proc_root} push.0 - call.::multisig::set_procedure_threshold + call.::miden::standards::components::auth::multisig::set_procedure_threshold dropw drop end @@ -1483,7 +1483,7 @@ async fn test_multisig_set_procedure_threshold_rejects_exceeding_approvers( begin push.{proc_root} push.3 - call.::multisig::set_procedure_threshold + call.::miden::standards::components::auth::multisig::set_procedure_threshold end "# ); diff --git a/crates/miden-testing/tests/auth/multisig_psm.rs b/crates/miden-testing/tests/auth/multisig_psm.rs index 2e50c688c5..433ee8a621 100644 --- a/crates/miden-testing/tests/auth/multisig_psm.rs +++ b/crates/miden-testing/tests/auth/multisig_psm.rs @@ -254,7 +254,7 @@ async fn test_multisig_update_psm_public_key( let update_psm_script = CodeBuilder::new() .with_dynamically_linked_library(multisig_psm_library())? .compile_tx_script(format!( - "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::multisig_psm::update_psm_public_key\n drop\n dropw\nend" + "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::miden::standards::components::auth::multisig_psm::update_psm_public_key\n drop\n dropw\nend" ))?; let update_salt = Word::from([Felt::new(991); 4]); @@ -397,7 +397,7 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( let update_psm_script = CodeBuilder::new() .with_dynamically_linked_library(multisig_psm_library())? .compile_tx_script(format!( - "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::multisig_psm::update_psm_public_key\n drop\n dropw\nend" + "begin\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::miden::standards::components::auth::multisig_psm::update_psm_public_key\n drop\n dropw\nend" ))?; let mut mock_chain_builder = @@ -476,7 +476,7 @@ async fn test_multisig_update_psm_public_key_must_be_called_alone( let update_psm_with_output_script = CodeBuilder::new() .with_dynamically_linked_library(multisig_psm_library())? .compile_tx_script(format!( - "use miden::protocol::output_note\nbegin\n push.{recipient}\n push.{note_type}\n push.{tag}\n exec.output_note::create\n swapdw\n dropw\n dropw\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::multisig_psm::update_psm_public_key\n drop\n dropw\nend", + "use miden::protocol::output_note\nbegin\n push.{recipient}\n push.{note_type}\n push.{tag}\n exec.output_note::create\n swapdw\n dropw\n dropw\n push.{new_psm_key_word}\n push.{new_psm_scheme_id}\n call.::miden::standards::components::auth::multisig_psm::update_psm_public_key\n drop\n dropw\nend", recipient = output_note.recipient().digest(), note_type = NoteType::Public as u8, tag = Felt::from(output_note.metadata().tag()), From b9016c54b4c7d099d3603de6d42da6eacbdf5784 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 26 Feb 2026 11:30:33 -0300 Subject: [PATCH 31/56] Refactor code structure for improved readability and maintainability --- CHANGELOG.md | 1 + .../asm/standards/metadata/fungible.masm | 34 ++++++------------- .../src/account/components/mod.rs | 14 ++++++++ .../src/account/faucets/network_fungible.rs | 1 + .../src/account/metadata/mod.rs | 21 +++++------- .../src/mock_chain/chain_builder.rs | 5 ++- 6 files changed, 39 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42c8efc36..f3871c8d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Restructured `miden-agglayer/asm` directory to separate bridge and faucet into per-component libraries, preventing cross-component procedure exposure ([#2294](https://github.com/0xMiden/miden-base/issues/2294)). - `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. - Prefixed standard account component names with `miden::standards::components` ([#2400](https://github.com/0xMiden/miden-base/pull/2400)). +- `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). - Fixed MASM inline comment casing to adhere to commenting conventions ([#2398](https://github.com/0xMiden/miden-base/pull/2398)). diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index f28a2cad98..be554105b4 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -184,18 +184,6 @@ proc get_external_link_chunk_6 push.EXTERNAL_LINK_6_SLOT[0..2] exec.active_account::get_item end - -# ================================================================================================= -# TOKEN METADATA (token_supply, max_supply, decimals, token_symbol) -# ================================================================================================= - -#! Returns the full token metadata word. -#! -#! The word is stored as [token_supply, max_supply, decimals, token_symbol] and -#! loaded onto the stack with word[0] on top (little-endian). -#! -#! Inputs: [] -#! Outputs: [token_supply, max_supply, decimals, token_symbol, pad(12)] (word[0] on top) #! #! Invocation: call pub proc get_token_metadata @@ -817,32 +805,30 @@ pub proc optional_set_external_link end # ================================================================================================= -# OPTIONAL SET MAX SUPPLY (owner-only when max_supply_mutable is 1) +# OPTIONAL SET MAX SUPPLY (owner-only when max_supply_mutable == 1) # ================================================================================================= -#! Updates max_supply if the max_supply_mutable flag is set and the note sender is the owner. -#! The new max_supply is passed on the stack (1 felt). +#! Updates the max supply if the max supply mutability flag is 1 +#! and the note sender is the owner. #! #! Inputs: [new_max_supply, pad(15)] #! Outputs: [pad(16)] #! #! Panics if: -#! - max_supply_mutable flag is 0 (immutable) -#! - note sender is not the owner -#! - new_max_supply < current token_supply +#! - the max supply mutability flag is not 1. +#! - the note sender is not the owner. +#! - the new max supply is less than the current token supply. #! #! Invocation: call (from note script context) pub proc optional_set_max_supply - # 1. Check mutable flag from mutability config word + # 1. Check max supply mutability exec.get_mutability_config_word + swapw dropw # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, new_max_supply, pad(15)] - # max_supply_mutable is word[3]; bring it to top - movup.3 - # => [max_supply_mutable, desc_mutable, logo_mutable, extlink_mutable, new_max_supply, pad(15)] + drop drop drop + # => [max_supply_mutable, new_max_supply, pad(15)] push.1 eq assert.err=ERR_MAX_SUPPLY_IMMUTABLE - # => [desc_mutable, logo_mutable, extlink_mutable, new_max_supply, pad(15)] - drop drop drop # => [new_max_supply, pad(15)] # 2. Verify note sender is the owner diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index f7783a46db..6a96b2c4cd 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -8,6 +8,7 @@ use miden_protocol::assembly::{Library, LibraryExport}; use miden_protocol::utils::serde::Deserializable; use miden_protocol::utils::sync::LazyLock; +use crate::StandardsLib; use crate::account::interface::AccountComponentInterface; // WALLET LIBRARIES @@ -97,6 +98,10 @@ static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed") }); +// Metadata Info component uses the standards library (get_name, get_description, etc. from metadata). +static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = + LazyLock::new(|| Library::from(StandardsLib::default())); + /// Returns the Basic Wallet Library. pub fn basic_wallet_library() -> Library { BASIC_WALLET_LIBRARY.clone() @@ -117,6 +122,15 @@ pub fn storage_schema_library() -> Library { STORAGE_SCHEMA_LIBRARY.clone() } +/// Returns the Metadata Info component library. +/// +/// Uses the standards library; the standalone [`Info`](crate::account::metadata::Info) +/// component exposes get_name, get_description, get_logo_uri, get_external_link from +/// `miden::standards::metadata::fungible`. +pub fn metadata_info_component_library() -> Library { + METADATA_INFO_COMPONENT_LIBRARY.clone() +} + /// Returns the Singlesig Library. pub fn singlesig_library() -> Library { SINGLESIG_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 4fa126d6b2..ebc6c09392 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -372,6 +372,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// /// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] type and /// contains no additional storage slots for its auth ([`NoAuth`]). +#[allow(clippy::too_many_arguments)] pub fn create_network_fungible_faucet( init_seed: [u8; 32], metadata: FungibleTokenMetadata, diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 4113264b80..2a589e3d03 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -238,27 +238,24 @@ pub static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(| ] }); -/// Schema commitment slot. -pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::metadata::storage_schema") - .expect("storage slot name should be valid") -}); - -/// The advice map key used by `optional_set_description` to read the 7 field words. -/// -/// Must match `DESCRIPTION_DATA_KEY` in `fungible.masm`. The value stored under this key -/// should be 28 felts: `[FIELD_0, FIELD_1, FIELD_2, FIELD_3, FIELD_4, FIELD_5, FIELD_6]`. +/// Advice map key for the description field data (7 words). pub const DESCRIPTION_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]); -/// The advice map key used by `optional_set_logo_uri` to read the 7 field words. +/// Advice map key for the logo URI field data (7 words). pub const LOGO_URI_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]); -/// The advice map key used by `optional_set_external_link` to read the 7 field words. +/// Advice map key for the external link field data (7 words). pub const EXTERNAL_LINK_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); +/// Schema commitment slot name. +pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::metadata::schema_commitment") + .expect("storage slot name should be valid") +}); + // SLOT ACCESSORS // ================================================================================================ diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 3c0d7dd16b..6c38c20add 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -407,6 +407,8 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; + let info = TokenMetadataInfo::new().with_name(name.clone()); + let network_faucet = NetworkFungibleFaucet::new( token_symbol, DEFAULT_FAUCET_DECIMALS, @@ -418,7 +420,8 @@ impl MockChainBuilder { None, ) .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")?; + .context("failed to create network fungible faucet")? + .with_info(info); let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) From 75673155067ea6403761c779ad896bd30e6b2a40 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 26 Feb 2026 16:03:23 -0300 Subject: [PATCH 32/56] Refactor metadata handling in fungible faucets - Updated the `create_basic_fungible_faucet` and `create_network_fungible_faucet` functions to use boolean flags for description, logo URI, and external link mutability instead of integer flags. - Modified the `Info` struct to replace the previous flag system with separate boolean fields for initialized and mutable states. - Adjusted storage layout documentation to reflect changes in metadata configuration. - Updated tests to align with the new boolean flag system for mutability and initialization. - Ensured backward compatibility by updating mock chain builder and test cases to use the new structure. --- crates/miden-standards/src/account/metadata/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 2a589e3d03..38c8cc5b59 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -24,7 +24,7 @@ //! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. //! Any change to slot indices or names must be applied in both Rust and MASM. //! -//! ## Config Word +//! ## Config Words //! //! A single config Word stores per-field boolean flags: //! From 0816ec8e2f68ea80225079c9ee49308988c798ea Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Sat, 28 Feb 2026 19:06:26 -0300 Subject: [PATCH 33/56] refactor: update token metadata references in faucet tests to use FungibleTokenMetadata --- .../src/account/components/mod.rs | 22 +++--- .../src/account/metadata/mod.rs | 2 +- crates/miden-testing/tests/metadata.rs | 78 ------------------- 3 files changed, 12 insertions(+), 90 deletions(-) diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 6a96b2c4cd..05faf94cd3 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -8,7 +8,6 @@ use miden_protocol::assembly::{Library, LibraryExport}; use miden_protocol::utils::serde::Deserializable; use miden_protocol::utils::sync::LazyLock; -use crate::StandardsLib; use crate::account::interface::AccountComponentInterface; // WALLET LIBRARIES @@ -89,6 +88,11 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { // METADATA LIBRARIES // ================================================================================================ +// Metadata Info component uses the standards library (get_name, get_description, etc. from +// metadata). +static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = + LazyLock::new(|| Library::from(crate::StandardsLib::default())); + // Initialize the Storage Schema library only once. static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!( @@ -98,10 +102,6 @@ static STORAGE_SCHEMA_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Storage Schema library is well-formed") }); -// Metadata Info component uses the standards library (get_name, get_description, etc. from metadata). -static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = - LazyLock::new(|| Library::from(StandardsLib::default())); - /// Returns the Basic Wallet Library. pub fn basic_wallet_library() -> Library { BASIC_WALLET_LIBRARY.clone() @@ -117,20 +117,20 @@ pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() } -/// Returns the Storage Schema Library. -pub fn storage_schema_library() -> Library { - STORAGE_SCHEMA_LIBRARY.clone() -} - /// Returns the Metadata Info component library. /// -/// Uses the standards library; the standalone [`Info`](crate::account::metadata::Info) +/// Uses the standards library; the standalone [`Info`](crate::account::metadata::TokenMetadata) /// component exposes get_name, get_description, get_logo_uri, get_external_link from /// `miden::standards::metadata::fungible`. pub fn metadata_info_component_library() -> Library { METADATA_INFO_COMPONENT_LIBRARY.clone() } +/// Returns the Storage Schema Library. +pub fn storage_schema_library() -> Library { + STORAGE_SCHEMA_LIBRARY.clone() +} + /// Returns the Singlesig Library. pub fn singlesig_library() -> Library { SINGLESIG_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 38c8cc5b59..2a589e3d03 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -24,7 +24,7 @@ //! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. //! Any change to slot indices or names must be applied in both Rust and MASM. //! -//! ## Config Words +//! ## Config Word //! //! A single config Word stores per-field boolean flags: //! diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 7d2622b152..106fd5d9aa 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -1829,27 +1829,6 @@ async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "LGO", - 1000, - owner_account_id, - Some(0), - false, - None, - Some((logo, false)), // immutable - None, - )?; - let mock_chain = builder.build()?; - - let new_logo = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; let tx_script = r#" begin @@ -1895,15 +1874,6 @@ async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; - let new_logo = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "LGO", @@ -1981,15 +1951,6 @@ async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; - let new_logo = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "LGO", @@ -2057,27 +2018,6 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "EXL", - 1000, - owner_account_id, - Some(0), - false, - None, - None, - Some((link, false)), // immutable - )?; - let mock_chain = builder.build()?; - - let new_link = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; let tx_script = r#" begin @@ -2124,15 +2064,6 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; - let new_link = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "EXL", @@ -2211,15 +2142,6 @@ async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result< Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; - let new_link = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "EXL", From 0a969d528ed4f75a0097b012d268cb414b5ed949 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 5 Mar 2026 11:16:57 -0300 Subject: [PATCH 34/56] chore: adding type as entry instead of separate variables --- crates/miden-standards/src/account/faucets/network_fungible.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index ebc6c09392..4fa126d6b2 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -372,7 +372,6 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// /// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] type and /// contains no additional storage slots for its auth ([`NoAuth`]). -#[allow(clippy::too_many_arguments)] pub fn create_network_fungible_faucet( init_seed: [u8; 32], metadata: FungibleTokenMetadata, From 2fdc7720c072abff1ea520734028b26f821895fc Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 11:32:41 -0300 Subject: [PATCH 35/56] feat: add owner support to TokenMetadata and update related components --- .../src/account/faucets/network_fungible.rs | 2 +- .../src/account/metadata/mod.rs | 22 +++++++++++++++++++ .../src/mock_chain/chain_builder.rs | 1 + crates/miden-testing/tests/metadata.rs | 5 +++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 4fa126d6b2..26cbfcc6aa 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -379,7 +379,7 @@ pub fn create_network_fungible_faucet( ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let mut info = TokenMetadataInfo::new().with_name(metadata.name().clone()); + let mut info = TokenMetadataInfo::new().with_name(metadata.name().clone()).with_owner(owner); if let Some(d) = metadata.description() { info = info.with_description(d.clone(), false); } diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 2a589e3d03..52416f2291 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -78,6 +78,7 @@ use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, + AccountId, AccountStorage, AccountType, StorageSlot, @@ -288,6 +289,7 @@ pub fn mutability_config_slot() -> &'static StorageSlotName { /// - Slot 19–25: external_link (7 Words) #[derive(Debug, Clone, Default)] pub struct TokenMetadata { + owner: Option, name: Option, description: Option, logo_uri: Option, @@ -304,6 +306,16 @@ impl TokenMetadata { Self::default() } + /// Sets the owner of this metadata component. + /// + /// The owner is stored in the `ownable::owner_config` slot and is used by the + /// `metadata::fungible` MASM procedures to authorize mutations (e.g. + /// `optional_set_description`). + pub fn with_owner(mut self, owner: AccountId) -> Self { + self.owner = Some(owner); + self + } + /// Sets whether the max supply can be updated by the owner via /// `optional_set_max_supply`. If `false` (default), the max supply is immutable. pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { @@ -423,6 +435,16 @@ impl TokenMetadata { pub fn storage_slots(&self) -> Vec { let mut slots: Vec = Vec::new(); + // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures + // for get_owner and verify_owner (used in optional_set_* mutations). + // Word layout: [0, 0, owner_suffix, owner_prefix] so that after get_item (which places + // word[0] on top), dropping the two leading zeros yields [owner_suffix, owner_prefix]. + let owner_word = self + .owner + .map(|id| Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()])) + .unwrap_or_default(); + slots.push(StorageSlot::with_value(owner_config_slot().clone(), owner_word)); + let name_words = self.name.as_ref().map(|n| n.to_words()).unwrap_or_default(); slots.push(StorageSlot::with_value( TokenMetadata::name_chunk_0_slot().clone(), diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 6c38c20add..5cc9711d40 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -458,6 +458,7 @@ impl MockChainBuilder { let mut info = TokenMetadataInfo::new() .with_name(name.clone()) + .with_owner(owner_account_id) .with_max_supply_mutable(max_supply_mutable); if let Some((words, mutable)) = description { info = info.with_description( diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 106fd5d9aa..151529d8a7 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -933,11 +933,16 @@ async fn metadata_get_owner_only() -> anyhow::Result<()> { ) .unwrap(); + let info = Info::new() + .with_name(TokenName::new("POL").unwrap()) + .with_owner(owner_account_id); + let account = AccountBuilder::new([4u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Public) .with_auth_component(NoAuth) .with_component(faucet) + .with_component(info) .build()?; let expected_prefix = owner_account_id.prefix().as_felt().as_canonical_u64(); From 7b8aa546cf59e7353943167634a60ae083bd8f91 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 11:34:06 -0300 Subject: [PATCH 36/56] fix: correct output order in get_owner function --- crates/miden-standards/asm/standards/metadata/fungible.masm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index be554105b4..ba09c8fb7f 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -344,7 +344,7 @@ end #! Returns the owner AccountId. #! #! Inputs: [pad(16)] -#! Outputs: [owner_prefix, owner_suffix, pad(14)] +#! Outputs: [owner_suffix, owner_prefix, pad(14)] #! #! Invocation: call pub use ownable::get_owner From 644f66bd773730339c5a8a7fcf0e5ff1c921b511 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 12:43:51 -0300 Subject: [PATCH 37/56] feat: enhance ownership handling in fungible faucets and metadata components --- crates/miden-protocol/src/transaction/mod.rs | 5 +- .../asm/standards/metadata/fungible.masm | 4 +- .../src/account/faucets/mod.rs | 2 + .../src/account/faucets/network_fungible.rs | 42 ++-------- .../src/account/metadata/mod.rs | 29 +++++-- crates/miden-testing/tests/metadata.rs | 82 +++++++++++++++++++ 6 files changed, 123 insertions(+), 41 deletions(-) diff --git a/crates/miden-protocol/src/transaction/mod.rs b/crates/miden-protocol/src/transaction/mod.rs index 6d472f013b..c35d948c56 100644 --- a/crates/miden-protocol/src/transaction/mod.rs +++ b/crates/miden-protocol/src/transaction/mod.rs @@ -30,7 +30,10 @@ pub use outputs::{ }; pub use partial_blockchain::PartialBlockchain; pub use proven_tx::{ - InputNoteCommitment, ProvenTransaction, ProvenTransactionBuilder, TxAccountUpdate, + InputNoteCommitment, + ProvenTransaction, + ProvenTransactionBuilder, + TxAccountUpdate, }; pub use transaction_id::TransactionId; pub use tx_args::{TransactionArgs, TransactionScript}; diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index ba09c8fb7f..95d0bc73a0 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -823,8 +823,10 @@ end pub proc optional_set_max_supply # 1. Check max supply mutability exec.get_mutability_config_word - swapw dropw # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, new_max_supply, pad(15)] + # Note: unlike other optional_set_* procedures, we cannot use swapw dropw here because + # new_max_supply occupies position 4 and would be discarded. Instead, drop 3 elements + # directly; the assert then brings the stack back to depth 16. drop drop drop # => [max_supply_mutable, new_max_supply, pad(15)] push.1 eq diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 9ac35b9dba..c5d4d1e47e 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -54,4 +54,6 @@ pub enum FungibleFaucetError { NotAFungibleFaucetAccount, #[error("failed to read ownership data from storage")] OwnershipError(#[source] Ownable2StepError), + #[error("network faucet ownership has been renounced (no owner)")] + OwnershipRenounced, } diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 26cbfcc6aa..1dd7dc638c 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -27,6 +27,7 @@ use super::{ LogoURI, TokenName, }; +use crate::account::access::Ownable2Step; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; @@ -171,19 +172,10 @@ impl NetworkFungibleFaucet { // Read token metadata from storage let metadata = FungibleTokenMetadata::try_from(storage)?; - // Obtain owner account ID from storage - let owner_account_id_word: Word = storage - .get_item(NetworkFungibleFaucet::owner_config_slot()) - .map_err(|err| FungibleFaucetError::StorageLookupFailed { - slot_name: NetworkFungibleFaucet::owner_config_slot().clone(), - source: err, - })?; - - // Convert Word back to AccountId - // Storage format: [0, 0, suffix, prefix] - let prefix = owner_account_id_word[3]; - let suffix = owner_account_id_word[2]; - let owner_account_id = AccountId::new_unchecked([prefix, suffix]); + // Obtain owner account ID from storage via Ownable2Step + let ownership = + Ownable2Step::try_from_storage(storage).map_err(FungibleFaucetError::OwnershipError)?; + let owner_account_id = ownership.owner().ok_or(FungibleFaucetError::OwnershipRenounced)?; Ok(Self { metadata, owner_account_id, info: None }) } @@ -200,7 +192,7 @@ impl NetworkFungibleFaucet { /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s owner configuration is /// stored (slot 1). pub fn owner_config_slot() -> &'static StorageSlotName { - crate::account::metadata::owner_config_slot() + Ownable2Step::slot_name() } /// Returns the storage slot schema for the metadata slot. @@ -222,18 +214,7 @@ impl NetworkFungibleFaucet { /// Returns the storage slot schema for the owner configuration slot. pub fn owner_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - ( - Self::owner_config_slot().clone(), - StorageSlotSchema::value( - "Owner account configuration", - [ - FeltSchema::new_void(), - FeltSchema::new_void(), - FeltSchema::felt("owner_suffix"), - FeltSchema::felt("owner_prefix"), - ], - ), - ) + Ownable2Step::slot_schema() } /// Returns the token metadata. @@ -300,13 +281,8 @@ impl From for AccountComponent { fn from(network_faucet: NetworkFungibleFaucet) -> Self { let metadata_slot = network_faucet.metadata.into(); - let owner_account_id_word: Word = [ - Felt::new(0), - Felt::new(0), - network_faucet.owner_account_id.suffix(), - network_faucet.owner_account_id.prefix().as_felt(), - ] - .into(); + let ownership = Ownable2Step::new(network_faucet.owner_account_id); + let owner_account_id_word: Word = ownership.to_word(); let owner_slot = StorageSlot::with_value( NetworkFungibleFaucet::owner_config_slot().clone(), diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 52416f2291..342e168480 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -89,7 +89,7 @@ use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use thiserror::Error; -use crate::account::components::storage_schema_library; +use crate::account::components::{metadata_info_component_library, storage_schema_library}; use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; // CONSTANTS — canonical layout: slots 0–22 @@ -439,11 +439,13 @@ impl TokenMetadata { // for get_owner and verify_owner (used in optional_set_* mutations). // Word layout: [0, 0, owner_suffix, owner_prefix] so that after get_item (which places // word[0] on top), dropping the two leading zeros yields [owner_suffix, owner_prefix]. - let owner_word = self - .owner - .map(|id| Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()])) - .unwrap_or_default(); - slots.push(StorageSlot::with_value(owner_config_slot().clone(), owner_word)); + // Only included when an owner is explicitly set, to avoid conflicting with components + // (like NetworkFungibleFaucet) that provide their own owner_config slot. + if let Some(id) = self.owner { + let owner_word = + Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()]); + slots.push(StorageSlot::with_value(owner_config_slot().clone(), owner_word)); + } let name_words = self.name.as_ref().map(|n| n.to_words()).unwrap_or_default(); slots.push(StorageSlot::with_value( @@ -489,6 +491,21 @@ impl TokenMetadata { } } +impl From for AccountComponent { + fn from(info: TokenMetadata) -> Self { + let metadata = + AccountComponentMetadata::new("miden::standards::metadata::info", AccountType::all()) + .with_description( + "Component exposing token name, description, logo URI and external link", + ); + + AccountComponent::new(metadata_info_component_library(), info.storage_slots(), metadata) + .expect( + "TokenMetadata component should satisfy the requirements of a valid account component", + ) + } +} + // SCHEMA COMMITMENT COMPONENT // ================================================================================================ diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 151529d8a7..be3f9519e8 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -933,6 +933,9 @@ async fn metadata_get_owner_only() -> anyhow::Result<()> { ) .unwrap(); + // Info provides the ownable::owner_config slot (single-step) that metadata::fungible::get_owner + // reads from, plus the metadata library. NetworkFungibleFaucet uses ownable2step::owner_config + // (different slot name), so there's no conflict. let info = Info::new() .with_name(TokenName::new("POL").unwrap()) .with_owner(owner_account_id); @@ -1834,6 +1837,27 @@ async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "LGO", + 1000, + owner_account_id, + Some(0), + false, + None, + Some((logo, false)), // immutable + None, + )?; + let mock_chain = builder.build()?; + + let new_logo = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ]; let tx_script = r#" begin @@ -1880,6 +1904,16 @@ async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { Word::from([25u32, 26, 27, 28]), ]; + let new_logo = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ]; + let faucet = builder.add_existing_network_faucet_with_metadata_info( "LGO", 1000, @@ -1956,6 +1990,15 @@ async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; + let new_logo = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "LGO", @@ -2023,6 +2066,27 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "EXL", + 1000, + owner_account_id, + Some(0), + false, + None, + None, + Some((link, false)), // immutable + )?; + let mock_chain = builder.build()?; + + let new_link = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ]; let tx_script = r#" begin @@ -2069,6 +2133,15 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; + let new_link = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "EXL", @@ -2147,6 +2220,15 @@ async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result< Word::from([21u32, 22, 23, 24]), Word::from([25u32, 26, 27, 28]), ]; + let new_link = [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ]; let faucet = builder.add_existing_network_faucet_with_metadata_info( "EXL", From 1dff20678f8b2d8593c716d679e426d9de6121c0 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 9 Mar 2026 15:09:53 -0300 Subject: [PATCH 38/56] fix: update documentation for metadata_slot and description commitment tests --- .../src/account/faucets/basic_fungible.rs | 12 +-- .../src/account/faucets/network_fungible.rs | 21 +--- .../src/account/faucets/token_metadata.rs | 21 +++- .../src/account/metadata/mod.rs | 5 +- .../src/mock_chain/chain_builder.rs | 6 +- crates/miden-testing/tests/metadata.rs | 99 +------------------ 6 files changed, 31 insertions(+), 133 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 6f9a743298..24584085c8 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -347,17 +347,7 @@ pub fn create_basic_fungible_faucet( }, }; - let mut info = TokenMetadataInfo::new().with_name(metadata.name().clone()); - if let Some(d) = metadata.description() { - info = info.with_description(d.clone(), false); - } - if let Some(l) = metadata.logo_uri() { - info = info.with_logo_uri(l.clone(), false); - } - if let Some(e) = metadata.external_link() { - info = info.with_external_link(e.clone(), false); - } - + let info = metadata.to_token_metadata_info(); let faucet = BasicFungibleFaucet::from_metadata(metadata).with_info(info); let account = AccountBuilder::new(init_seed) diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 1dd7dc638c..623a07f027 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -13,7 +13,6 @@ use miden_protocol::account::{ AccountStorage, AccountStorageMode, AccountType, - StorageSlot, StorageSlotName, }; use miden_protocol::asset::TokenSymbol; @@ -281,13 +280,7 @@ impl From for AccountComponent { fn from(network_faucet: NetworkFungibleFaucet) -> Self { let metadata_slot = network_faucet.metadata.into(); - let ownership = Ownable2Step::new(network_faucet.owner_account_id); - let owner_account_id_word: Word = ownership.to_word(); - - let owner_slot = StorageSlot::with_value( - NetworkFungibleFaucet::owner_config_slot().clone(), - owner_account_id_word, - ); + let owner_slot = Ownable2Step::new(network_faucet.owner_account_id).to_storage_slot(); let mut slots = vec![metadata_slot, owner_slot]; if let Some(info) = &network_faucet.info { @@ -355,17 +348,7 @@ pub fn create_network_fungible_faucet( ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let mut info = TokenMetadataInfo::new().with_name(metadata.name().clone()).with_owner(owner); - if let Some(d) = metadata.description() { - info = info.with_description(d.clone(), false); - } - if let Some(l) = metadata.logo_uri() { - info = info.with_logo_uri(l.clone(), false); - } - if let Some(e) = metadata.external_link() { - info = info.with_external_link(e.clone(), false); - } - + let info = metadata.to_token_metadata_info().with_owner(owner); let faucet = NetworkFungibleFaucet::from_metadata(metadata, owner).with_info(info); let account = AccountBuilder::new(init_seed) diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index b2a0be1a13..852fab5e63 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -402,9 +402,8 @@ impl FungibleTokenMetadata { // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`StorageSlotName`] where the token metadata is stored. - /// Returns the storage slot name for token metadata (canonical slot shared with metadata - /// module). + /// Returns the [`StorageSlotName`] where the token metadata is stored (canonical slot shared + /// with the metadata module). pub fn metadata_slot() -> &'static StorageSlotName { metadata::token_metadata_slot() } @@ -449,6 +448,22 @@ impl FungibleTokenMetadata { self.external_link.as_ref() } + /// Builds a [`TokenMetadataInfo`](crate::account::metadata::TokenMetadata) from this + /// metadata, copying the name and any optional description/logo_uri/external_link fields. + pub fn to_token_metadata_info(&self) -> metadata::TokenMetadata { + let mut info = metadata::TokenMetadata::new().with_name(self.name.clone()); + if let Some(d) = self.description() { + info = info.with_description(d.clone(), false); + } + if let Some(l) = self.logo_uri() { + info = info.with_logo_uri(l.clone(), false); + } + if let Some(e) = self.external_link() { + info = info.with_external_link(e.clone(), false); + } + info + } + // MUTATORS // -------------------------------------------------------------------------------------------- diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 342e168480..f8888316e9 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -425,9 +425,7 @@ impl TokenMetadata { (name, description, logo_uri, external_link) } -} -impl TokenMetadata { /// Returns the storage slots for this metadata (without creating an `AccountComponent`). /// /// These slots are meant to be included directly in a faucet component rather than @@ -491,6 +489,9 @@ impl TokenMetadata { } } +/// Converts [`TokenMetadata`] into a standalone [`AccountComponent`] that includes the metadata +/// MASM library (`metadata_info_component_library`). Use this when adding metadata as a separate +/// component alongside a faucet that does not embed info via `.with_info()`. impl From for AccountComponent { fn from(info: TokenMetadata) -> Self { let metadata = diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 5cc9711d40..06066e962d 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -447,10 +447,8 @@ impl MockChainBuilder { logo_uri: Option<([Word; 7], bool)>, external_link: Option<([Word; 7], bool)>, ) -> anyhow::Result { - let max_supply = Felt::try_from(max_supply) - .map_err(|err| anyhow::anyhow!("failed to convert max_supply to felt: {err}"))?; - let token_supply = Felt::try_from(token_supply.unwrap_or(0)) - .map_err(|err| anyhow::anyhow!("failed to convert token_supply to felt: {err}"))?; + let max_supply = Felt::try_from(max_supply)?; + let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; let name = TokenName::new(token_symbol) .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); let token_symbol = diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index be3f9519e8..2db717cd70 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -145,7 +145,7 @@ async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { Ok(()) } -/// Tests that get_description_commitment returns the RPO256 hash of the 6 description words. +/// Tests that get_description_commitment returns the RPO256 hash of the 7 description words. #[tokio::test] async fn metadata_info_get_description_from_masm() -> anyhow::Result<()> { let desc_text = "some test description text"; @@ -432,8 +432,6 @@ fn network_faucet_initialized_with_max_name_and_full_description() { None, None, ) - .unwrap() - .with_token_supply(Felt::new(0)) .unwrap(); let extension = Info::new() @@ -491,8 +489,6 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( None, None, ) - .unwrap() - .with_token_supply(Felt::new(0)) .unwrap(); let extension = Info::new() @@ -776,90 +772,6 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { Ok(()) } -/// Isolated test: only get_name. -#[tokio::test] -async fn metadata_get_name_only() -> anyhow::Result<()> { - let token_name = TokenName::new("test name").unwrap(); - let name = token_name.to_words(); - let extension = Info::new().with_name(token_name); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_name - push.{expected_name_0} - assert_eqw.err="name chunk 0 does not match" - push.{expected_name_1} - assert_eqw.err="name chunk 1 does not match" - end - "#, - expected_name_0 = name[0], - expected_name_1 = name[1], - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: get_description_commitment returns the RPO256 hash of the 6 description words. -#[tokio::test] -async fn metadata_get_description_only() -> anyhow::Result<()> { - let desc_text = "some test description text"; - let description_typed = Description::new(desc_text).unwrap(); - let description = description_typed.to_words(); - let extension = Info::new().with_description(description_typed, false); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); - let expected_commitment = Hasher::hash_elements(&desc_felts); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_description_commitment - # => [COMMITMENT, pad(12)] - push.{expected} - assert_eqw.err="description commitment does not match" - end - "#, - expected = expected_commitment, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - /// Isolated test: get_mutability_config. #[tokio::test] async fn metadata_get_config_only() -> anyhow::Result<()> { @@ -1198,7 +1110,7 @@ fn field_advice_map_value(field: &[Word; 7]) -> Vec { value } -/// When description flag is 1 (immutable), optional_set_description panics. +/// When description mutable flag is 0 (immutable), optional_set_description panics. #[tokio::test] async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -1624,8 +1536,7 @@ async fn optional_set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> // ================================================================================================= /// Tests that all is_*_mutable procedures correctly read the config flags. -/// Each field is tested with flag=2 (mutable, expects 1) and flag=1 (immutable, expects 0). -/// Also tests is_max_supply_mutable with true (expects 1). +/// Each field is tested with flag=1 (mutable) and flag=0 (immutable). #[tokio::test] async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { let desc = Description::new("test").unwrap(); @@ -1680,7 +1591,7 @@ async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { // get_logo_uri_commitment: commitment test // ================================================================================================= -/// Tests that get_logo_uri_commitment returns the RPO256 hash of the 6 logo URI words. +/// Tests that get_logo_uri_commitment returns the RPO256 hash of the 7 logo URI words. #[tokio::test] async fn metadata_get_logo_uri_commitment() -> anyhow::Result<()> { let logo_text = "https://example.com/logo.png"; @@ -1728,7 +1639,7 @@ async fn metadata_get_logo_uri_commitment() -> anyhow::Result<()> { // get_external_link_commitment: commitment test // ================================================================================================= -/// Tests that get_external_link_commitment returns the RPO256 hash of the 6 external link words. +/// Tests that get_external_link_commitment returns the RPO256 hash of the 7 external link words. #[tokio::test] async fn metadata_get_external_link_commitment() -> anyhow::Result<()> { let link_text = "https://example.com"; From b7a6146ecf1dc2eef919edf98dd67cf265490a01 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 12:23:02 -0300 Subject: [PATCH 39/56] Update crates/miden-standards/asm/standards/access/ownable.masm Co-authored-by: Philipp Gackstatter --- crates/miden-standards/asm/standards/access/ownable.masm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/miden-standards/asm/standards/access/ownable.masm b/crates/miden-standards/asm/standards/access/ownable.masm index bf9e2bef72..1420c920b4 100644 --- a/crates/miden-standards/asm/standards/access/ownable.masm +++ b/crates/miden-standards/asm/standards/access/ownable.masm @@ -89,9 +89,9 @@ end #! Invocation: call pub proc get_owner exec.owner - # => [owner_suffix, owner_prefix, pad(16)] depth=18 - # get_item returns 4 elements from 2 inputs (net +2); owner drops 2, leaving +2 excess. - # Remove 2 excess pad elements to restore call-convention depth of 16. + # => [owner_suffix, owner_prefix, pad(16)] + + # truncate stack to 16 movup.2 drop movup.2 drop # => [owner_suffix, owner_prefix, pad(14)] end From c891bf9db929188ecf8a5bbbb768839d518f99f5 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 13:30:55 -0300 Subject: [PATCH 40/56] refactor: improve comments and assertions in distribute function for minting constraints --- .../asm/standards/faucets/mod.masm | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 8d5bd9b8f1..a6894c3f13 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -12,9 +12,6 @@ use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT const ASSET_PTR=0 const PRIVATE_NOTE=2 -# Token metadata slot — duplicated here so distribute can read it via exec without -# going through the call-safe get_max_supply wrapper. -const TOKEN_METADATA_SLOT = word("miden::standards::metadata::token_metadata") # ERRORS # ================================================================================================= @@ -72,10 +69,18 @@ pub proc distribute swap movup.2 drop movup.2 drop # => [max_supply, token_supply, amount, tag, note_type, RECIPIENT] - # Assert that minting does not violate any supply constraints. - # - assert token_supply <= max_supply - # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT - # - assert amount <= max_mint_amount + # To make sure we cannot mint more than intended, we need to check: + # 1) (max_supply - token_supply) <= max_supply, i.e. the subtraction does not wrap around + # 2) amount + token_supply does not exceed max_supply + # 3) amount + token_supply is less than FUNGIBLE_ASSET_MAX_AMOUNT + # + # This is done with the following concrete assertions: + # - assert token_supply <= max_supply which ensures 1) + # - assert max_supply <= FUNGIBLE_ASSET_MAX_AMOUNT to help ensure 3) + # - assert amount <= max_mint_amount to ensure 2) as well as 3) + # - this ensures 3) because token_supply + max_mint_amount at most ends up being equal to + # max_supply and we already asserted that max_supply does not exceed + # FUNGIBLE_ASSET_MAX_AMOUNT # --------------------------------------------------------------------------------------------- dup.1 dup.1 From 1b860985904f8a8c046b38609cfa32af4bdfdaac Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 13:42:47 -0300 Subject: [PATCH 41/56] Refactor fungible metadata procedures: rename optional setters to standard setters and remove unused commitment functions --- .../faucets/basic_fungible_faucet.masm | 6 - .../faucets/network_fungible_faucet.masm | 14 +- .../asm/standards/metadata/fungible.masm | 360 +----------------- .../src/account/components/mod.rs | 4 +- .../src/account/metadata/mod.rs | 12 +- crates/miden-testing/tests/metadata.rs | 286 ++------------ 6 files changed, 61 insertions(+), 621 deletions(-) diff --git a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index adab99b10a..f6fa952f3c 100644 --- a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -12,12 +12,6 @@ pub use ::miden::standards::metadata::fungible::is_max_supply_mutable pub use ::miden::standards::metadata::fungible::is_description_mutable pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable pub use ::miden::standards::metadata::fungible::is_external_link_mutable -pub use ::miden::standards::metadata::fungible::get_description_commitment -pub use ::miden::standards::metadata::fungible::get_description -pub use ::miden::standards::metadata::fungible::get_logo_uri_commitment -pub use ::miden::standards::metadata::fungible::get_logo_uri -pub use ::miden::standards::metadata::fungible::get_external_link_commitment -pub use ::miden::standards::metadata::fungible::get_external_link pub use ::miden::standards::metadata::fungible::get_token_metadata pub use ::miden::standards::metadata::fungible::get_max_supply pub use ::miden::standards::metadata::fungible::get_decimals diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index ef9f83a1d6..b3e4c886b4 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -12,18 +12,12 @@ pub use ::miden::standards::metadata::fungible::is_max_supply_mutable pub use ::miden::standards::metadata::fungible::is_description_mutable pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable pub use ::miden::standards::metadata::fungible::is_external_link_mutable -pub use ::miden::standards::metadata::fungible::get_description_commitment -pub use ::miden::standards::metadata::fungible::get_description -pub use ::miden::standards::metadata::fungible::get_logo_uri_commitment -pub use ::miden::standards::metadata::fungible::get_logo_uri -pub use ::miden::standards::metadata::fungible::get_external_link_commitment -pub use ::miden::standards::metadata::fungible::get_external_link pub use ::miden::standards::metadata::fungible::get_token_metadata pub use ::miden::standards::metadata::fungible::get_max_supply pub use ::miden::standards::metadata::fungible::get_decimals pub use ::miden::standards::metadata::fungible::get_token_symbol pub use ::miden::standards::metadata::fungible::get_token_supply -pub use ::miden::standards::metadata::fungible::optional_set_description -pub use ::miden::standards::metadata::fungible::optional_set_logo_uri -pub use ::miden::standards::metadata::fungible::optional_set_external_link -pub use ::miden::standards::metadata::fungible::optional_set_max_supply +pub use ::miden::standards::metadata::fungible::set_description +pub use ::miden::standards::metadata::fungible::set_logo_uri +pub use ::miden::standards::metadata::fungible::set_external_link +pub use ::miden::standards::metadata::fungible::set_max_supply diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index 95d0bc73a0..2d865545cf 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -7,8 +7,6 @@ use miden::protocol::active_account use miden::protocol::native_account use miden::standards::access::ownable -use miden::core::crypto::hashes::poseidon2 -use miden::core::mem # ================================================================================================= # CONSTANTS — slot names @@ -95,95 +93,6 @@ proc get_mutability_config_word exec.active_account::get_item end -# --- Description chunk loaders --- -proc get_description_chunk_0 - push.DESCRIPTION_0_SLOT[0..2] - exec.active_account::get_item -end -proc get_description_chunk_1 - push.DESCRIPTION_1_SLOT[0..2] - exec.active_account::get_item -end -proc get_description_chunk_2 - push.DESCRIPTION_2_SLOT[0..2] - exec.active_account::get_item -end -proc get_description_chunk_3 - push.DESCRIPTION_3_SLOT[0..2] - exec.active_account::get_item -end -proc get_description_chunk_4 - push.DESCRIPTION_4_SLOT[0..2] - exec.active_account::get_item -end -proc get_description_chunk_5 - push.DESCRIPTION_5_SLOT[0..2] - exec.active_account::get_item -end -proc get_description_chunk_6 - push.DESCRIPTION_6_SLOT[0..2] - exec.active_account::get_item -end - -# --- Logo URI chunk loaders --- -proc get_logo_uri_chunk_0 - push.LOGO_URI_0_SLOT[0..2] - exec.active_account::get_item -end -proc get_logo_uri_chunk_1 - push.LOGO_URI_1_SLOT[0..2] - exec.active_account::get_item -end -proc get_logo_uri_chunk_2 - push.LOGO_URI_2_SLOT[0..2] - exec.active_account::get_item -end -proc get_logo_uri_chunk_3 - push.LOGO_URI_3_SLOT[0..2] - exec.active_account::get_item -end -proc get_logo_uri_chunk_4 - push.LOGO_URI_4_SLOT[0..2] - exec.active_account::get_item -end -proc get_logo_uri_chunk_5 - push.LOGO_URI_5_SLOT[0..2] - exec.active_account::get_item -end -proc get_logo_uri_chunk_6 - push.LOGO_URI_6_SLOT[0..2] - exec.active_account::get_item -end - -# --- External link chunk loaders --- -proc get_external_link_chunk_0 - push.EXTERNAL_LINK_0_SLOT[0..2] - exec.active_account::get_item -end -proc get_external_link_chunk_1 - push.EXTERNAL_LINK_1_SLOT[0..2] - exec.active_account::get_item -end -proc get_external_link_chunk_2 - push.EXTERNAL_LINK_2_SLOT[0..2] - exec.active_account::get_item -end -proc get_external_link_chunk_3 - push.EXTERNAL_LINK_3_SLOT[0..2] - exec.active_account::get_item -end -proc get_external_link_chunk_4 - push.EXTERNAL_LINK_4_SLOT[0..2] - exec.active_account::get_item -end -proc get_external_link_chunk_5 - push.EXTERNAL_LINK_5_SLOT[0..2] - exec.active_account::get_item -end -proc get_external_link_chunk_6 - push.EXTERNAL_LINK_6_SLOT[0..2] - exec.active_account::get_item -end #! #! Invocation: call pub proc get_token_metadata @@ -350,260 +259,7 @@ end pub use ownable::get_owner # ================================================================================================= -# DESCRIPTION (7 words) — commitment/unhashing pattern -# ================================================================================================= - -#! Returns an RPO256 commitment to the description and inserts the 7 words into the advice map. -#! -#! The commitment is computed over the 28 felts of the description. The preimage (7 words) -#! is placed in the advice map keyed by the commitment so that a subsequent -#! `get_description` call can retrieve and verify it. -#! -#! Inputs: [pad(16)] -#! Outputs: [COMMITMENT, pad(12)] -#! -#! Invocation: call -@locals(28) -pub proc get_description_commitment - exec.get_description_chunk_0 - loc_storew_le.FIELD_0_LOC dropw - exec.get_description_chunk_1 - loc_storew_le.FIELD_1_LOC dropw - exec.get_description_chunk_2 - loc_storew_le.FIELD_2_LOC dropw - exec.get_description_chunk_3 - loc_storew_le.FIELD_3_LOC dropw - exec.get_description_chunk_4 - loc_storew_le.FIELD_4_LOC dropw - exec.get_description_chunk_5 - loc_storew_le.FIELD_5_LOC dropw - exec.get_description_chunk_6 - loc_storew_le.FIELD_6_LOC dropw - # => [pad(16)] - - # Hash the 7 words (28 felts) in local memory via RPO256. - # hash_elements expects [ptr, num_felts]. - push.28 locaddr.FIELD_0_LOC - exec.poseidon2::hash_elements - # => [COMMITMENT, pad(16)] depth=20 - - # Insert the 7-word preimage into the advice map keyed by COMMITMENT. - # adv.insert_mem expects [COMMITMENT, start_addr, end_addr] on stack. - locaddr.FIELD_0_LOC dup add.28 - movdn.5 movdn.4 - adv.insert_mem - movup.4 drop movup.4 drop - # => [COMMITMENT, pad(16)] depth=20 - - swapw dropw - # => [COMMITMENT, pad(12)] depth=16 -end - -#! Writes the description (7 Words) to memory at the given pointer. -#! -#! Loads the description from storage, computes its RPO256 commitment, inserts the preimage -#! into the advice map, then pipes it into destination memory and verifies the commitment. -#! -#! Inputs: [dest_ptr, pad(15)] -#! Outputs: [dest_ptr, pad(15)] -#! -#! Invocation: exec -@locals(28) -pub proc get_description - exec.get_description_chunk_0 - loc_storew_le.FIELD_0_LOC dropw - exec.get_description_chunk_1 - loc_storew_le.FIELD_1_LOC dropw - exec.get_description_chunk_2 - loc_storew_le.FIELD_2_LOC dropw - exec.get_description_chunk_3 - loc_storew_le.FIELD_3_LOC dropw - exec.get_description_chunk_4 - loc_storew_le.FIELD_4_LOC dropw - exec.get_description_chunk_5 - loc_storew_le.FIELD_5_LOC dropw - exec.get_description_chunk_6 - loc_storew_le.FIELD_6_LOC dropw - # => [dest_ptr, pad(15)] depth=16 - - # Hash the 7 words (28 felts) in local memory. - push.28 locaddr.FIELD_0_LOC - # => [start_ptr, 28, dest_ptr, pad(15)] depth=18 - exec.poseidon2::hash_elements - # => [COMMITMENT, dest_ptr, pad(15)] depth=20 - - # Insert preimage into advice map keyed by COMMITMENT. - locaddr.FIELD_0_LOC dup add.28 - # => [end_ptr, start_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 - movdn.5 movdn.4 - # => [COMMITMENT, start_ptr, end_ptr, dest_ptr, pad(15)] - adv.insert_mem - movup.4 drop movup.4 drop - # => [COMMITMENT, dest_ptr, pad(15)] depth=20 - - # Load preimage onto the advice stack. - adv.push_mapval - # AS => [[DESCRIPTION_DATA]] - - # Set up pipe_preimage_to_memory: [num_words, dest_ptr, COMMITMENT, ...] - # dest_ptr is at position 4 (right after COMMITMENT) - dup.4 push.7 - # => [7, dest_ptr, COMMITMENT, dest_ptr, pad(15)] depth=22 - - exec.mem::pipe_preimage_to_memory drop - # => [dest_ptr, pad(15)] depth=16 (consumed 6 [6,dest_ptr,COMMITMENT], pushed 1 [updated_ptr], dropped it) -end - -# ================================================================================================= -# LOGO URI (7 words) — commitment/unhashing pattern -# ================================================================================================= - -#! Returns an RPO256 commitment to the logo URI and inserts the 7 words into the advice map. -#! -#! Inputs: [pad(16)] -#! Outputs: [COMMITMENT, pad(12)] -#! -#! Invocation: call -@locals(28) -pub proc get_logo_uri_commitment - exec.get_logo_uri_chunk_0 - loc_storew_le.FIELD_0_LOC dropw - exec.get_logo_uri_chunk_1 - loc_storew_le.FIELD_1_LOC dropw - exec.get_logo_uri_chunk_2 - loc_storew_le.FIELD_2_LOC dropw - exec.get_logo_uri_chunk_3 - loc_storew_le.FIELD_3_LOC dropw - exec.get_logo_uri_chunk_4 - loc_storew_le.FIELD_4_LOC dropw - exec.get_logo_uri_chunk_5 - loc_storew_le.FIELD_5_LOC dropw - exec.get_logo_uri_chunk_6 - loc_storew_le.FIELD_6_LOC dropw - - push.28 locaddr.FIELD_0_LOC - exec.poseidon2::hash_elements - - locaddr.FIELD_0_LOC dup add.28 - movdn.5 movdn.4 - adv.insert_mem - movup.4 drop movup.4 drop - - swapw dropw -end - -#! Writes the logo URI (7 Words) to memory at the given pointer. -#! -#! Inputs: [dest_ptr, pad(15)] -#! Outputs: [dest_ptr, pad(15)] -#! -#! Invocation: exec -@locals(28) -pub proc get_logo_uri - exec.get_logo_uri_chunk_0 - loc_storew_le.FIELD_0_LOC dropw - exec.get_logo_uri_chunk_1 - loc_storew_le.FIELD_1_LOC dropw - exec.get_logo_uri_chunk_2 - loc_storew_le.FIELD_2_LOC dropw - exec.get_logo_uri_chunk_3 - loc_storew_le.FIELD_3_LOC dropw - exec.get_logo_uri_chunk_4 - loc_storew_le.FIELD_4_LOC dropw - exec.get_logo_uri_chunk_5 - loc_storew_le.FIELD_5_LOC dropw - exec.get_logo_uri_chunk_6 - loc_storew_le.FIELD_6_LOC dropw - - push.28 locaddr.FIELD_0_LOC - exec.poseidon2::hash_elements - - locaddr.FIELD_0_LOC dup add.28 - movdn.5 movdn.4 - adv.insert_mem - movup.4 drop movup.4 drop - - adv.push_mapval - dup.4 push.7 - exec.mem::pipe_preimage_to_memory drop -end - -# ================================================================================================= -# EXTERNAL LINK (7 words) — commitment/unhashing pattern -# ================================================================================================= - -#! Returns an RPO256 commitment to the external link and inserts the 7 words into the advice map. -#! -#! Inputs: [pad(16)] -#! Outputs: [COMMITMENT, pad(12)] -#! -#! Invocation: call -@locals(28) -pub proc get_external_link_commitment - exec.get_external_link_chunk_0 - loc_storew_le.FIELD_0_LOC dropw - exec.get_external_link_chunk_1 - loc_storew_le.FIELD_1_LOC dropw - exec.get_external_link_chunk_2 - loc_storew_le.FIELD_2_LOC dropw - exec.get_external_link_chunk_3 - loc_storew_le.FIELD_3_LOC dropw - exec.get_external_link_chunk_4 - loc_storew_le.FIELD_4_LOC dropw - exec.get_external_link_chunk_5 - loc_storew_le.FIELD_5_LOC dropw - exec.get_external_link_chunk_6 - loc_storew_le.FIELD_6_LOC dropw - - push.28 locaddr.FIELD_0_LOC - exec.poseidon2::hash_elements - - locaddr.FIELD_0_LOC dup add.28 - movdn.5 movdn.4 - adv.insert_mem - movup.4 drop movup.4 drop - - swapw dropw -end - -#! Writes the external link (7 Words) to memory at the given pointer. -#! -#! Inputs: [dest_ptr, pad(15)] -#! Outputs: [dest_ptr, pad(15)] -#! -#! Invocation: exec -@locals(28) -pub proc get_external_link - exec.get_external_link_chunk_0 - loc_storew_le.FIELD_0_LOC dropw - exec.get_external_link_chunk_1 - loc_storew_le.FIELD_1_LOC dropw - exec.get_external_link_chunk_2 - loc_storew_le.FIELD_2_LOC dropw - exec.get_external_link_chunk_3 - loc_storew_le.FIELD_3_LOC dropw - exec.get_external_link_chunk_4 - loc_storew_le.FIELD_4_LOC dropw - exec.get_external_link_chunk_5 - loc_storew_le.FIELD_5_LOC dropw - exec.get_external_link_chunk_6 - loc_storew_le.FIELD_6_LOC dropw - - push.28 locaddr.FIELD_0_LOC - exec.poseidon2::hash_elements - - locaddr.FIELD_0_LOC dup add.28 - movdn.5 movdn.4 - adv.insert_mem - movup.4 drop movup.4 drop - - adv.push_mapval - dup.4 push.7 - exec.mem::pipe_preimage_to_memory drop -end - -# ================================================================================================= -# OPTIONAL SET DESCRIPTION (owner-only when desc_mutable == 1) +# SET DESCRIPTION (owner-only when desc_mutable == 1) # ================================================================================================= #! Updates the description (7 Words) if the description mutability flag is 1 @@ -621,7 +277,7 @@ end #! - the note sender is not the owner. #! #! Invocation: call (from note script context) -pub proc optional_set_description +pub proc set_description # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] exec.get_mutability_config_word swapw dropw @@ -673,7 +329,7 @@ pub proc optional_set_description end # ================================================================================================= -# OPTIONAL SET LOGO URI (owner-only when logo_mutable == 1) +# SET LOGO URI (owner-only when logo_mutable == 1) # ================================================================================================= #! Updates the logo URI (7 Words) if the logo URI mutability flag is 1 @@ -691,7 +347,7 @@ end #! - the note sender is not the owner. #! #! Invocation: call (from note script context) -pub proc optional_set_logo_uri +pub proc set_logo_uri # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] exec.get_mutability_config_word swapw dropw @@ -739,7 +395,7 @@ pub proc optional_set_logo_uri end # ================================================================================================= -# OPTIONAL SET EXTERNAL LINK (owner-only when extlink_mutable == 1) +# SET EXTERNAL LINK (owner-only when extlink_mutable == 1) # ================================================================================================= #! Updates the external link (7 Words) if the external link mutability flag is 1 @@ -757,7 +413,7 @@ end #! - the note sender is not the owner. #! #! Invocation: call (from note script context) -pub proc optional_set_external_link +pub proc set_external_link # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] exec.get_mutability_config_word swapw dropw @@ -805,7 +461,7 @@ pub proc optional_set_external_link end # ================================================================================================= -# OPTIONAL SET MAX SUPPLY (owner-only when max_supply_mutable == 1) +# SET MAX SUPPLY (owner-only when max_supply_mutable == 1) # ================================================================================================= #! Updates the max supply if the max supply mutability flag is 1 @@ -820,7 +476,7 @@ end #! - the new max supply is less than the current token supply. #! #! Invocation: call (from note script context) -pub proc optional_set_max_supply +pub proc set_max_supply # 1. Check max supply mutability exec.get_mutability_config_word # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, new_max_supply, pad(15)] diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 902089053f..692bb4a3b2 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -100,7 +100,7 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { // METADATA LIBRARIES // ================================================================================================ -// Metadata Info component uses the standards library (get_name, get_description, etc. from +// Metadata Info component uses the standards library (get_name, set_description, etc. from // metadata). static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = LazyLock::new(|| Library::from(crate::StandardsLib::default())); @@ -137,7 +137,7 @@ pub fn network_fungible_faucet_library() -> Library { /// Returns the Metadata Info component library. /// /// Uses the standards library; the standalone [`Info`](crate::account::metadata::TokenMetadata) -/// component exposes get_name, get_description, get_logo_uri, get_external_link from +/// component exposes get_name, set_description, set_logo_uri, set_external_link from /// `miden::standards::metadata::fungible`. pub fn metadata_info_component_library() -> Library { METADATA_INFO_COMPONENT_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index f8888316e9..99c5a1683a 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -38,9 +38,9 @@ //! //! All metadata procedures (getters, `get_owner`, setters) live in //! `miden::standards::metadata::fungible`, which depends on ownable. The standalone -//! The TokenMetadata component uses the standards library and exposes `get_name`, -//! `get_description`, `get_logo_uri`, `get_external_link`; for owner and mutable fields use a -//! component that re-exports from fungible (e.g. network fungible faucet). +//! The TokenMetadata component uses the standards library and exposes `get_name`; for owner +//! and mutable fields use a component that re-exports from fungible (e.g. network fungible +//! faucet). //! //! ## String encoding (UTF-8) //! @@ -310,14 +310,14 @@ impl TokenMetadata { /// /// The owner is stored in the `ownable::owner_config` slot and is used by the /// `metadata::fungible` MASM procedures to authorize mutations (e.g. - /// `optional_set_description`). + /// `set_description`). pub fn with_owner(mut self, owner: AccountId) -> Self { self.owner = Some(owner); self } /// Sets whether the max supply can be updated by the owner via - /// `optional_set_max_supply`. If `false` (default), the max supply is immutable. + /// `set_max_supply`. If `false` (default), the max supply is immutable. pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { self.max_supply_mutable = mutable; self @@ -434,7 +434,7 @@ impl TokenMetadata { let mut slots: Vec = Vec::new(); // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures - // for get_owner and verify_owner (used in optional_set_* mutations). + // for get_owner and verify_owner (used in set_* mutations). // Word layout: [0, 0, owner_suffix, owner_prefix] so that after get_item (which places // word[0] on top), dropping the two leading zeros yields [owner_suffix, owner_prefix]. // Only included when an owner is explicitly set, to avoid conflicting with components diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 37754ad03c..1a3c17215c 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -146,51 +146,6 @@ async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { Ok(()) } -/// Tests that get_description_commitment returns the RPO256 hash of the 7 description words. -#[tokio::test] -async fn metadata_info_get_description_from_masm() -> anyhow::Result<()> { - let desc_text = "some test description text"; - let description_typed = Description::new(desc_text).unwrap(); - let description = description_typed.to_words(); - - let extension = Info::new().with_description(description_typed, false); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); - let expected_commitment = Hasher::hash_elements(&desc_felts); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_description_commitment - # => [COMMITMENT, pad(12)] - - push.{expected} - assert_eqw.err="description commitment does not match" - end - "#, - expected = expected_commitment, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - /// Tests that the metadata extension works alongside a fungible faucet. #[test] fn metadata_info_with_faucet_storage() { @@ -478,7 +433,6 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); let desc_text = "network faucet description"; let description_typed = Description::new(desc_text).unwrap(); - let description = description_typed.to_words(); let network_faucet = NetworkFungibleFaucet::new( "MAS".try_into().unwrap(), @@ -503,9 +457,6 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( .with_component(Ownable2Step::new(owner_account_id)) .build()?; - let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); - let expected_desc_commitment = Hasher::hash_elements(&desc_felts); - let tx_script = format!( r#" begin @@ -514,16 +465,10 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( assert_eqw.err="network faucet name chunk 0 does not match" push.{expected_name_1} assert_eqw.err="network faucet name chunk 1 does not match" - - call.::miden::standards::metadata::fungible::get_description_commitment - # => [COMMITMENT, pad(12)] - push.{expected_desc_commitment} - assert_eqw.err="network faucet description commitment does not match" end "#, expected_name_0 = name_words[0], expected_name_1 = name_words[1], - expected_desc_commitment = expected_desc_commitment, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -1035,7 +980,6 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { let name = token_name.to_words(); let desc_text = "readable description"; let description_typed = Description::new(desc_text).unwrap(); - let description = description_typed.to_words(); let faucet = BasicFungibleFaucet::new( "MAS".try_into().unwrap(), @@ -1056,10 +1000,7 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { .with_component(faucet.with_info(extension)) .build()?; - let desc_felts: Vec = description.iter().flat_map(|w| w.iter().copied()).collect(); - let expected_desc_commitment = Hasher::hash_elements(&desc_felts); - - // MASM script to read name and description commitment via the metadata procedures and verify + // MASM script to read name via the metadata procedures and verify let tx_script = format!( r#" begin @@ -1070,17 +1011,10 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { assert_eqw.err="faucet name chunk 0 does not match" push.{expected_name_1} assert_eqw.err="faucet name chunk 1 does not match" - - # Get description commitment and verify - call.::miden::standards::metadata::fungible::get_description_commitment - # => [COMMITMENT, pad(12)] - push.{expected_desc_commitment} - assert_eqw.err="faucet description commitment does not match" end "#, expected_name_0 = name[0], expected_name_1 = name[1], - expected_desc_commitment = expected_desc_commitment, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -1098,7 +1032,7 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { } // ================================================================================================= -// optional_set_description: mutable flag and verify_owner +// set_description: mutable flag and verify_owner // ================================================================================================= /// Builds the advice map value for field setters. @@ -1110,9 +1044,9 @@ fn field_advice_map_value(field: &[Word; 7]) -> Vec { value } -/// When description mutable flag is 0 (immutable), optional_set_description panics. +/// When description mutable flag is 0 (immutable), set_description panics. #[tokio::test] -async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { +async fn set_description_immutable_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( [1; 15], @@ -1153,7 +1087,7 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { let tx_script = r#" begin - call.::miden::standards::metadata::fungible::optional_set_description + call.::miden::standards::metadata::fungible::set_description end "#; @@ -1174,10 +1108,10 @@ async fn optional_set_description_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When description mutable flag is 1 and note sender is the owner, optional_set_description +/// When description mutable flag is 1 and note sender is the owner, set_description /// succeeds. #[tokio::test] -async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> { +async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -1224,7 +1158,7 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> let set_desc_note_script_code = r#" begin - call.::miden::standards::metadata::fungible::optional_set_description + call.::miden::standards::metadata::fungible::set_description end "#; @@ -1259,10 +1193,10 @@ async fn optional_set_description_mutable_owner_succeeds() -> anyhow::Result<()> Ok(()) } -/// When description mutable flag is 1 but note sender is not the owner, optional_set_description +/// When description mutable flag is 1 but note sender is not the owner, set_description /// panics. #[tokio::test] -async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<()> { +async fn set_description_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -1311,7 +1245,7 @@ async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<() let set_desc_note_script_code = r#" begin - call.::miden::standards::metadata::fungible::optional_set_description + call.::miden::standards::metadata::fungible::set_description end "#; @@ -1341,12 +1275,12 @@ async fn optional_set_description_mutable_non_owner_fails() -> anyhow::Result<() } // ================================================================================================= -// optional_set_max_supply: mutable flag and verify_owner +// set_max_supply: mutable flag and verify_owner // ================================================================================================= -/// When max_supply_mutable is 0 (immutable), optional_set_max_supply panics. +/// When max_supply_mutable is 0 (immutable), set_max_supply panics. #[tokio::test] -async fn optional_set_max_supply_immutable_fails() -> anyhow::Result<()> { +async fn set_max_supply_immutable_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( [1; 15], @@ -1372,7 +1306,7 @@ async fn optional_set_max_supply_immutable_fails() -> anyhow::Result<()> { r#" begin push.{new_max_supply} - call.::miden::standards::metadata::fungible::optional_set_max_supply + call.::miden::standards::metadata::fungible::set_max_supply end "# ); @@ -1393,9 +1327,9 @@ async fn optional_set_max_supply_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When max_supply_mutable is 1 and note sender is the owner, optional_set_max_supply succeeds. +/// When max_supply_mutable is 1 and note sender is the owner, set_max_supply succeeds. #[tokio::test] -async fn optional_set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { +async fn set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { use miden_standards::account::faucets::NetworkFungibleFaucet; let mut builder = MockChain::builder(); @@ -1426,7 +1360,7 @@ async fn optional_set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> begin push.{new_max_supply} swap drop - call.::miden::standards::metadata::fungible::optional_set_max_supply + call.::miden::standards::metadata::fungible::set_max_supply end "# ); @@ -1466,9 +1400,9 @@ async fn optional_set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> Ok(()) } -/// When max_supply_mutable is 1 but note sender is not the owner, optional_set_max_supply panics. +/// When max_supply_mutable is 1 but note sender is not the owner, set_max_supply panics. #[tokio::test] -async fn optional_set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { +async fn set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -1502,7 +1436,7 @@ async fn optional_set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> begin push.{new_max_supply} swap drop - call.::miden::standards::metadata::fungible::optional_set_max_supply + call.::miden::standards::metadata::fungible::set_max_supply end "# ); @@ -1588,150 +1522,12 @@ async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { } // ================================================================================================= -// get_logo_uri_commitment: commitment test -// ================================================================================================= - -/// Tests that get_logo_uri_commitment returns the RPO256 hash of the 7 logo URI words. -#[tokio::test] -async fn metadata_get_logo_uri_commitment() -> anyhow::Result<()> { - let logo_text = "https://example.com/logo.png"; - let logo_typed = LogoURI::new(logo_text).unwrap(); - let logo_words = logo_typed.to_words(); - - let extension = Info::new().with_logo_uri(logo_typed, false); - - let account = AccountBuilder::new([10u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let logo_felts: Vec = logo_words.iter().flat_map(|w| w.iter().copied()).collect(); - let expected_commitment = Hasher::hash_elements(&logo_felts); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_logo_uri_commitment - # => [COMMITMENT, pad(12)] - push.{expected} - assert_eqw.err="logo URI commitment does not match" - end - "#, - expected = expected_commitment, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -// ================================================================================================= -// get_external_link_commitment: commitment test -// ================================================================================================= - -/// Tests that get_external_link_commitment returns the RPO256 hash of the 7 external link words. -#[tokio::test] -async fn metadata_get_external_link_commitment() -> anyhow::Result<()> { - let link_text = "https://example.com"; - let link_typed = ExternalLink::new(link_text).unwrap(); - let link_words = link_typed.to_words(); - - let extension = Info::new().with_external_link(link_typed, false); - - let account = AccountBuilder::new([11u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let link_felts: Vec = link_words.iter().flat_map(|w| w.iter().copied()).collect(); - let expected_commitment = Hasher::hash_elements(&link_felts); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_external_link_commitment - # => [COMMITMENT, pad(12)] - push.{expected} - assert_eqw.err="external link commitment does not match" - end - "#, - expected = expected_commitment, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Tests that commitment of an empty (all-zero) description is deterministic. -#[tokio::test] -async fn metadata_get_description_commitment_zero_field() -> anyhow::Result<()> { - // No description set — all storage slots will be zero words - let extension = Info::new(); - - let account = AccountBuilder::new([12u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - // Expected: RPO256 hash of 28 zero felts (7 Words) - let zero_felts = vec![Felt::new(0); 28]; - let expected_commitment = Hasher::hash_elements(&zero_felts); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_description_commitment - # => [COMMITMENT, pad(12)] - push.{expected} - assert_eqw.err="zero description commitment does not match" - end - "#, - expected = expected_commitment, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -// ================================================================================================= -// optional_set_logo_uri: mutable flag and verify_owner +// set_logo_uri: mutable flag and verify_owner // ================================================================================================= -/// When logo URI flag is 0 (immutable), optional_set_logo_uri panics. +/// When logo URI flag is 0 (immutable), set_logo_uri panics. #[tokio::test] -async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { +async fn set_logo_uri_immutable_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( [1; 15], @@ -1772,7 +1568,7 @@ async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { let tx_script = r#" begin - call.::miden::standards::metadata::fungible::optional_set_logo_uri + call.::miden::standards::metadata::fungible::set_logo_uri end "#; @@ -1793,9 +1589,9 @@ async fn optional_set_logo_uri_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When logo URI mutable flag is 1 and note sender is the owner, optional_set_logo_uri succeeds. +/// When logo URI mutable flag is 1 and note sender is the owner, set_logo_uri succeeds. #[tokio::test] -async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { +async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -1839,7 +1635,7 @@ async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { let set_logo_note_script_code = r#" begin - call.::miden::standards::metadata::fungible::optional_set_logo_uri + call.::miden::standards::metadata::fungible::set_logo_uri end "#; @@ -1874,9 +1670,9 @@ async fn optional_set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { Ok(()) } -/// When logo URI mutable flag is 1 but note sender is not the owner, optional_set_logo_uri panics. +/// When logo URI mutable flag is 1 but note sender is not the owner, set_logo_uri panics. #[tokio::test] -async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { +async fn set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -1925,7 +1721,7 @@ async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { let set_logo_note_script_code = r#" begin - call.::miden::standards::metadata::fungible::optional_set_logo_uri + call.::miden::standards::metadata::fungible::set_logo_uri end "#; @@ -1955,12 +1751,12 @@ async fn optional_set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { } // ================================================================================================= -// optional_set_external_link: mutable flag and verify_owner +// set_external_link: mutable flag and verify_owner // ================================================================================================= -/// When external link flag is 0 (immutable), optional_set_external_link panics. +/// When external link flag is 0 (immutable), set_external_link panics. #[tokio::test] -async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { +async fn set_external_link_immutable_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( [1; 15], @@ -2001,7 +1797,7 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { let tx_script = r#" begin - call.::miden::standards::metadata::fungible::optional_set_external_link + call.::miden::standards::metadata::fungible::set_external_link end "#; @@ -2022,10 +1818,10 @@ async fn optional_set_external_link_immutable_fails() -> anyhow::Result<()> { Ok(()) } -/// When external link mutable flag is 1 and note sender is the owner, optional_set_external_link +/// When external link mutable flag is 1 and note sender is the owner, set_external_link /// succeeds. #[tokio::test] -async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { +async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -2068,7 +1864,7 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( let set_link_note_script_code = r#" begin - call.::miden::standards::metadata::fungible::optional_set_external_link + call.::miden::standards::metadata::fungible::set_external_link end "#; @@ -2104,9 +1900,9 @@ async fn optional_set_external_link_mutable_owner_succeeds() -> anyhow::Result<( } /// When external link mutable flag is 1 but note sender is not the owner, -/// optional_set_external_link panics. +/// set_external_link panics. #[tokio::test] -async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { +async fn set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner_account_id = AccountId::dummy( @@ -2155,7 +1951,7 @@ async fn optional_set_external_link_mutable_non_owner_fails() -> anyhow::Result< let set_link_note_script_code = r#" begin - call.::miden::standards::metadata::fungible::optional_set_external_link + call.::miden::standards::metadata::fungible::set_external_link end "#; From 8f7c3f8cdac8399645aa63dbff8775b9b8d0492b Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 14:12:41 -0300 Subject: [PATCH 42/56] refactor: standardize TokenMetadata usage across faucets and tests --- .../src/account/faucets/basic_fungible.rs | 14 +-- .../src/account/faucets/network_fungible.rs | 6 +- .../src/mock_chain/chain_builder.rs | 6 +- crates/miden-testing/tests/metadata.rs | 86 +++++++++---------- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 24584085c8..25b1e5a902 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -32,7 +32,7 @@ use crate::account::components::basic_fungible_faucet_library; /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::TokenMetadata as TokenMetadataInfo; +use crate::account::metadata::TokenMetadata; use crate::procedure_digest; // BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -77,7 +77,7 @@ procedure_digest!( /// [builder]: crate::code_builder::CodeBuilder pub struct BasicFungibleFaucet { metadata: FungibleTokenMetadata, - info: Option, + info: Option, } impl BasicFungibleFaucet { @@ -140,7 +140,7 @@ impl BasicFungibleFaucet { /// Attaches token metadata (name, description, logo, link, mutability flags) to the /// faucet. These storage slots will be included in the component when built. - pub fn with_info(mut self, info: TokenMetadataInfo) -> Self { + pub fn with_info(mut self, info: TokenMetadata) -> Self { self.info = Some(info); self } @@ -385,7 +385,7 @@ mod tests { create_basic_fungible_faucet, }; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; - use crate::account::metadata::TokenMetadata as TokenMetadataInfo; + use crate::account::metadata::TokenMetadata; use crate::account::wallets::BasicWallet; #[test] @@ -463,11 +463,11 @@ mod tests { // Check that Info component has name and description let name_0 = faucet_account .storage() - .get_item(TokenMetadataInfo::name_chunk_0_slot()) + .get_item(TokenMetadata::name_chunk_0_slot()) .unwrap(); let name_1 = faucet_account .storage() - .get_item(TokenMetadataInfo::name_chunk_1_slot()) + .get_item(TokenMetadata::name_chunk_1_slot()) .unwrap(); let decoded_name = TokenName::try_from_words(&[name_0, name_1]).unwrap(); assert_eq!(decoded_name.as_str(), token_name_string); @@ -475,7 +475,7 @@ mod tests { for (i, expected) in expected_desc_words.iter().enumerate() { let chunk = faucet_account .storage() - .get_item(TokenMetadataInfo::description_slot(i)) + .get_item(TokenMetadata::description_slot(i)) .unwrap(); assert_eq!(chunk, *expected); } diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 8e704e9f54..c426c56b89 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -29,7 +29,7 @@ use crate::account::access::AccessControl; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::TokenMetadata as TokenMetadataInfo; +use crate::account::metadata::TokenMetadata; use crate::procedure_digest; /// The schema type for token symbols. @@ -78,7 +78,7 @@ procedure_digest!( /// [builder]: crate::code_builder::CodeBuilder pub struct NetworkFungibleFaucet { metadata: FungibleTokenMetadata, - info: Option, + info: Option, } impl NetworkFungibleFaucet { @@ -138,7 +138,7 @@ impl NetworkFungibleFaucet { /// Attaches token metadata (name, description, logo, link, mutability flags) to the /// faucet. These storage slots will be included in the component when built. - pub fn with_info(mut self, info: TokenMetadataInfo) -> Self { + pub fn with_info(mut self, info: TokenMetadata) -> Self { self.info = Some(info); self } diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 8916eb465c..e1769515a4 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -55,7 +55,7 @@ use miden_standards::account::faucets::{ NetworkFungibleFaucet, TokenName, }; -use miden_standards::account::metadata::TokenMetadata as TokenMetadataInfo; +use miden_standards::account::metadata::TokenMetadata; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote}; use miden_standards::testing::account_component::MockAccountComponent; @@ -408,7 +408,7 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let info = TokenMetadataInfo::new().with_name(name.clone()); + let info = TokenMetadata::new().with_name(name.clone()); let network_faucet = NetworkFungibleFaucet::new( token_symbol, @@ -455,7 +455,7 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let mut info = TokenMetadataInfo::new() + let mut info = TokenMetadata::new() .with_name(name.clone()) .with_owner(owner_account_id) .with_max_supply_mutable(max_supply_mutable); diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs index 1a3c17215c..409852e303 100644 --- a/crates/miden-testing/tests/metadata.rs +++ b/crates/miden-testing/tests/metadata.rs @@ -31,7 +31,7 @@ use miden_standards::account::metadata::{ FieldBytesError, LOGO_URI_DATA_KEY, NAME_UTF8_MAX_BYTES, - TokenMetadata as Info, + TokenMetadata, field_from_bytes, mutability_config_slot, }; @@ -46,7 +46,7 @@ use miden_standards::errors::standards::{ use miden_standards::testing::note::NoteBuilder; use miden_testing::{MockChain, TransactionContextBuilder, assert_transaction_executor_error}; -fn build_faucet_with_info(info: Info) -> BasicFungibleFaucet { +fn build_faucet_with_info(info: TokenMetadata) -> BasicFungibleFaucet { BasicFungibleFaucet::new( "TST".try_into().unwrap(), 2, @@ -66,7 +66,7 @@ async fn metadata_info_get_name_from_masm() -> anyhow::Result<()> { let token_name = TokenName::new("test name").unwrap(); let name = token_name.to_words(); - let extension = Info::new().with_name(token_name); + let extension = TokenMetadata::new().with_name(token_name); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -114,7 +114,7 @@ async fn metadata_info_get_name_from_masm() -> anyhow::Result<()> { /// Tests that reading zero-valued name returns empty words. #[tokio::test] async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { - let extension = Info::new(); + let extension = TokenMetadata::new(); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -158,7 +158,7 @@ fn metadata_info_with_faucet_storage() { let description_typed = Description::new(desc_text).unwrap(); let description = description_typed.to_words(); - let extension = Info::new().with_name(token_name).with_description(description_typed, false); + let extension = TokenMetadata::new().with_name(token_name).with_description(description_typed, false); let faucet = BasicFungibleFaucet::new( "TST".try_into().unwrap(), @@ -187,14 +187,14 @@ fn metadata_info_with_faucet_storage() { assert_eq!(faucet_metadata[2], Felt::new(8)); // decimals // Verify name - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); assert_eq!(name_0, name[0]); assert_eq!(name_1, name[1]); // Verify description for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } } @@ -204,15 +204,15 @@ fn metadata_info_with_faucet_storage() { fn name_32_bytes_accepted() { let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); let token_name = TokenName::new(&max_name).unwrap(); - let extension = Info::new().with_name(token_name); + let extension = TokenMetadata::new().with_name(token_name); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) .with_component(build_faucet_with_info(extension)) .build() .unwrap(); - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); let decoded = miden_standards::account::metadata::name_to_utf8(&[name_0, name_1]).unwrap(); assert_eq!(decoded, max_name); } @@ -235,7 +235,7 @@ fn description_7_words_full_capacity() { let desc_text = "a".repeat(Description::MAX_BYTES); let description_typed = Description::new(&desc_text).unwrap(); let description = description_typed.to_words(); - let extension = Info::new().with_description(description_typed, false); + let extension = TokenMetadata::new().with_description(description_typed, false); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) .with_auth_component(NoAuth) @@ -243,7 +243,7 @@ fn description_7_words_full_capacity() { .build() .unwrap(); for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } } @@ -259,7 +259,7 @@ fn field_over_max_bytes_rejected() { assert!(matches!(result, Err(FieldBytesError::TooLong(n)) if n == over)); } -/// Tests that BasicFungibleFaucet with Info component (name/description) works correctly. +/// Tests that BasicFungibleFaucet with TokenMetadata component (name/description) works correctly. #[test] fn faucet_with_integrated_metadata() { use miden_protocol::Felt; @@ -281,7 +281,7 @@ fn faucet_with_integrated_metadata() { None, ) .unwrap(); - let extension = Info::new().with_name(token_name).with_description(description_typed, false); + let extension = TokenMetadata::new().with_name(token_name).with_description(description_typed, false); let account = AccountBuilder::new([2u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) @@ -298,18 +298,18 @@ fn faucet_with_integrated_metadata() { assert_eq!(faucet_metadata[2], Felt::new(6)); // decimals // Verify name - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); assert_eq!(name_0, name[0]); assert_eq!(name_1, name[1]); // Verify description for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } - // Verify the faucet can be recovered from the account (metadata only; name/desc are in Info) + // Verify the faucet can be recovered from the account (metadata only; name/desc are in TokenMetadata) let recovered_faucet = BasicFungibleFaucet::try_from(&account).unwrap(); assert_eq!(recovered_faucet.max_supply(), Felt::new(500_000)); assert_eq!(recovered_faucet.decimals(), 6); @@ -335,7 +335,7 @@ fn faucet_initialized_with_max_name_and_full_description() { None, ) .unwrap(); - let extension = Info::new() + let extension = TokenMetadata::new() .with_name(TokenName::new(&max_name).unwrap()) .with_description(description_typed, false); @@ -348,12 +348,12 @@ fn faucet_initialized_with_max_name_and_full_description() { .unwrap(); let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); assert_eq!(name_0, name_words[0]); assert_eq!(name_1, name_words[1]); for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); @@ -389,7 +389,7 @@ fn network_faucet_initialized_with_max_name_and_full_description() { ) .unwrap(); - let extension = Info::new() + let extension = TokenMetadata::new() .with_name(TokenName::new(&max_name).unwrap()) .with_description(description_typed, false); @@ -403,12 +403,12 @@ fn network_faucet_initialized_with_max_name_and_full_description() { .unwrap(); let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); - let name_0 = account.storage().get_item(Info::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(Info::name_chunk_1_slot()).unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); assert_eq!(name_0, name_words[0]); assert_eq!(name_1, name_words[1]); for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(Info::description_slot(i)).unwrap(); + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } let faucet_metadata = @@ -445,7 +445,7 @@ async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<( ) .unwrap(); - let extension = Info::new() + let extension = TokenMetadata::new() .with_name(TokenName::new(&max_name).unwrap()) .with_description(description_typed, false); @@ -721,7 +721,7 @@ async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { /// Isolated test: get_mutability_config. #[tokio::test] async fn metadata_get_config_only() -> anyhow::Result<()> { - let extension = Info::new() + let extension = TokenMetadata::new() .with_description(Description::new("test").unwrap(), true) // mutable .with_max_supply_mutable(true); @@ -790,10 +790,10 @@ async fn metadata_get_owner_only() -> anyhow::Result<()> { ) .unwrap(); - // Info provides the ownable::owner_config slot (single-step) that metadata::fungible::get_owner + // TokenMetadata provides the ownable::owner_config slot (single-step) that metadata::fungible::get_owner // reads from, plus the metadata library. NetworkFungibleFaucet uses ownable2step::owner_config // (different slot name), so there's no conflict. - let info = Info::new() + let info = TokenMetadata::new() .with_name(TokenName::new("POL").unwrap()) .with_owner(owner_account_id); @@ -991,7 +991,7 @@ async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { None, ) .unwrap(); - let extension = Info::new().with_name(token_name).with_description(description_typed, false); + let extension = TokenMetadata::new().with_name(token_name).with_description(description_typed, false); let account = AccountBuilder::new([3u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) @@ -1186,7 +1186,7 @@ async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { updated_faucet.apply_delta(executed.account_delta())?; for (i, expected) in new_desc.iter().enumerate() { - let chunk = updated_faucet.storage().get_item(Info::description_slot(i))?; + let chunk = updated_faucet.storage().get_item(TokenMetadata::description_slot(i))?; assert_eq!(chunk, *expected, "description_{i} should be updated"); } @@ -1477,18 +1477,18 @@ async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { let logo = LogoURI::new("https://example.com/logo").unwrap(); let link = ExternalLink::new("https://example.com").unwrap(); - let cases: Vec<(Info, &str, u8)> = vec![ - (Info::new().with_max_supply_mutable(true), "is_max_supply_mutable", 1), - (Info::new().with_description(desc.clone(), true), "is_description_mutable", 1), - (Info::new().with_description(desc, false), "is_description_mutable", 0), - (Info::new().with_logo_uri(logo.clone(), true), "is_logo_uri_mutable", 1), - (Info::new().with_logo_uri(logo, false), "is_logo_uri_mutable", 0), + let cases: Vec<(TokenMetadata, &str, u8)> = vec![ + (TokenMetadata::new().with_max_supply_mutable(true), "is_max_supply_mutable", 1), + (TokenMetadata::new().with_description(desc.clone(), true), "is_description_mutable", 1), + (TokenMetadata::new().with_description(desc, false), "is_description_mutable", 0), + (TokenMetadata::new().with_logo_uri(logo.clone(), true), "is_logo_uri_mutable", 1), + (TokenMetadata::new().with_logo_uri(logo, false), "is_logo_uri_mutable", 0), ( - Info::new().with_external_link(link.clone(), true), + TokenMetadata::new().with_external_link(link.clone(), true), "is_external_link_mutable", 1, ), - (Info::new().with_external_link(link, false), "is_external_link_mutable", 0), + (TokenMetadata::new().with_external_link(link, false), "is_external_link_mutable", 0), ]; for (info, proc_name, expected) in cases { @@ -1663,7 +1663,7 @@ async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { updated_faucet.apply_delta(executed.account_delta())?; for (i, expected) in new_logo.iter().enumerate() { - let chunk = updated_faucet.storage().get_item(Info::logo_uri_slot(i))?; + let chunk = updated_faucet.storage().get_item(TokenMetadata::logo_uri_slot(i))?; assert_eq!(chunk, *expected, "logo_uri_{i} should be updated"); } @@ -1892,7 +1892,7 @@ async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { updated_faucet.apply_delta(executed.account_delta())?; for (i, expected) in new_link.iter().enumerate() { - let chunk = updated_faucet.storage().get_item(Info::external_link_slot(i))?; + let chunk = updated_faucet.storage().get_item(TokenMetadata::external_link_slot(i))?; assert_eq!(chunk, *expected, "external_link_{i} should be updated"); } From bd71231223ce80b2d41fce6e4a32a461e913cd57 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 19:01:06 -0300 Subject: [PATCH 43/56] chore: Refactor code structure for improved readability and maintainability --- .../faucets/basic_fungible_faucet.masm | 14 +- .../faucets/fungible_token_metadata.masm | 21 + .../faucets/network_fungible_faucet.masm | 18 +- .../src/account/components/mod.rs | 20 + .../src/account/faucets/basic_fungible.rs | 301 +-- .../src/account/faucets/network_fungible.rs | 223 +- .../src/account/faucets/token_metadata.rs | 350 ++- .../src/account/interface/component.rs | 6 + .../src/account/interface/extension.rs | 6 + .../src/account/interface/test.rs | 10 +- .../src/account/metadata/mod.rs | 56 +- .../src/mock_chain/chain_builder.rs | 86 +- crates/miden-testing/src/standards/mod.rs | 1 + .../src/standards/token_metadata.rs | 1306 +++++++++++ crates/miden-testing/tests/lib.rs | 1 - crates/miden-testing/tests/metadata.rs | 1981 ----------------- 16 files changed, 1861 insertions(+), 2539 deletions(-) create mode 100644 crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm create mode 100644 crates/miden-testing/src/standards/token_metadata.rs delete mode 100644 crates/miden-testing/tests/metadata.rs diff --git a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm index f6fa952f3c..95e1a87433 100644 --- a/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/basic_fungible_faucet.masm @@ -1,19 +1,7 @@ # The MASM code of the Basic Fungible Faucet Account Component. # # See the `BasicFungibleFaucet` Rust type's documentation for more details. +# This component depends on `FungibleTokenMetadata` being present in the account. pub use ::miden::standards::faucets::basic_fungible::distribute pub use ::miden::standards::faucets::basic_fungible::burn - -# Metadata — getters only (no setters for basic faucet) -pub use ::miden::standards::metadata::fungible::get_name -pub use ::miden::standards::metadata::fungible::get_mutability_config -pub use ::miden::standards::metadata::fungible::is_max_supply_mutable -pub use ::miden::standards::metadata::fungible::is_description_mutable -pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable -pub use ::miden::standards::metadata::fungible::is_external_link_mutable -pub use ::miden::standards::metadata::fungible::get_token_metadata -pub use ::miden::standards::metadata::fungible::get_max_supply -pub use ::miden::standards::metadata::fungible::get_decimals -pub use ::miden::standards::metadata::fungible::get_token_symbol -pub use ::miden::standards::metadata::fungible::get_token_supply diff --git a/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm b/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm new file mode 100644 index 0000000000..9066b76f61 --- /dev/null +++ b/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm @@ -0,0 +1,21 @@ +# The MASM code of the Fungible Token Metadata Account Component. +# +# Re-exported from module miden::standards::components::fungible_token_metadata +# All metadata-related procedures are re-exported here. + +pub use ::miden::standards::metadata::fungible::get_name +pub use ::miden::standards::metadata::fungible::get_mutability_config +pub use ::miden::standards::metadata::fungible::is_max_supply_mutable +pub use ::miden::standards::metadata::fungible::is_description_mutable +pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable +pub use ::miden::standards::metadata::fungible::is_external_link_mutable +pub use ::miden::standards::metadata::fungible::get_token_metadata +pub use ::miden::standards::metadata::fungible::get_max_supply +pub use ::miden::standards::metadata::fungible::get_decimals +pub use ::miden::standards::metadata::fungible::get_token_symbol +pub use ::miden::standards::metadata::fungible::get_token_supply +pub use ::miden::standards::metadata::fungible::set_description +pub use ::miden::standards::metadata::fungible::set_logo_uri +pub use ::miden::standards::metadata::fungible::set_external_link +pub use ::miden::standards::metadata::fungible::set_max_supply +pub use ::miden::standards::metadata::fungible::get_owner diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index b3e4c886b4..cbef972e9b 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -1,23 +1,7 @@ # The MASM code of the Network Fungible Faucet Account Component. # # See the `NetworkFungibleFaucet` Rust type's documentation for more details. +# This component depends on `FungibleTokenMetadata` being present in the account. pub use ::miden::standards::faucets::network_fungible::distribute pub use ::miden::standards::faucets::network_fungible::burn - -# Metadata — all from metadata::fungible -pub use ::miden::standards::metadata::fungible::get_name -pub use ::miden::standards::metadata::fungible::get_mutability_config -pub use ::miden::standards::metadata::fungible::is_max_supply_mutable -pub use ::miden::standards::metadata::fungible::is_description_mutable -pub use ::miden::standards::metadata::fungible::is_logo_uri_mutable -pub use ::miden::standards::metadata::fungible::is_external_link_mutable -pub use ::miden::standards::metadata::fungible::get_token_metadata -pub use ::miden::standards::metadata::fungible::get_max_supply -pub use ::miden::standards::metadata::fungible::get_decimals -pub use ::miden::standards::metadata::fungible::get_token_symbol -pub use ::miden::standards::metadata::fungible::get_token_supply -pub use ::miden::standards::metadata::fungible::set_description -pub use ::miden::standards::metadata::fungible::set_logo_uri -pub use ::miden::standards::metadata::fungible::set_external_link -pub use ::miden::standards::metadata::fungible::set_max_supply diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 692bb4a3b2..440a8f0789 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -100,6 +100,15 @@ static NETWORK_FUNGIBLE_FAUCET_LIBRARY: LazyLock = LazyLock::new(|| { // METADATA LIBRARIES // ================================================================================================ +// Initialize the Fungible Token Metadata library only once. +static FUNGIBLE_TOKEN_METADATA_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/faucets/fungible_token_metadata.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Fungible Token Metadata library is well-formed") +}); + // Metadata Info component uses the standards library (get_name, set_description, etc. from // metadata). static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = @@ -134,6 +143,11 @@ pub fn network_fungible_faucet_library() -> Library { NETWORK_FUNGIBLE_FAUCET_LIBRARY.clone() } +/// Returns the Fungible Token Metadata Library. +pub fn fungible_token_metadata_library() -> Library { + FUNGIBLE_TOKEN_METADATA_LIBRARY.clone() +} + /// Returns the Metadata Info component library. /// /// Uses the standards library; the standalone [`Info`](crate::account::metadata::TokenMetadata) @@ -180,6 +194,7 @@ pub fn no_auth_library() -> Library { /// crate. pub enum StandardAccountComponent { BasicWallet, + FungibleTokenMetadata, BasicFungibleFaucet, NetworkFungibleFaucet, AuthSingleSig, @@ -194,6 +209,7 @@ impl StandardAccountComponent { pub fn procedure_digests(&self) -> impl Iterator { let library = match self { Self::BasicWallet => BASIC_WALLET_LIBRARY.as_ref(), + Self::FungibleTokenMetadata => FUNGIBLE_TOKEN_METADATA_LIBRARY.as_ref(), Self::BasicFungibleFaucet => BASIC_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::NetworkFungibleFaucet => NETWORK_FUNGIBLE_FAUCET_LIBRARY.as_ref(), Self::AuthSingleSig => SINGLESIG_LIBRARY.as_ref(), @@ -237,6 +253,9 @@ impl StandardAccountComponent { Self::BasicWallet => { component_interface_vec.push(AccountComponentInterface::BasicWallet) }, + Self::FungibleTokenMetadata => { + component_interface_vec.push(AccountComponentInterface::FungibleTokenMetadata) + }, Self::BasicFungibleFaucet => { component_interface_vec.push(AccountComponentInterface::BasicFungibleFaucet) }, @@ -269,6 +288,7 @@ impl StandardAccountComponent { component_interface_vec: &mut Vec, ) { Self::BasicWallet.extract_component(procedures_set, component_interface_vec); + Self::FungibleTokenMetadata.extract_component(procedures_set, component_interface_vec); Self::BasicFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::NetworkFungibleFaucet.extract_component(procedures_set, component_interface_vec); Self::AuthSingleSig.extract_component(procedures_set, component_interface_vec); diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 25b1e5a902..06d6340413 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -1,38 +1,18 @@ -use miden_protocol::account::component::{ - AccountComponentMetadata, - FeltSchema, - SchemaType, - StorageSchema, - StorageSlotSchema, -}; +use miden_protocol::Word; +use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, - AccountStorage, AccountStorageMode, AccountType, - StorageSlotName, -}; -use miden_protocol::asset::TokenSymbol; -use miden_protocol::{Felt, Word}; - -use super::{ - Description, - ExternalLink, - FungibleFaucetError, - FungibleTokenMetadata, - LogoURI, - TokenName, }; + +use super::{FungibleFaucetError, FungibleTokenMetadata}; use crate::account::AuthMethod; use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig}; use crate::account::components::basic_fungible_faucet_library; - -/// The schema type for token symbols. -const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::TokenMetadata; use crate::procedure_digest; // BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT @@ -70,15 +50,11 @@ procedure_digest!( /// /// This component supports accounts of type [`AccountType::FungibleFaucet`]. /// -/// ## Storage Layout -/// -/// - [`Self::metadata_slot`]: Stores [`FungibleTokenMetadata`]. +/// This component depends on [`FungibleTokenMetadata`] being present in the account for storage +/// of token metadata. It has no storage slots of its own. /// /// [builder]: crate::code_builder::CodeBuilder -pub struct BasicFungibleFaucet { - metadata: FungibleTokenMetadata, - info: Option, -} +pub struct BasicFungibleFaucet; impl BasicFungibleFaucet { // CONSTANTS @@ -87,147 +63,12 @@ impl BasicFungibleFaucet { /// The name of the component. pub const NAME: &'static str = "miden::standards::components::faucets::basic_fungible_faucet"; - /// The maximum number of decimals supported by the component. - pub const MAX_DECIMALS: u8 = FungibleTokenMetadata::MAX_DECIMALS; - const DISTRIBUTE_PROC_NAME: &str = "distribute"; const BURN_PROC_NAME: &str = "burn"; - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata and with - /// an initial token supply of zero. - /// - /// Optional `description`, `logo_uri`, and `external_link` are stored in the metadata - /// the faucet's storage slots when building an account (e.g. via - /// [`create_basic_fungible_faucet`]). - /// - /// # Errors - /// - /// Returns an error if: - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply parameter exceeds maximum possible amount for a fungible asset - /// ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]) - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - name: TokenName, - description: Option, - logo_uri: Option, - external_link: Option, - ) -> Result { - let metadata = FungibleTokenMetadata::new( - symbol, - decimals, - max_supply, - name, - description, - logo_uri, - external_link, - )?; - Ok(Self { metadata, info: None }) - } - - /// Creates a new [`BasicFungibleFaucet`] component from the given [`FungibleTokenMetadata`]. - /// - /// This is a convenience constructor that allows creating a faucet from pre-validated - /// metadata. - pub fn from_metadata(metadata: FungibleTokenMetadata) -> Self { - Self { metadata, info: None } - } - - /// Attaches token metadata (name, description, logo, link, mutability flags) to the - /// faucet. These storage slots will be included in the component when built. - pub fn with_info(mut self, info: TokenMetadata) -> Self { - self.info = Some(info); - self - } - - /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account - /// interface and storage. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided [`AccountInterface`] does not contain a - /// [`AccountComponentInterface::BasicFungibleFaucet`] component. - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply value exceeds maximum possible amount for a fungible asset of - /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. - /// - the token supply exceeds the max supply. - /// - the token symbol encoded value exceeds the maximum value of - /// [`TokenSymbol::MAX_ENCODED_VALUE`]. - fn try_from_interface( - interface: AccountInterface, - storage: &AccountStorage, - ) -> Result { - // Check that the procedures of the basic fungible faucet exist in the account. - if !interface.components().contains(&AccountComponentInterface::BasicFungibleFaucet) { - return Err(FungibleFaucetError::MissingBasicFungibleFaucetInterface); - } - - let metadata = FungibleTokenMetadata::try_from(storage)?; - Ok(Self { metadata, info: None }) - } - // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Returns the [`StorageSlotName`] where the [`BasicFungibleFaucet`]'s metadata is stored (slot - /// 0). - pub fn metadata_slot() -> &'static StorageSlotName { - FungibleTokenMetadata::metadata_slot() - } - - /// Returns the storage slot schema for the metadata slot. - pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); - ( - Self::metadata_slot().clone(), - StorageSlotSchema::value( - "Token metadata", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u8("decimals"), - FeltSchema::new_typed(token_symbol_type, "symbol"), - ], - ), - ) - } - - /// Returns the token metadata. - pub fn metadata(&self) -> &FungibleTokenMetadata { - &self.metadata - } - - /// Returns the symbol of the faucet. - pub fn symbol(&self) -> TokenSymbol { - self.metadata.symbol() - } - - /// Returns the decimals of the faucet. - pub fn decimals(&self) -> u8 { - self.metadata.decimals() - } - - /// Returns the max supply (in base units) of the faucet. - /// - /// This is the highest amount of tokens that can be minted from this faucet. - pub fn max_supply(&self) -> Felt { - self.metadata.max_supply() - } - - /// Returns the token supply (in base units) of the faucet. - /// - /// This is the amount of tokens that were minted from the faucet so far. Its value can never - /// exceed [`Self::max_supply`]. - pub fn token_supply(&self) -> Felt { - self.metadata.token_supply() - } - /// Returns the digest of the `distribute` account procedure. pub fn distribute_digest() -> Word { *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE @@ -238,39 +79,27 @@ impl BasicFungibleFaucet { *BASIC_FUNGIBLE_FAUCET_BURN } - // MUTATORS - // -------------------------------------------------------------------------------------------- + /// Checks that the account contains the basic fungible faucet interface and extracts + /// the [`FungibleTokenMetadata`] from storage. + fn try_from_interface( + interface: AccountInterface, + _storage: &miden_protocol::account::AccountStorage, + ) -> Result { + if !interface.components().contains(&AccountComponentInterface::BasicFungibleFaucet) { + return Err(FungibleFaucetError::MissingBasicFungibleFaucetInterface); + } - /// Sets the token_supply (in base units) of the basic fungible faucet. - /// - /// # Errors - /// - /// Returns an error if: - /// - the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - self.metadata = self.metadata.with_token_supply(token_supply)?; - Ok(self) + Ok(BasicFungibleFaucet) } } impl From for AccountComponent { - fn from(faucet: BasicFungibleFaucet) -> Self { - let storage_slot = faucet.metadata.into(); - - let mut slots = vec![storage_slot]; - if let Some(info) = &faucet.info { - slots.extend(info.storage_slots()); - } - - let storage_schema = StorageSchema::new([BasicFungibleFaucet::metadata_slot_schema()]) - .expect("storage schema should be valid"); - + fn from(_faucet: BasicFungibleFaucet) -> Self { let metadata = AccountComponentMetadata::new(BasicFungibleFaucet::NAME, [AccountType::FungibleFaucet]) - .with_description("Basic fungible faucet component for minting and burning tokens") - .with_storage_schema(storage_schema); + .with_description("Basic fungible faucet component for minting and burning tokens"); - AccountComponent::new(basic_fungible_faucet_library(), slots, metadata) + AccountComponent::new(basic_fungible_faucet_library(), vec![], metadata) .expect("basic fungible faucet component should satisfy the requirements of a valid account component") } } @@ -308,9 +137,9 @@ impl TryFrom<&Account> for BasicFungibleFaucet { /// /// The storage layout of the faucet account is defined by the combination of the following /// components (see their docs for details): -/// - [`BasicFungibleFaucet`] +/// - [`FungibleTokenMetadata`] (token metadata, name, description, etc.) +/// - [`BasicFungibleFaucet`] (distribute and burn procedures) /// - [`AuthSingleSigAcl`] -/// - Token metadata (name, description, etc.) when provided via [`BasicFungibleFaucet::with_info`] pub fn create_basic_fungible_faucet( init_seed: [u8; 32], metadata: FungibleTokenMetadata, @@ -347,14 +176,12 @@ pub fn create_basic_fungible_faucet( }, }; - let info = metadata.to_token_metadata_info(); - let faucet = BasicFungibleFaucet::from_metadata(metadata).with_info(info); - let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(account_storage_mode) .with_auth_component(auth_component) - .with_component(faucet) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .build() .map_err(FungibleFaucetError::AccountError)?; @@ -367,8 +194,8 @@ pub fn create_basic_fungible_faucet( #[cfg(test)] mod tests { use assert_matches::assert_matches; - use miden_protocol::Word; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; + use miden_protocol::{Felt, Word}; use super::{ AccountBuilder, @@ -376,15 +203,12 @@ mod tests { AccountType, AuthMethod, BasicFungibleFaucet, - Description, - Felt, FungibleFaucetError, FungibleTokenMetadata, - TokenName, - TokenSymbol, create_basic_fungible_faucet, }; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; + use crate::account::faucets::{Description, TokenName}; use crate::account::metadata::TokenMetadata; use crate::account::wallets::BasicWallet; @@ -403,7 +227,8 @@ mod tests { let max_supply = Felt::new(123); let token_symbol_string = "POL"; - let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap(); + let token_symbol = + miden_protocol::asset::TokenSymbol::try_from(token_symbol_string).unwrap(); let token_name_string = "polygon"; let description_string = "A polygon token"; let decimals = 2u8; @@ -456,27 +281,22 @@ mod tests { // Check that faucet metadata was initialized to the given values. // Storage layout: [token_supply, max_supply, decimals, symbol] assert_eq!( - faucet_account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(), + faucet_account + .storage() + .get_item(FungibleTokenMetadata::metadata_slot()) + .unwrap(), [Felt::ZERO, Felt::new(123), Felt::new(2), token_symbol.into()].into() ); - // Check that Info component has name and description - let name_0 = faucet_account - .storage() - .get_item(TokenMetadata::name_chunk_0_slot()) - .unwrap(); - let name_1 = faucet_account - .storage() - .get_item(TokenMetadata::name_chunk_1_slot()) - .unwrap(); + // Check that name was stored + let name_0 = faucet_account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = faucet_account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); let decoded_name = TokenName::try_from_words(&[name_0, name_1]).unwrap(); assert_eq!(decoded_name.as_str(), token_name_string); let expected_desc_words = Description::new(description_string).unwrap().to_words(); for (i, expected) in expected_desc_words.iter().enumerate() { - let chunk = faucet_account - .storage() - .get_item(TokenMetadata::description_slot(i)) - .unwrap(); + let chunk = + faucet_account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); assert_eq!(chunk, *expected); } @@ -484,12 +304,8 @@ mod tests { assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet); - // Verify the faucet can be extracted and has correct metadata - let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap(); - assert_eq!(faucet_component.symbol(), token_symbol); - assert_eq!(faucet_component.decimals(), decimals); - assert_eq!(faucet_component.max_supply(), max_supply); - assert_eq!(faucet_component.token_supply(), Felt::ZERO); + // Verify the faucet component can be extracted + let _faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap(); } #[test] @@ -500,21 +316,23 @@ mod tests { let mock_seed = mock_word.as_bytes(); // valid account - let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol"); + let token_symbol = + miden_protocol::asset::TokenSymbol::new("POL").expect("invalid token symbol"); + let metadata = FungibleTokenMetadata::new( + token_symbol, + 10, + Felt::new(100), + TokenName::new("POL").unwrap(), + None, + None, + None, + ) + .expect("failed to create token metadata"); + let faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) - .with_component( - BasicFungibleFaucet::new( - token_symbol, - 10, - Felt::new(100), - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .expect("failed to create a fungible faucet component"), - ) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .with_auth_component(AuthSingleSig::new( mock_public_key, AuthScheme::Falcon512Poseidon2, @@ -522,17 +340,16 @@ mod tests { .build_existing() .expect("failed to create wallet account"); - let basic_ff = BasicFungibleFaucet::try_from(faucet_account) + let _basic_ff = BasicFungibleFaucet::try_from(faucet_account) .expect("basic fungible faucet creation failed"); - assert_eq!(basic_ff.symbol(), token_symbol); - assert_eq!(basic_ff.decimals(), 10); - assert_eq!(basic_ff.max_supply(), Felt::new(100)); - assert_eq!(basic_ff.token_supply(), Felt::ZERO); // invalid account: basic fungible faucet component is missing let invalid_faucet_account = AccountBuilder::new(mock_seed) .account_type(AccountType::FungibleFaucet) - .with_auth_component(AuthSingleSig::new(mock_public_key, AuthScheme::Falcon512Poseidon2)) + .with_auth_component(AuthSingleSig::new( + mock_public_key, + AuthScheme::Falcon512Poseidon2, + )) // we need to add some other component so the builder doesn't fail .with_component(BasicWallet) .build_existing() diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index c426c56b89..620129c33d 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -1,40 +1,22 @@ -use miden_protocol::account::component::{ - AccountComponentMetadata, - FeltSchema, - SchemaType, - StorageSchema, - StorageSlotSchema, -}; +use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, - AccountStorage, AccountStorageMode, AccountType, - StorageSlotName, }; use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; -use super::{ - Description, - ExternalLink, - FungibleFaucetError, - FungibleTokenMetadata, - LogoURI, - TokenName, -}; +use super::{FungibleFaucetError, FungibleTokenMetadata}; use crate::account::access::AccessControl; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; +use crate::account::faucets::TokenName; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -use crate::account::metadata::TokenMetadata; use crate::procedure_digest; -/// The schema type for token symbols. -const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; - // NETWORK FUNGIBLE FAUCET ACCOUNT COMPONENT // ================================================================================================ @@ -71,15 +53,11 @@ procedure_digest!( /// `distribute`. When building an account with this component, /// [`crate::account::access::Ownable2Step`] must also be included. /// -/// ## Storage Layout -/// -/// - [`Self::metadata_slot`]: Fungible faucet metadata. +/// This component depends on [`FungibleTokenMetadata`] being present in the account for storage +/// of token metadata. It has no storage slots of its own. /// /// [builder]: crate::code_builder::CodeBuilder -pub struct NetworkFungibleFaucet { - metadata: FungibleTokenMetadata, - info: Option, -} +pub struct NetworkFungibleFaucet; impl NetworkFungibleFaucet { // CONSTANTS @@ -88,79 +66,27 @@ impl NetworkFungibleFaucet { /// The name of the component. pub const NAME: &'static str = "miden::standards::components::faucets::network_fungible_faucet"; - /// The maximum number of decimals supported by the component. - pub const MAX_DECIMALS: u8 = FungibleTokenMetadata::MAX_DECIMALS; - const DISTRIBUTE_PROC_NAME: &str = "distribute"; const BURN_PROC_NAME: &str = "burn"; - // CONSTRUCTORS + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`NetworkFungibleFaucet`] component from the given pieces of metadata. - /// - /// Optional `description`, `logo_uri`, and `external_link` are stored in the component's - /// storage slots when building an account. - /// - /// # Errors: - /// Returns an error if: - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply parameter exceeds maximum possible amount for a fungible asset - /// ([`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]) - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - name: TokenName, - description: Option, - logo_uri: Option, - external_link: Option, - ) -> Result { - let metadata = FungibleTokenMetadata::new( - symbol, - decimals, - max_supply, - name, - description, - logo_uri, - external_link, - )?; - Ok(Self { metadata, info: None }) - } - - /// Creates a new [`NetworkFungibleFaucet`] component from the given [`FungibleTokenMetadata`]. - /// - /// This is a convenience constructor that allows creating a faucet from pre-validated - /// metadata. - pub fn from_metadata(metadata: FungibleTokenMetadata) -> Self { - Self { metadata, info: None } + /// Returns the digest of the `distribute` account procedure. + pub fn distribute_digest() -> Word { + *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE } - /// Attaches token metadata (name, description, logo, link, mutability flags) to the - /// faucet. These storage slots will be included in the component when built. - pub fn with_info(mut self, info: TokenMetadata) -> Self { - self.info = Some(info); - self + /// Returns the digest of the `burn` account procedure. + pub fn burn_digest() -> Word { + *NETWORK_FUNGIBLE_FAUCET_BURN } - /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account - /// interface and storage. - /// - /// # Errors: - /// Returns an error if: - /// - the provided [`AccountInterface`] does not contain a - /// [`AccountComponentInterface::NetworkFungibleFaucet`] component. - /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`]. - /// - the max supply value exceeds maximum possible amount for a fungible asset of - /// [`miden_protocol::asset::FungibleAsset::MAX_AMOUNT`]. - /// - the token supply exceeds the max supply. - /// - the token symbol encoded value exceeds the maximum value of - /// [`TokenSymbol::MAX_ENCODED_VALUE`]. + /// Checks that the account contains the network fungible faucet interface. fn try_from_interface( interface: AccountInterface, - storage: &AccountStorage, + _storage: &miden_protocol::account::AccountStorage, ) -> Result { - // Check that the procedures of the network fungible faucet exist in the account. if !interface .components() .contains(&AccountComponentInterface::NetworkFungibleFaucet) @@ -168,113 +94,19 @@ impl NetworkFungibleFaucet { return Err(FungibleFaucetError::MissingNetworkFungibleFaucetInterface); } - // Read token metadata from storage - let metadata = FungibleTokenMetadata::try_from(storage)?; - - Ok(Self { metadata, info: None }) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns the [`StorageSlotName`] where the [`NetworkFungibleFaucet`]'s metadata is stored - /// (slot 0). - pub fn metadata_slot() -> &'static StorageSlotName { - FungibleTokenMetadata::metadata_slot() - } - - /// Returns the storage slot schema for the metadata slot. - pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { - let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); - ( - Self::metadata_slot().clone(), - StorageSlotSchema::value( - "Token metadata", - [ - FeltSchema::felt("token_supply").with_default(Felt::new(0)), - FeltSchema::felt("max_supply"), - FeltSchema::u8("decimals"), - FeltSchema::new_typed(token_symbol_type, "symbol"), - ], - ), - ) - } - - /// Returns the token metadata. - pub fn metadata(&self) -> &FungibleTokenMetadata { - &self.metadata - } - - /// Returns the symbol of the faucet. - pub fn symbol(&self) -> TokenSymbol { - self.metadata.symbol() - } - - /// Returns the decimals of the faucet. - pub fn decimals(&self) -> u8 { - self.metadata.decimals() - } - - /// Returns the max supply (in base units) of the faucet. - /// - /// This is the highest amount of tokens that can be minted from this faucet. - pub fn max_supply(&self) -> Felt { - self.metadata.max_supply() - } - - /// Returns the token supply (in base units) of the faucet. - /// - /// This is the amount of tokens that were minted from the faucet so far. Its value can never - /// exceed [`Self::max_supply`]. - pub fn token_supply(&self) -> Felt { - self.metadata.token_supply() - } - - /// Returns the digest of the `distribute` account procedure. - pub fn distribute_digest() -> Word { - *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE - } - - /// Returns the digest of the `burn` account procedure. - pub fn burn_digest() -> Word { - *NETWORK_FUNGIBLE_FAUCET_BURN - } - - // MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Sets the token_supply (in base units) of the network fungible faucet. - /// - /// # Errors - /// - /// Returns an error if: - /// - the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - self.metadata = self.metadata.with_token_supply(token_supply)?; - Ok(self) + Ok(NetworkFungibleFaucet) } } impl From for AccountComponent { - fn from(network_faucet: NetworkFungibleFaucet) -> Self { - let metadata_slot = network_faucet.metadata.into(); - - let mut slots = vec![metadata_slot]; - if let Some(info) = &network_faucet.info { - slots.extend(info.storage_slots()); - } - - let storage_schema = StorageSchema::new([NetworkFungibleFaucet::metadata_slot_schema()]) - .expect("storage schema should be valid"); - + fn from(_network_faucet: NetworkFungibleFaucet) -> Self { let metadata = AccountComponentMetadata::new( NetworkFungibleFaucet::NAME, [AccountType::FungibleFaucet], ) - .with_description("Network fungible faucet component for minting and burning tokens") - .with_storage_schema(storage_schema); + .with_description("Network fungible faucet component for minting and burning tokens"); - AccountComponent::new(network_fungible_faucet_library(), slots, metadata) + AccountComponent::new(network_fungible_faucet_library(), vec![], metadata) .expect("network fungible faucet component should satisfy the requirements of a valid account component") } } @@ -314,7 +146,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// - [`AccountStorageMode::Network`] for storage /// - [`NoAuth`] for authentication /// -/// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] and +/// The storage layout of the faucet account is documented on the [`FungibleTokenMetadata`] and /// [`crate::account::access::Ownable2Step`] types, and contains no additional storage slots for /// its auth ([`NoAuth`]). pub fn create_network_fungible_faucet( @@ -327,15 +159,15 @@ pub fn create_network_fungible_faucet( ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let faucet = NetworkFungibleFaucet::new( - symbol, decimals, max_supply, name, None, None, None, - )?; + let metadata = + FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None)?; let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(auth_component) - .with_component(faucet) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) .with_component(access_control) .build() .map_err(FungibleFaucetError::AccountError)?; @@ -352,6 +184,7 @@ mod tests { use super::*; use crate::account::access::Ownable2Step; + use crate::account::faucets::TokenName; #[test] fn test_create_network_fungible_faucet() { @@ -384,11 +217,7 @@ mod tests { expected_owner_word ); - let faucet = NetworkFungibleFaucet::try_from(&account) + let _faucet = NetworkFungibleFaucet::try_from(&account) .expect("network fungible faucet should be extractable from account"); - assert_eq!(faucet.symbol(), symbol); - assert_eq!(faucet.decimals(), decimals); - assert_eq!(faucet.max_supply(), max_supply); - assert_eq!(faucet.token_supply(), Felt::ZERO); } } diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 852fab5e63..4c5c82f8a1 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -1,11 +1,27 @@ use alloc::boxed::Box; use alloc::string::String; - -use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; +use alloc::vec::Vec; + +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + SchemaType, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + AccountComponent, + AccountId, + AccountStorage, + AccountType, + StorageSlot, + StorageSlotName, +}; use miden_protocol::asset::{FungibleAsset, TokenSymbol}; use miden_protocol::{Felt, Word}; use super::FungibleFaucetError; +use crate::account::components::fungible_token_metadata_library; use crate::account::metadata::{self, FieldBytesError, NameUtf8Error}; // ENCODING CONSTANTS @@ -295,11 +311,12 @@ fn decode_field_from_words(words: &[Word; 7]) -> Result /// The metadata is stored in a single storage slot as: /// `[token_supply, max_supply, decimals, symbol]` /// -/// `name` and optional `description`/`logo_uri`/`external_link` are not serialized into that -/// slot. They are kept here as convenience accessors and for use when constructing the -/// [`TokenMetadata`](crate::account::metadata::TokenMetadata) storage slots via -/// [`BasicFungibleFaucet::with_info`](super::BasicFungibleFaucet::with_info) or -/// [`NetworkFungibleFaucet::with_info`](super::NetworkFungibleFaucet::with_info). +/// `name` and optional `description`/`logo_uri`/`external_link` are stored in separate +/// storage slots (slots 2–25). All fields are serialized into the component's storage +/// via [`storage_slots`](Self::storage_slots) when converting to an [`AccountComponent`]. +/// The schema type for token symbols. +const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; + #[derive(Debug, Clone)] pub struct FungibleTokenMetadata { token_supply: Felt, @@ -310,6 +327,11 @@ pub struct FungibleTokenMetadata { description: Option, logo_uri: Option, external_link: Option, + owner: Option, + description_mutable: bool, + logo_uri_mutable: bool, + external_link_mutable: bool, + max_supply_mutable: bool, } impl FungibleTokenMetadata { @@ -396,6 +418,11 @@ impl FungibleTokenMetadata { description, logo_uri, external_link, + owner: None, + description_mutable: false, + logo_uri_mutable: false, + external_link_mutable: false, + max_supply_mutable: false, }) } @@ -448,20 +475,99 @@ impl FungibleTokenMetadata { self.external_link.as_ref() } - /// Builds a [`TokenMetadataInfo`](crate::account::metadata::TokenMetadata) from this - /// metadata, copying the name and any optional description/logo_uri/external_link fields. - pub fn to_token_metadata_info(&self) -> metadata::TokenMetadata { - let mut info = metadata::TokenMetadata::new().with_name(self.name.clone()); - if let Some(d) = self.description() { - info = info.with_description(d.clone(), false); + /// Returns the storage slot schema for the metadata slot. + pub fn metadata_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + let token_symbol_type = SchemaType::new(TOKEN_SYMBOL_TYPE).expect("valid type"); + ( + Self::metadata_slot().clone(), + StorageSlotSchema::value( + "Token metadata", + [ + FeltSchema::felt("token_supply").with_default(Felt::new(0)), + FeltSchema::felt("max_supply"), + FeltSchema::u8("decimals"), + FeltSchema::new_typed(token_symbol_type, "symbol"), + ], + ), + ) + } + + /// Returns all the storage slots for this component (metadata word + name + config + + /// description + logo_uri + external_link). + pub fn storage_slots(&self) -> Vec { + let mut slots: Vec = Vec::new(); + + // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures + // for verify_owner (used in set_* mutations). + if let Some(id) = self.owner { + let owner_word = + Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()]); + slots.push(StorageSlot::with_value(metadata::owner_config_slot().clone(), owner_word)); } - if let Some(l) = self.logo_uri() { - info = info.with_logo_uri(l.clone(), false); + + // Slot 0: metadata word [token_supply, max_supply, decimals, symbol] + let metadata_word = Word::new([ + self.token_supply, + self.max_supply, + Felt::from(self.decimals), + self.symbol.into(), + ]); + slots.push(StorageSlot::with_value(Self::metadata_slot().clone(), metadata_word)); + + // Slots 2-3: name (2 Words) + let name_words = self.name.to_words(); + slots.push(StorageSlot::with_value( + metadata::TokenMetadata::name_chunk_0_slot().clone(), + name_words[0], + )); + slots.push(StorageSlot::with_value( + metadata::TokenMetadata::name_chunk_1_slot().clone(), + name_words[1], + )); + + // Slot 4: mutability config + let mutability_config_word = Word::from([ + Felt::from(self.description_mutable as u32), + Felt::from(self.logo_uri_mutable as u32), + Felt::from(self.external_link_mutable as u32), + Felt::from(self.max_supply_mutable as u32), + ]); + slots.push(StorageSlot::with_value( + metadata::mutability_config_slot().clone(), + mutability_config_word, + )); + + // Slots 5-11: description (7 Words) + let desc_words: [Word; 7] = + self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); + for (i, word) in desc_words.iter().enumerate() { + slots.push(StorageSlot::with_value( + metadata::TokenMetadata::description_slot(i).clone(), + *word, + )); } - if let Some(e) = self.external_link() { - info = info.with_external_link(e.clone(), false); + + // Slots 12-18: logo_uri (7 Words) + let logo_words: [Word; 7] = + self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); + for (i, word) in logo_words.iter().enumerate() { + slots.push(StorageSlot::with_value( + metadata::TokenMetadata::logo_uri_slot(i).clone(), + *word, + )); + } + + // Slots 19-25: external_link (7 Words) + let link_words: [Word; 7] = + self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); + for (i, word) in link_words.iter().enumerate() { + slots.push(StorageSlot::with_value( + metadata::TokenMetadata::external_link_slot(i).clone(), + *word, + )); } - info + + slots } // MUTATORS @@ -485,6 +591,40 @@ impl FungibleTokenMetadata { Ok(self) } + + /// Sets whether the description can be updated by the owner. + pub fn with_description_mutable(mut self, mutable: bool) -> Self { + self.description_mutable = mutable; + self + } + + /// Sets whether the logo URI can be updated by the owner. + pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self { + self.logo_uri_mutable = mutable; + self + } + + /// Sets whether the external link can be updated by the owner. + pub fn with_external_link_mutable(mut self, mutable: bool) -> Self { + self.external_link_mutable = mutable; + self + } + + /// Sets whether the max supply can be updated by the owner. + pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { + self.max_supply_mutable = mutable; + self + } + + /// Sets the owner for the `ownable::owner_config` slot. + /// + /// This is required by the `metadata::fungible` MASM procedures + /// (`set_description`, `set_logo_uri`, `set_external_link`, `set_max_supply`) + /// which use `ownable::verify_owner` to authorize mutations. + pub fn with_owner(mut self, owner: AccountId) -> Self { + self.owner = Some(owner); + self + } } // TRAIT IMPLEMENTATIONS @@ -536,6 +676,27 @@ impl From for StorageSlot { } } +impl From for AccountComponent { + fn from(metadata: FungibleTokenMetadata) -> Self { + let storage_schema = StorageSchema::new([FungibleTokenMetadata::metadata_slot_schema()]) + .expect("storage schema should be valid"); + + let component_metadata = AccountComponentMetadata::new( + "miden::standards::components::faucets::fungible_token_metadata", + [AccountType::FungibleFaucet], + ) + .with_description("Fungible token metadata component storing token metadata, name, mutability config, description, logo URI, and external link") + .with_storage_schema(storage_schema); + + AccountComponent::new( + fungible_token_metadata_library(), + metadata.storage_slots(), + component_metadata, + ) + .expect("fungible token metadata component should satisfy the requirements of a valid account component") + } +} + impl TryFrom<&StorageSlot> for FungibleTokenMetadata { type Error = FungibleFaucetError; @@ -809,4 +970,157 @@ mod tests { assert_eq!(restored.max_supply(), max_supply); assert_eq!(restored.token_supply(), token_supply); } + + #[test] + fn mutability_builders() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(1_000), name, None, None, None) + .unwrap() + .with_description_mutable(true) + .with_logo_uri_mutable(true) + .with_external_link_mutable(false) + .with_max_supply_mutable(true); + + let slots = metadata.storage_slots(); + + // Slot at index 3 is mutability_config: [desc, logo, extlink, max_supply] + let config_slot = &slots[3]; + let config_word = config_slot.value(); + assert_eq!(config_word[0], Felt::from(1u32), "desc_mutable"); + assert_eq!(config_word[1], Felt::from(1u32), "logo_mutable"); + assert_eq!(config_word[2], Felt::from(0u32), "extlink_mutable"); + assert_eq!(config_word[3], Felt::from(1u32), "max_supply_mutable"); + } + + #[test] + fn mutability_defaults_to_false() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(1_000), name, None, None, None) + .unwrap(); + + let slots = metadata.storage_slots(); + let config_word = slots[3].value(); + assert_eq!(config_word[0], Felt::ZERO, "desc_mutable default"); + assert_eq!(config_word[1], Felt::ZERO, "logo_mutable default"); + assert_eq!(config_word[2], Felt::ZERO, "extlink_mutable default"); + assert_eq!(config_word[3], Felt::ZERO, "max_supply_mutable default"); + } + + #[test] + fn storage_slots_includes_metadata_word() { + let symbol = TokenSymbol::new("POL").unwrap(); + let name = TokenName::new("polygon").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(123), name, None, None, None).unwrap(); + let slots = metadata.storage_slots(); + + // First slot is the metadata word [token_supply, max_supply, decimals, symbol] + let metadata_word = slots[0].value(); + assert_eq!(metadata_word[0], Felt::ZERO); // token_supply + assert_eq!(metadata_word[1], Felt::new(123)); // max_supply + assert_eq!(metadata_word[2], Felt::from(2u32)); // decimals + assert_eq!(metadata_word[3], Felt::from(symbol)); // symbol + } + + #[test] + fn storage_slots_includes_name() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("my token").unwrap(); + let expected_words = name.to_words(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let slots = metadata.storage_slots(); + + // Slots 1 and 2 are name chunks + assert_eq!(slots[1].value(), expected_words[0]); + assert_eq!(slots[2].value(), expected_words[1]); + } + + #[test] + fn storage_slots_includes_description() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + let description = Description::new("A cool token").unwrap(); + let expected_words = description.to_words(); + + let metadata = FungibleTokenMetadata::new( + symbol, + 2, + Felt::new(100), + name, + Some(description), + None, + None, + ) + .unwrap(); + let slots = metadata.storage_slots(); + + // Slots 4..11 are description (7 words), starting after metadata + name(2) + config + for (i, expected) in expected_words.iter().enumerate() { + assert_eq!(slots[4 + i].value(), *expected, "description word {i}"); + } + } + + #[test] + fn storage_slots_total_count() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let slots = metadata.storage_slots(); + + // 1 metadata + 2 name + 1 config + 7 description + 7 logo + 7 external_link = 25 + assert_eq!(slots.len(), 25); + } + + #[test] + fn into_account_component() { + use miden_protocol::account::{AccountBuilder, AccountType}; + + use crate::account::auth::NoAuth; + use crate::account::faucets::basic_fungible::BasicFungibleFaucet; + + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("test token").unwrap(); + let description = Description::new("A test").unwrap(); + + let metadata = FungibleTokenMetadata::new( + symbol, + 4, + Felt::new(10_000), + name, + Some(description), + None, + None, + ) + .unwrap() + .with_max_supply_mutable(true); + + // Should build an account successfully with FungibleTokenMetadata as a component + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .expect("account build should succeed"); + + // Verify metadata slot is accessible + let md_word = account.storage().get_item(FungibleTokenMetadata::metadata_slot()).unwrap(); + assert_eq!(md_word[1], Felt::new(10_000)); // max_supply + assert_eq!(md_word[2], Felt::from(4u32)); // decimals + + // Verify mutability config + let config = account.storage().get_item(metadata::mutability_config_slot()).unwrap(); + assert_eq!(config[3], Felt::from(1u32), "max_supply_mutable"); + } } diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index 51615b7151..65e236685b 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -19,6 +19,9 @@ pub enum AccountComponentInterface { /// Exposes procedures from the [`BasicWallet`][crate::account::wallets::BasicWallet] module. BasicWallet, /// Exposes procedures from the + /// [`FungibleTokenMetadata`][crate::account::faucets::FungibleTokenMetadata] module. + FungibleTokenMetadata, + /// Exposes procedures from the /// [`BasicFungibleFaucet`][crate::account::faucets::BasicFungibleFaucet] module. BasicFungibleFaucet, /// Exposes procedures from the @@ -57,6 +60,9 @@ impl AccountComponentInterface { pub fn name(&self) -> String { match self { AccountComponentInterface::BasicWallet => "Basic Wallet".to_string(), + AccountComponentInterface::FungibleTokenMetadata => { + "Fungible Token Metadata".to_string() + }, AccountComponentInterface::BasicFungibleFaucet => "Basic Fungible Faucet".to_string(), AccountComponentInterface::NetworkFungibleFaucet => { "Network Fungible Faucet".to_string() diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index f23b1414a7..cf8b783080 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -13,6 +13,7 @@ use crate::account::components::{ StandardAccountComponent, basic_fungible_faucet_library, basic_wallet_library, + fungible_token_metadata_library, multisig_library, multisig_psm_library, network_fungible_faucet_library, @@ -92,6 +93,11 @@ impl AccountInterfaceExt for AccountInterface { component_proc_digests .extend(basic_wallet_library().mast_forest().procedure_digests()); }, + AccountComponentInterface::FungibleTokenMetadata => { + component_proc_digests.extend( + fungible_token_metadata_library().mast_forest().procedure_digests(), + ); + }, AccountComponentInterface::BasicFungibleFaucet => { component_proc_digests .extend(basic_fungible_faucet_library().mast_forest().procedure_digests()); diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 3ddb633540..374908aad9 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -55,7 +55,7 @@ fn test_basic_wallet_default_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - BasicFungibleFaucet::new( + crate::account::faucets::FungibleTokenMetadata::new( TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), @@ -64,8 +64,9 @@ fn test_basic_wallet_default_notes() { None, None, ) - .expect("failed to create a fungible faucet component"), + .expect("failed to create token metadata"), ) + .with_component(BasicFungibleFaucet) .build_existing() .expect("failed to create wallet account"); let faucet_account_interface = AccountInterface::from_account(&faucet_account); @@ -325,7 +326,7 @@ fn test_basic_fungible_faucet_custom_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - BasicFungibleFaucet::new( + crate::account::faucets::FungibleTokenMetadata::new( TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), @@ -334,8 +335,9 @@ fn test_basic_fungible_faucet_custom_notes() { None, None, ) - .expect("failed to create a fungible faucet component"), + .expect("failed to create token metadata"), ) + .with_component(BasicFungibleFaucet) .build_existing() .expect("failed to create wallet account"); let faucet_account_interface = AccountInterface::from_account(&faucet_account); diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 99c5a1683a..e24ff76e20 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -63,9 +63,10 @@ //! .with_description(Description::new("A cool token").unwrap(), true) //! .with_logo_uri(LogoURI::new("https://example.com/logo.png").unwrap(), false); //! -//! let faucet = BasicFungibleFaucet::new(/* ... */).unwrap().with_info(info); +//! let metadata = FungibleTokenMetadata::new(/* ... */).unwrap(); //! let account = AccountBuilder::new(seed) -//! .with_component(faucet) +//! .with_component(metadata) +//! .with_component(BasicFungibleFaucet) //! .build()?; //! ``` @@ -490,8 +491,8 @@ impl TokenMetadata { } /// Converts [`TokenMetadata`] into a standalone [`AccountComponent`] that includes the metadata -/// MASM library (`metadata_info_component_library`). Use this when adding metadata as a separate -/// component alongside a faucet that does not embed info via `.with_info()`. +/// MASM library (`metadata_info_component_library`). Use this when adding generic metadata as a +/// separate component (e.g. for non-faucet accounts). impl From for AccountComponent { fn from(info: TokenMetadata) -> Self { let metadata = @@ -660,25 +661,35 @@ mod tests { mutability_config_slot, }; use crate::account::auth::{AuthSingleSig, NoAuth}; - use crate::account::faucets::{BasicFungibleFaucet, Description, TokenName}; + use crate::account::faucets::{ + BasicFungibleFaucet, + Description, + FungibleTokenMetadata, + TokenName, + }; - fn build_account_with_info(info: InfoType) -> Account { - let name = TokenName::new("T").unwrap(); - let faucet = BasicFungibleFaucet::new( + fn build_faucet_metadata( + name: TokenName, + description: Option, + ) -> FungibleTokenMetadata { + FungibleTokenMetadata::new( miden_protocol::asset::TokenSymbol::new("TST").unwrap(), 2, miden_protocol::Felt::new(1_000), name, - None, + description, None, None, ) .unwrap() - .with_info(info); + } + + fn build_account_with_metadata(metadata: FungibleTokenMetadata) -> Account { AccountBuilder::new([1u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .with_auth_component(NoAuth) - .with_component(faucet) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .build() .unwrap() } @@ -691,8 +702,8 @@ mod tests { let name_words = name.to_words(); let desc_words = description.to_words(); - let info = InfoType::new().with_name(name).with_description(description, false); - let account = build_account_with_info(info); + let metadata = build_faucet_metadata(name, Some(description)); + let account = build_account_with_metadata(metadata); let name_0 = account.storage().get_item(InfoType::name_chunk_0_slot()).unwrap(); let name_1 = account.storage().get_item(InfoType::name_chunk_1_slot()).unwrap(); @@ -707,17 +718,20 @@ mod tests { #[test] fn metadata_info_empty_works() { - let _account = build_account_with_info(InfoType::new()); + let name = TokenName::new("T").unwrap(); + let metadata = build_faucet_metadata(name, None); + let _account = build_account_with_metadata(metadata); } #[test] fn config_slots_set_correctly() { use miden_protocol::Felt; - let info = InfoType::new() - .with_description(Description::new("test").unwrap(), true) + let name = TokenName::new("T").unwrap(); + let metadata = build_faucet_metadata(name, Some(Description::new("test").unwrap())) + .with_description_mutable(true) .with_max_supply_mutable(true); - let account = build_account_with_info(info); + let account = build_account_with_metadata(metadata); let mut_word = account.storage().get_item(mutability_config_slot()).unwrap(); assert_eq!(mut_word[0], Felt::from(1u32), "desc_mutable should be 1"); @@ -725,7 +739,9 @@ mod tests { assert_eq!(mut_word[2], Felt::from(0u32), "extlink_mutable should be 0"); assert_eq!(mut_word[3], Felt::from(1u32), "max_supply_mutable should be 1"); - let account_default = build_account_with_info(InfoType::new()); + let name_default = TokenName::new("T").unwrap(); + let metadata_default = build_faucet_metadata(name_default, None); + let account_default = build_account_with_metadata(metadata_default); let mut_default = account_default.storage().get_item(mutability_config_slot()).unwrap(); assert_eq!(mut_default[0], Felt::from(0u32), "desc_mutable should be 0 by default"); assert_eq!(mut_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); @@ -773,8 +789,8 @@ mod tests { fn metadata_info_with_name() { let name = TokenName::new("My Token").unwrap(); let name_words = name.to_words(); - let info = InfoType::new().with_name(name); - let account = build_account_with_info(info); + let metadata = build_faucet_metadata(name, None); + let account = build_account_with_metadata(metadata); let name_0 = account.storage().get_item(InfoType::name_chunk_0_slot()).unwrap(); let name_1 = account.storage().get_item(InfoType::name_chunk_1_slot()).unwrap(); let decoded = TokenName::try_from_words(&[name_0, name_1]).unwrap(); diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index e1769515a4..7f91d89540 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -51,11 +51,11 @@ use miden_standards::account::faucets::{ BasicFungibleFaucet, Description, ExternalLink, + FungibleTokenMetadata, LogoURI, NetworkFungibleFaucet, TokenName, }; -use miden_standards::account::metadata::TokenMetadata; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote}; use miden_standards::testing::account_component::MockAccountComponent; @@ -335,7 +335,7 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; - let basic_faucet = BasicFungibleFaucet::new( + let metadata = FungibleTokenMetadata::new( token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt, @@ -344,12 +344,13 @@ impl MockChainBuilder { None, None, ) - .context("failed to create BasicFungibleFaucet")?; + .context("failed to create FungibleTokenMetadata")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) .account_type(AccountType::FungibleFaucet) - .with_component(basic_faucet); + .with_component(metadata) + .with_component(BasicFungibleFaucet); self.add_account_from_builder(auth_method, account_builder, AccountState::New) } @@ -371,7 +372,7 @@ impl MockChainBuilder { .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let basic_faucet = BasicFungibleFaucet::new( + let metadata = FungibleTokenMetadata::new( token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, @@ -380,12 +381,13 @@ impl MockChainBuilder { None, None, ) - .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create basic fungible faucet")?; + .and_then(|m| m.with_token_supply(token_supply)) + .context("failed to create fungible token metadata")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Public) - .with_component(basic_faucet) + .with_component(metadata) + .with_component(BasicFungibleFaucet) .account_type(AccountType::FungibleFaucet); self.add_account_from_builder(auth_method, account_builder, AccountState::Exists) @@ -408,9 +410,7 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let info = TokenMetadata::new().with_name(name.clone()); - - let network_faucet = NetworkFungibleFaucet::new( + let metadata = FungibleTokenMetadata::new( token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, @@ -419,13 +419,13 @@ impl MockChainBuilder { None, None, ) - .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")? - .with_info(info); + .and_then(|m| m.with_token_supply(token_supply)) + .context("failed to create fungible token metadata")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) - .with_component(network_faucet) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) .with_component(Ownable2Step::new(owner_account_id)) .account_type(AccountType::FungibleFaucet); @@ -455,45 +455,39 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let mut info = TokenMetadata::new() - .with_name(name.clone()) - .with_owner(owner_account_id) - .with_max_supply_mutable(max_supply_mutable); - if let Some((words, mutable)) = description { - info = info.with_description( - Description::try_from_words(&words).expect("valid description words"), - mutable, - ); - } - if let Some((words, mutable)) = logo_uri { - info = info.with_logo_uri( - LogoURI::try_from_words(&words).expect("valid logo_uri words"), - mutable, - ); - } - if let Some((words, mutable)) = external_link { - info = info.with_external_link( - ExternalLink::try_from_words(&words).expect("valid external_link words"), - mutable, - ); - } - - let network_faucet = NetworkFungibleFaucet::new( + let mut metadata = FungibleTokenMetadata::new( token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, name, - None, - None, - None, + description.map(|(words, _)| { + Description::try_from_words(&words).expect("valid description words") + }), + logo_uri + .map(|(words, _)| LogoURI::try_from_words(&words).expect("valid logo_uri words")), + external_link.map(|(words, _)| { + ExternalLink::try_from_words(&words).expect("valid external_link words") + }), ) - .and_then(|f| f.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")? - .with_info(info); + .and_then(|m| m.with_token_supply(token_supply)) + .context("failed to create fungible token metadata")? + .with_owner(owner_account_id) + .with_max_supply_mutable(max_supply_mutable); + + if let Some((_, mutable)) = description { + metadata = metadata.with_description_mutable(mutable); + } + if let Some((_, mutable)) = logo_uri { + metadata = metadata.with_logo_uri_mutable(mutable); + } + if let Some((_, mutable)) = external_link { + metadata = metadata.with_external_link_mutable(mutable); + } let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) - .with_component(network_faucet) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) .with_component(Ownable2Step::new(owner_account_id)) .account_type(AccountType::FungibleFaucet); diff --git a/crates/miden-testing/src/standards/mod.rs b/crates/miden-testing/src/standards/mod.rs index 76cf06a85d..b0f8c808a7 100644 --- a/crates/miden-testing/src/standards/mod.rs +++ b/crates/miden-testing/src/standards/mod.rs @@ -1,2 +1,3 @@ mod network_account_target; mod note_tag; +mod token_metadata; diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs new file mode 100644 index 0000000000..c40c2cf00c --- /dev/null +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -0,0 +1,1306 @@ +//! Integration tests for the Token Metadata standard (`FungibleTokenMetadata`). + +extern crate alloc; + +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_crypto::rand::RpoRandomCoin; +use miden_protocol::account::{ + AccountBuilder, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, +}; +use miden_protocol::assembly::DefaultSourceManager; +use miden_protocol::asset::TokenSymbol; +use miden_protocol::errors::MasmError; +use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::{Felt, Word}; +use miden_standards::account::access::Ownable2Step; +use miden_standards::account::auth::NoAuth; +use miden_standards::account::faucets::{ + BasicFungibleFaucet, + Description, + ExternalLink, + FungibleTokenMetadata, + LogoURI, + NetworkFungibleFaucet, + TokenName, +}; +use miden_standards::account::metadata::{ + DESCRIPTION_DATA_KEY, + EXTERNAL_LINK_DATA_KEY, + FieldBytesError, + LOGO_URI_DATA_KEY, + NAME_UTF8_MAX_BYTES, + TokenMetadata, + field_from_bytes, +}; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::errors::standards::{ + ERR_DESCRIPTION_NOT_MUTABLE, + ERR_EXTERNAL_LINK_NOT_MUTABLE, + ERR_LOGO_URI_NOT_MUTABLE, + ERR_MAX_SUPPLY_IMMUTABLE, + ERR_SENDER_NOT_OWNER, +}; +use miden_standards::testing::note::NoteBuilder; + +use crate::{MockChain, TransactionContextBuilder, assert_transaction_executor_error}; + +// SHARED HELPERS +// ================================================================================================ + +fn initial_field_data() -> [Word; 7] { + [ + Word::from([1u32, 2, 3, 4]), + Word::from([5u32, 6, 7, 8]), + Word::from([9u32, 10, 11, 12]), + Word::from([13u32, 14, 15, 16]), + Word::from([17u32, 18, 19, 20]), + Word::from([21u32, 22, 23, 24]), + Word::from([25u32, 26, 27, 28]), + ] +} + +fn new_field_data() -> [Word; 7] { + [ + Word::from([100u32, 101, 102, 103]), + Word::from([104u32, 105, 106, 107]), + Word::from([108u32, 109, 110, 111]), + Word::from([112u32, 113, 114, 115]), + Word::from([116u32, 117, 118, 119]), + Word::from([120u32, 121, 122, 123]), + Word::from([124u32, 125, 126, 127]), + ] +} + +fn owner_account_id() -> AccountId { + AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ) +} + +fn non_owner_account_id() -> AccountId { + AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ) +} + +/// Build a minimal faucet metadata (no optional fields). +fn build_faucet_metadata() -> FungibleTokenMetadata { + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + None, + ) + .unwrap() +} + +/// Build a standard POL faucet metadata (used by scalar getter tests). +fn build_pol_faucet_metadata() -> FungibleTokenMetadata { + FungibleTokenMetadata::new( + TokenSymbol::new("POL").unwrap(), + 8, + Felt::new(1_000_000), + TokenName::new("POL").unwrap(), + None, + None, + None, + ) + .unwrap() +} + +/// Build a basic faucet account with POL metadata. +fn build_pol_faucet_account() -> miden_protocol::account::Account { + AccountBuilder::new([4u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(build_pol_faucet_metadata()) + .with_component(BasicFungibleFaucet) + .build() + .unwrap() +} + +/// Flatten `[Word; 7]` into `Vec` for advice map values. +fn field_advice_map_value(field: &[Word; 7]) -> Vec { + let mut value = Vec::with_capacity(28); + for word in field.iter() { + value.extend(word.iter()); + } + value +} + +/// Execute a tx script against the given account and assert success. +async fn execute_tx_script( + account: miden_protocol::account::Account, + tx_script_code: impl AsRef, +) -> anyhow::Result<()> { + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(tx_script_code.as_ref())?; + let tx_context = TransactionContextBuilder::new(account) + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + tx_context.execute().await?; + Ok(()) +} + +// ================================================================================================= +// GETTER TESTS – name +// ================================================================================================= + +#[tokio::test] +async fn get_name_from_masm() -> anyhow::Result<()> { + let token_name = TokenName::new("test name").unwrap(); + let name = token_name.to_words(); + + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + token_name, + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{n0} + assert_eqw.err="name chunk 0 does not match" + push.{n1} + assert_eqw.err="name chunk 1 does not match" + end + "#, + n0 = name[0], + n1 = name[1], + ), + ) + .await +} + +#[tokio::test] +async fn get_name_zeros_returns_empty() -> anyhow::Result<()> { + // Build a faucet with an empty name to verify get_name returns zero words. + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("").unwrap(), + None, + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + r#" + begin + call.::miden::standards::metadata::fungible::get_name + padw assert_eqw.err="name chunk 0 should be empty" + padw assert_eqw.err="name chunk 1 should be empty" + end + "#, + ) + .await +} + +// ================================================================================================= +// GETTER TESTS – scalar fields +// ================================================================================================= + +#[tokio::test] +async fn faucet_get_decimals() -> anyhow::Result<()> { + let expected = Felt::from(8u8).as_canonical_u64(); + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_decimals + push.{expected} assert_eq.err="decimals does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_token_symbol() -> anyhow::Result<()> { + let expected = Felt::from(TokenSymbol::new("POL").unwrap()).as_canonical_u64(); + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_symbol + push.{expected} assert_eq.err="token_symbol does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_token_supply() -> anyhow::Result<()> { + execute_tx_script( + build_pol_faucet_account(), + r#" + begin + call.::miden::standards::metadata::fungible::get_token_supply + push.0 assert_eq.err="token_supply does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "#, + ) + .await +} + +#[tokio::test] +async fn faucet_get_max_supply() -> anyhow::Result<()> { + let expected = Felt::new(1_000_000).as_canonical_u64(); + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_max_supply + push.{expected} assert_eq.err="max_supply does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_token_metadata() -> anyhow::Result<()> { + let symbol = TokenSymbol::new("POL").unwrap(); + let expected_symbol = Felt::from(symbol).as_canonical_u64(); + let expected_decimals = Felt::from(8u8).as_canonical_u64(); + let expected_max_supply = Felt::new(1_000_000).as_canonical_u64(); + + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_token_metadata + push.0 assert_eq.err="token_supply does not match" + push.{expected_max_supply} assert_eq.err="max_supply does not match" + push.{expected_decimals} assert_eq.err="decimals does not match" + push.{expected_symbol} assert_eq.err="token_symbol does not match" + end + "# + ), + ) + .await +} + +#[tokio::test] +async fn faucet_get_decimals_symbol_and_max_supply() -> anyhow::Result<()> { + let symbol = TokenSymbol::new("POL").unwrap(); + let expected_decimals = Felt::from(8u8).as_canonical_u64(); + let expected_symbol = Felt::from(symbol).as_canonical_u64(); + let expected_max_supply = Felt::new(1_000_000).as_canonical_u64(); + + execute_tx_script( + build_pol_faucet_account(), + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_decimals + push.{expected_decimals} assert_eq.err="decimals does not match" + push.0 + call.::miden::standards::metadata::fungible::get_token_symbol + push.{expected_symbol} assert_eq.err="token_symbol does not match" + push.0 + call.::miden::standards::metadata::fungible::get_max_supply + push.{expected_max_supply} assert_eq.err="max_supply does not match" + end + "# + ), + ) + .await +} + +// ================================================================================================= +// GETTER TESTS – mutability config +// ================================================================================================= + +#[tokio::test] +async fn get_mutability_config() -> anyhow::Result<()> { + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(Description::new("test").unwrap()), + None, + None, + ) + .unwrap() + .with_description_mutable(true) + .with_max_supply_mutable(true); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + r#" + begin + call.::miden::standards::metadata::fungible::get_mutability_config + push.1 assert_eq.err="desc_mutable should be 1" + push.0 assert_eq.err="logo_mutable should be 0" + push.0 assert_eq.err="extlink_mutable should be 0" + push.1 assert_eq.err="max_supply_mutable should be 1" + end + "#, + ) + .await +} + +/// Tests all `is_*_mutable` procedures with flag=0 and flag=1. +#[tokio::test] +async fn is_field_mutable_checks() -> anyhow::Result<()> { + let desc = Description::new("test").unwrap(); + let logo = LogoURI::new("https://example.com/logo").unwrap(); + let link = ExternalLink::new("https://example.com").unwrap(); + + // (metadata_builder, proc_name, expected_value) + let cases: Vec<(FungibleTokenMetadata, &str, u8)> = vec![ + ( + build_faucet_metadata().with_max_supply_mutable(true), + "is_max_supply_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(desc.clone()), + None, + None, + ) + .unwrap() + .with_description_mutable(true), + "is_description_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(desc), + None, + None, + ) + .unwrap(), + "is_description_mutable", + 0, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + Some(logo.clone()), + None, + ) + .unwrap() + .with_logo_uri_mutable(true), + "is_logo_uri_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + Some(logo), + None, + ) + .unwrap(), + "is_logo_uri_mutable", + 0, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + Some(link.clone()), + ) + .unwrap() + .with_external_link_mutable(true), + "is_external_link_mutable", + 1, + ), + ( + FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + None, + None, + Some(link), + ) + .unwrap(), + "is_external_link_mutable", + 0, + ), + ]; + + for (metadata, proc_name, expected) in cases { + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + format!( + "begin + call.::miden::standards::metadata::fungible::{proc_name} + push.{expected} + assert_eq.err=\"{proc_name} returned unexpected value\" + end" + ), + ) + .await?; + } + + Ok(()) +} + +// ================================================================================================= +// GETTER TESTS – owner +// ================================================================================================= + +#[tokio::test] +async fn get_owner() -> anyhow::Result<()> { + let owner = owner_account_id(); + + let metadata = build_pol_faucet_metadata().with_owner(owner); + + let account = AccountBuilder::new([4u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(NetworkFungibleFaucet) + .build()?; + + let expected_prefix = owner.prefix().as_felt().as_canonical_u64(); + let expected_suffix = owner.suffix().as_canonical_u64(); + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_owner + push.{expected_suffix} assert_eq.err="owner suffix does not match" + push.{expected_prefix} assert_eq.err="owner prefix does not match" + push.0 assert_eq.err="clean stack: pad must be 0" + end + "# + ), + ) + .await +} + +// ================================================================================================= +// STORAGE LAYOUT TESTS +// ================================================================================================= + +#[test] +fn faucet_with_metadata_storage_layout() { + let token_name = TokenName::new("test faucet name").unwrap(); + let desc_text = "faucet description text for testing"; + let description = Description::new(desc_text).unwrap(); + let desc_words = description.to_words(); + + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 8, + Felt::new(1_000_000), + token_name, + Some(description), + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap(); + + // Verify faucet metadata + let faucet_metadata = + account.storage().get_item(FungibleTokenMetadata::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata[0], Felt::new(0)); + assert_eq!(faucet_metadata[1], Felt::new(1_000_000)); + assert_eq!(faucet_metadata[2], Felt::new(8)); + + // Verify description + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } +} + +#[test] +fn name_32_bytes_accepted() { + let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); + let token_name = TokenName::new(&max_name).unwrap(); + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + token_name, + None, + None, + None, + ) + .unwrap(); + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); + let decoded = miden_standards::account::metadata::name_to_utf8(&[name_0, name_1]).unwrap(); + assert_eq!(decoded, max_name); +} + +#[test] +fn name_33_bytes_rejected() { + let result = TokenName::new(&"a".repeat(33)); + assert!(matches!( + result, + Err(miden_standards::account::metadata::NameUtf8Error::TooLong(33)) + )); +} + +#[test] +fn description_7_words_full_capacity() { + let desc_text = "a".repeat(Description::MAX_BYTES); + let description = Description::new(&desc_text).unwrap(); + let desc_words = description.to_words(); + let metadata = FungibleTokenMetadata::new( + "TST".try_into().unwrap(), + 2, + Felt::new(1_000), + TokenName::new("T").unwrap(), + Some(description), + None, + None, + ) + .unwrap(); + let account = AccountBuilder::new([1u8; 32]) + .account_type(AccountType::FungibleFaucet) + .with_auth_component(NoAuth) + .with_component(metadata) + .with_component(BasicFungibleFaucet) + .build() + .unwrap(); + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } +} + +#[test] +fn field_over_max_bytes_rejected() { + use miden_standards::account::metadata::FIELD_MAX_BYTES; + let over = FIELD_MAX_BYTES + 1; + let result = field_from_bytes("a".repeat(over).as_bytes()); + assert!(matches!(result, Err(FieldBytesError::TooLong(n)) if n == over)); +} + +// ================================================================================================= +// FAUCET INITIALIZATION – basic + network with max name/description +// ================================================================================================= + +fn verify_faucet_with_max_name_and_description( + seed: [u8; 32], + symbol: &str, + max_supply: u64, + storage_mode: AccountStorageMode, + extra_components: Vec, +) { + let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); + let desc_text = "a".repeat(Description::MAX_BYTES); + let description = Description::new(&desc_text).unwrap(); + let desc_words = description.to_words(); + + let faucet_metadata = FungibleTokenMetadata::new( + symbol.try_into().unwrap(), + 6, + Felt::new(max_supply), + TokenName::new(&max_name).unwrap(), + Some(description), + None, + None, + ) + .unwrap(); + + let mut builder = AccountBuilder::new(seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(storage_mode) + .with_auth_component(NoAuth) + .with_component(faucet_metadata); + + for comp in extra_components { + builder = builder.with_component(comp); + } + + let account = builder.build().unwrap(); + + let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); + let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); + assert_eq!(name_0, name_words[0]); + assert_eq!(name_1, name_words[1]); + for (i, expected) in desc_words.iter().enumerate() { + let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); + assert_eq!(chunk, *expected); + } + let faucet_metadata_val = + account.storage().get_item(FungibleTokenMetadata::metadata_slot()).unwrap(); + assert_eq!(faucet_metadata_val[1], Felt::new(max_supply)); +} + +#[test] +fn basic_faucet_with_max_name_and_full_description() { + verify_faucet_with_max_name_and_description( + [5u8; 32], + "MAX", + 1_000_000, + AccountStorageMode::Public, + vec![BasicFungibleFaucet.into()], + ); +} + +#[test] +fn network_faucet_with_max_name_and_full_description() { + verify_faucet_with_max_name_and_description( + [6u8; 32], + "NET", + 2_000_000, + AccountStorageMode::Network, + vec![NetworkFungibleFaucet.into(), Ownable2Step::new(owner_account_id()).into()], + ); +} + +// ================================================================================================= +// MASM NAME READBACK – basic + network faucets +// ================================================================================================= + +#[tokio::test] +async fn basic_faucet_name_readable_from_masm() -> anyhow::Result<()> { + let token_name = TokenName::new("readable name").unwrap(); + let name = token_name.to_words(); + + let faucet_metadata = FungibleTokenMetadata::new( + "MAS".try_into().unwrap(), + 10, + Felt::new(999_999), + token_name, + Some(Description::new("readable description").unwrap()), + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([3u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(NoAuth) + .with_component(faucet_metadata) + .with_component(BasicFungibleFaucet) + .build()?; + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{n0} + assert_eqw.err="faucet name chunk 0 does not match" + push.{n1} + assert_eqw.err="faucet name chunk 1 does not match" + end + "#, + n0 = name[0], + n1 = name[1], + ), + ) + .await +} + +#[tokio::test] +async fn network_faucet_name_readable_from_masm() -> anyhow::Result<()> { + let max_name = "b".repeat(NAME_UTF8_MAX_BYTES); + let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + + let network_faucet_metadata = FungibleTokenMetadata::new( + "MAS".try_into().unwrap(), + 6, + Felt::new(1_000_000), + TokenName::new(&max_name).unwrap(), + Some(Description::new("network faucet description").unwrap()), + None, + None, + ) + .unwrap(); + + let account = AccountBuilder::new([7u8; 32]) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Network) + .with_auth_component(NoAuth) + .with_component(network_faucet_metadata) + .with_component(NetworkFungibleFaucet) + .with_component(Ownable2Step::new(owner_account_id())) + .build()?; + + execute_tx_script( + account, + format!( + r#" + begin + call.::miden::standards::metadata::fungible::get_name + push.{n0} + assert_eqw.err="network faucet name chunk 0 does not match" + push.{n1} + assert_eqw.err="network faucet name chunk 1 does not match" + end + "#, + n0 = name_words[0], + n1 = name_words[1], + ), + ) + .await +} + +// ================================================================================================= +// SETTER TESTS – set_description, set_logo_uri, set_external_link (parameterised) +// ================================================================================================= + +struct FieldSetterFaucetArgs { + description: Option<([Word; 7], bool)>, + logo_uri: Option<([Word; 7], bool)>, + external_link: Option<([Word; 7], bool)>, +} + +fn description_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs { + FieldSetterFaucetArgs { + description: Some((data, mutable)), + logo_uri: None, + external_link: None, + } +} + +fn logo_uri_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs { + FieldSetterFaucetArgs { + description: None, + logo_uri: Some((data, mutable)), + external_link: None, + } +} + +fn external_link_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs { + FieldSetterFaucetArgs { + description: None, + logo_uri: None, + external_link: Some((data, mutable)), + } +} + +async fn test_field_setter_immutable_fails( + proc_name: &str, + advice_key: Word, + immutable_error: MasmError, + args: FieldSetterFaucetArgs, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let new_data = new_field_data(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "FLD", + 1000, + owner, + Some(0), + false, + args.description, + args.logo_uri, + args.external_link, + )?; + let mock_chain = builder.build()?; + + let tx_script_code = format!( + r#" + begin + call.::miden::standards::metadata::fungible::{proc_name} + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(&tx_script_code)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .extend_advice_map([(advice_key, field_advice_map_value(&new_data))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, immutable_error); + + Ok(()) +} + +async fn test_field_setter_owner_succeeds( + proc_name: &str, + advice_key: Word, + args: FieldSetterFaucetArgs, + slot_fn: fn(usize) -> &'static miden_protocol::account::StorageSlotName, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let new_data = new_field_data(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "FLD", + 1000, + owner, + Some(0), + false, + args.description, + args.logo_uri, + args.external_link, + )?; + let mock_chain = builder.build()?; + + let note_script_code = format!( + r#" + begin + call.::miden::standards::metadata::fungible::{proc_name} + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let note = NoteBuilder::new(owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([7, 8, 9, 10u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .extend_advice_map([(advice_key, field_advice_map_value(&new_data))]) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + for (i, expected) in new_data.iter().enumerate() { + let chunk = updated_faucet.storage().get_item(slot_fn(i))?; + assert_eq!(chunk, *expected, "field chunk {i} should be updated"); + } + + Ok(()) +} + +async fn test_field_setter_non_owner_fails( + proc_name: &str, + advice_key: Word, + args: FieldSetterFaucetArgs, +) -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let non_owner = non_owner_account_id(); + let new_data = new_field_data(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "FLD", + 1000, + owner, + Some(0), + false, + args.description, + args.logo_uri, + args.external_link, + )?; + let mock_chain = builder.build()?; + + let note_script_code = format!( + r#" + begin + call.::miden::standards::metadata::fungible::{proc_name} + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let note = NoteBuilder::new(non_owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([11, 12, 13, 14u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .extend_advice_map([(advice_key, field_advice_map_value(&new_data))]) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} + +// --- set_description --- + +#[tokio::test] +async fn set_description_immutable_fails() -> anyhow::Result<()> { + test_field_setter_immutable_fails( + "set_description", + DESCRIPTION_DATA_KEY, + ERR_DESCRIPTION_NOT_MUTABLE, + description_config(initial_field_data(), false), + ) + .await +} + +#[tokio::test] +async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { + test_field_setter_owner_succeeds( + "set_description", + DESCRIPTION_DATA_KEY, + description_config(initial_field_data(), true), + TokenMetadata::description_slot, + ) + .await +} + +#[tokio::test] +async fn set_description_mutable_non_owner_fails() -> anyhow::Result<()> { + test_field_setter_non_owner_fails( + "set_description", + DESCRIPTION_DATA_KEY, + description_config(initial_field_data(), true), + ) + .await +} + +// --- set_logo_uri --- + +#[tokio::test] +async fn set_logo_uri_immutable_fails() -> anyhow::Result<()> { + test_field_setter_immutable_fails( + "set_logo_uri", + LOGO_URI_DATA_KEY, + ERR_LOGO_URI_NOT_MUTABLE, + logo_uri_config(initial_field_data(), false), + ) + .await +} + +#[tokio::test] +async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { + test_field_setter_owner_succeeds( + "set_logo_uri", + LOGO_URI_DATA_KEY, + logo_uri_config(initial_field_data(), true), + TokenMetadata::logo_uri_slot, + ) + .await +} + +#[tokio::test] +async fn set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { + test_field_setter_non_owner_fails( + "set_logo_uri", + LOGO_URI_DATA_KEY, + logo_uri_config(initial_field_data(), true), + ) + .await +} + +// --- set_external_link --- + +#[tokio::test] +async fn set_external_link_immutable_fails() -> anyhow::Result<()> { + test_field_setter_immutable_fails( + "set_external_link", + EXTERNAL_LINK_DATA_KEY, + ERR_EXTERNAL_LINK_NOT_MUTABLE, + external_link_config(initial_field_data(), false), + ) + .await +} + +#[tokio::test] +async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { + test_field_setter_owner_succeeds( + "set_external_link", + EXTERNAL_LINK_DATA_KEY, + external_link_config(initial_field_data(), true), + TokenMetadata::external_link_slot, + ) + .await +} + +#[tokio::test] +async fn set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { + test_field_setter_non_owner_fails( + "set_external_link", + EXTERNAL_LINK_DATA_KEY, + external_link_config(initial_field_data(), true), + ) + .await +} + +// ================================================================================================= +// SETTER TESTS – set_max_supply +// ================================================================================================= + +#[tokio::test] +async fn set_max_supply_immutable_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner, + Some(0), + false, + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let tx_script_code = r#" + begin + push.2000 + call.::miden::standards::metadata::fungible::set_max_supply + end + "#; + + let source_manager = Arc::new(DefaultSourceManager::default()); + let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_tx_script(tx_script_code)?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[])? + .tx_script(tx_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_MAX_SUPPLY_IMMUTABLE); + + Ok(()) +} + +#[tokio::test] +async fn set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let new_max_supply: u64 = 2000; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner, + Some(0), + true, + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let note_script_code = format!( + r#" + begin + push.{new_max_supply} + swap drop + call.::miden::standards::metadata::fungible::set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); + let note = NoteBuilder::new(owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([20, 21, 22, 23u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .with_source_manager(source_manager) + .build()?; + + let executed = tx_context.execute().await?; + let mut updated_faucet = faucet.clone(); + updated_faucet.apply_delta(executed.account_delta())?; + + let metadata_word = updated_faucet.storage().get_item(FungibleTokenMetadata::metadata_slot())?; + assert_eq!(metadata_word[1], Felt::new(new_max_supply), "max_supply should be updated"); + assert_eq!(metadata_word[0], Felt::new(0), "token_supply should remain unchanged"); + + Ok(()) +} + +#[tokio::test] +async fn set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + let owner = owner_account_id(); + let non_owner = non_owner_account_id(); + let new_max_supply: u64 = 2000; + + let faucet = builder.add_existing_network_faucet_with_metadata_info( + "MSM", + 1000, + owner, + Some(0), + true, + None, + None, + None, + )?; + let mock_chain = builder.build()?; + + let note_script_code = format!( + r#" + begin + push.{new_max_supply} + swap drop + call.::miden::standards::metadata::fungible::set_max_supply + end + "# + ); + + let source_manager = Arc::new(DefaultSourceManager::default()); + let note_script = CodeBuilder::with_source_manager(source_manager.clone()) + .compile_note_script(¬e_script_code)?; + + let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); + let note = NoteBuilder::new(non_owner, &mut rng) + .note_type(NoteType::Private) + .tag(NoteTag::default().into()) + .serial_number(Word::from([30, 31, 32, 33u32])) + .code(¬e_script_code) + .build()?; + + let tx_context = mock_chain + .build_tx_context(faucet.id(), &[], &[note])? + .add_note_script(note_script) + .with_source_manager(source_manager) + .build()?; + + let result = tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); + + Ok(()) +} diff --git a/crates/miden-testing/tests/lib.rs b/crates/miden-testing/tests/lib.rs index 880ed28bf0..b27b9a00d0 100644 --- a/crates/miden-testing/tests/lib.rs +++ b/crates/miden-testing/tests/lib.rs @@ -2,7 +2,6 @@ extern crate alloc; mod agglayer; mod auth; -mod metadata; mod scripts; mod wallet; diff --git a/crates/miden-testing/tests/metadata.rs b/crates/miden-testing/tests/metadata.rs deleted file mode 100644 index 409852e303..0000000000 --- a/crates/miden-testing/tests/metadata.rs +++ /dev/null @@ -1,1981 +0,0 @@ -//! Integration tests for the Metadata Extension component. - -extern crate alloc; - -use alloc::sync::Arc; - -use miden_crypto::hash::poseidon2::Poseidon2 as Hasher; -use miden_crypto::rand::RpoRandomCoin; -use miden_protocol::account::{ - AccountBuilder, - AccountId, - AccountIdVersion, - AccountStorageMode, - AccountType, -}; -use miden_protocol::assembly::DefaultSourceManager; -use miden_protocol::note::{NoteTag, NoteType}; -use miden_protocol::{Felt, Word}; -use miden_standards::account::access::Ownable2Step; -use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::{ - BasicFungibleFaucet, - Description, - ExternalLink, - LogoURI, - TokenName, -}; -use miden_standards::account::metadata::{ - DESCRIPTION_DATA_KEY, - EXTERNAL_LINK_DATA_KEY, - FieldBytesError, - LOGO_URI_DATA_KEY, - NAME_UTF8_MAX_BYTES, - TokenMetadata, - field_from_bytes, - mutability_config_slot, -}; -use miden_standards::code_builder::CodeBuilder; -use miden_standards::errors::standards::{ - ERR_DESCRIPTION_NOT_MUTABLE, - ERR_EXTERNAL_LINK_NOT_MUTABLE, - ERR_LOGO_URI_NOT_MUTABLE, - ERR_MAX_SUPPLY_IMMUTABLE, - ERR_SENDER_NOT_OWNER, -}; -use miden_standards::testing::note::NoteBuilder; -use miden_testing::{MockChain, TransactionContextBuilder, assert_transaction_executor_error}; - -fn build_faucet_with_info(info: TokenMetadata) -> BasicFungibleFaucet { - BasicFungibleFaucet::new( - "TST".try_into().unwrap(), - 2, - Felt::new(1_000), - TokenName::new("T").unwrap(), - None, - None, - None, - ) - .unwrap() - .with_info(info) -} - -/// Tests that the metadata extension can store and retrieve name via MASM. -#[tokio::test] -async fn metadata_info_get_name_from_masm() -> anyhow::Result<()> { - let token_name = TokenName::new("test name").unwrap(); - let name = token_name.to_words(); - - let extension = TokenMetadata::new().with_name(token_name); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - // MASM script to read name and verify values - let tx_script = format!( - r#" - begin - # Get name (returns [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)]) - call.::miden::standards::metadata::fungible::get_name - # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] - - # Verify chunk 0 (on top) - push.{expected_name_0} - assert_eqw.err="name chunk 0 does not match" - # => [NAME_CHUNK_1, pad(12)] - - # Verify chunk 1 - push.{expected_name_1} - assert_eqw.err="name chunk 1 does not match" - # => [pad(16)] - end - "#, - expected_name_0 = name[0], - expected_name_1 = name[1], - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Tests that reading zero-valued name returns empty words. -#[tokio::test] -async fn metadata_info_get_name_zeros_returns_empty() -> anyhow::Result<()> { - let extension = TokenMetadata::new(); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let tx_script = r#" - begin - call.::miden::standards::metadata::fungible::get_name - # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] - padw assert_eqw.err="name chunk 0 should be empty" - padw assert_eqw.err="name chunk 1 should be empty" - end - "# - .to_string(); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Tests that the metadata extension works alongside a fungible faucet. -#[test] -fn metadata_info_with_faucet_storage() { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - - let token_name = TokenName::new("test faucet name").unwrap(); - let name = token_name.to_words(); - let desc_text = "faucet description text for testing"; - let description_typed = Description::new(desc_text).unwrap(); - let description = description_typed.to_words(); - - let extension = TokenMetadata::new().with_name(token_name).with_description(description_typed, false); - - let faucet = BasicFungibleFaucet::new( - "TST".try_into().unwrap(), - 8, // decimals - Felt::new(1_000_000), // max_supply - TokenName::new("TST").unwrap(), - None, - None, - None, - ) - .unwrap() - .with_info(extension); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build() - .unwrap(); - - // Verify faucet metadata is intact (Word layout: [token_supply, max_supply, decimals, symbol]) - let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); - assert_eq!(faucet_metadata[0], Felt::new(0)); // token_supply - assert_eq!(faucet_metadata[1], Felt::new(1_000_000)); // max_supply - assert_eq!(faucet_metadata[2], Felt::new(8)); // decimals - - // Verify name - let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); - assert_eq!(name_0, name[0]); - assert_eq!(name_1, name[1]); - - // Verify description - for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); - assert_eq!(chunk, *expected); - } -} - -/// Tests that a name at the maximum allowed length (32 bytes, 2 slots) is accepted. -#[test] -fn name_32_bytes_accepted() { - let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); - let token_name = TokenName::new(&max_name).unwrap(); - let extension = TokenMetadata::new().with_name(token_name); - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build() - .unwrap(); - let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); - let decoded = miden_standards::account::metadata::name_to_utf8(&[name_0, name_1]).unwrap(); - assert_eq!(decoded, max_name); -} - -/// Tests that a name longer than the maximum (33 bytes) is rejected. -#[test] -fn name_33_bytes_rejected() { - let too_long = "a".repeat(33); - let result = TokenName::new(&too_long); - assert!(result.is_err()); - assert!(matches!( - result, - Err(miden_standards::account::metadata::NameUtf8Error::TooLong(33)) - )); -} - -/// Tests that description at full capacity (7 Words) is supported. -#[test] -fn description_7_words_full_capacity() { - let desc_text = "a".repeat(Description::MAX_BYTES); - let description_typed = Description::new(&desc_text).unwrap(); - let description = description_typed.to_words(); - let extension = TokenMetadata::new().with_description(description_typed, false); - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build() - .unwrap(); - for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); - assert_eq!(chunk, *expected); - } -} - -/// Tests that a field exceeding [`FIELD_MAX_BYTES`] is rejected. -#[test] -fn field_over_max_bytes_rejected() { - use miden_standards::account::metadata::FIELD_MAX_BYTES; - let over = FIELD_MAX_BYTES + 1; - let long_string = "a".repeat(over); - let result = field_from_bytes(long_string.as_bytes()); - assert!(result.is_err()); - assert!(matches!(result, Err(FieldBytesError::TooLong(n)) if n == over)); -} - -/// Tests that BasicFungibleFaucet with TokenMetadata component (name/description) works correctly. -#[test] -fn faucet_with_integrated_metadata() { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - - let token_name = TokenName::new("integrated name").unwrap(); - let name = token_name.to_words(); - let desc_text = "integrated description text"; - let description_typed = Description::new(desc_text).unwrap(); - let description = description_typed.to_words(); - - let faucet = BasicFungibleFaucet::new( - "INT".try_into().unwrap(), - 6, // decimals - Felt::new(500_000), // max_supply - TokenName::new("INT").unwrap(), - None, - None, - None, - ) - .unwrap(); - let extension = TokenMetadata::new().with_name(token_name).with_description(description_typed, false); - - let account = AccountBuilder::new([2u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet.with_info(extension)) - .build() - .unwrap(); - - // Verify faucet metadata is intact (Word layout: [token_supply, max_supply, decimals, symbol]) - let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); - assert_eq!(faucet_metadata[0], Felt::new(0)); // token_supply - assert_eq!(faucet_metadata[1], Felt::new(500_000)); // max_supply - assert_eq!(faucet_metadata[2], Felt::new(6)); // decimals - - // Verify name - let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); - assert_eq!(name_0, name[0]); - assert_eq!(name_1, name[1]); - - // Verify description - for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); - assert_eq!(chunk, *expected); - } - - // Verify the faucet can be recovered from the account (metadata only; name/desc are in TokenMetadata) - let recovered_faucet = BasicFungibleFaucet::try_from(&account).unwrap(); - assert_eq!(recovered_faucet.max_supply(), Felt::new(500_000)); - assert_eq!(recovered_faucet.decimals(), 6); -} - -/// Tests initializing a fungible faucet with maximum-length name and full description. -#[test] -fn faucet_initialized_with_max_name_and_full_description() { - use miden_protocol::account::AccountStorageMode; - - let max_name = "0".repeat(NAME_UTF8_MAX_BYTES); - let desc_text = "a".repeat(Description::MAX_BYTES); - let description_typed = Description::new(&desc_text).unwrap(); - let description = description_typed.to_words(); - - let faucet = BasicFungibleFaucet::new( - "MAX".try_into().unwrap(), - 6, - Felt::new(1_000_000), - TokenName::new("MAX").unwrap(), - None, - None, - None, - ) - .unwrap(); - let extension = TokenMetadata::new() - .with_name(TokenName::new(&max_name).unwrap()) - .with_description(description_typed, false); - - let account = AccountBuilder::new([5u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet.with_info(extension)) - .build() - .unwrap(); - - let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); - let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); - assert_eq!(name_0, name_words[0]); - assert_eq!(name_1, name_words[1]); - for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); - assert_eq!(chunk, *expected); - } - let faucet_metadata = account.storage().get_item(BasicFungibleFaucet::metadata_slot()).unwrap(); - assert_eq!(faucet_metadata[1], Felt::new(1_000_000)); -} - -/// Tests initializing a network fungible faucet with max name and full description. -#[test] -fn network_faucet_initialized_with_max_name_and_full_description() { - use miden_protocol::account::AccountStorageMode; - use miden_standards::account::faucets::NetworkFungibleFaucet; - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); - let desc_text = "a".repeat(Description::MAX_BYTES); - let description_typed = Description::new(&desc_text).unwrap(); - let description = description_typed.to_words(); - - let network_faucet = NetworkFungibleFaucet::new( - "NET".try_into().unwrap(), - 6, - Felt::new(2_000_000), - TokenName::new("NET").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let extension = TokenMetadata::new() - .with_name(TokenName::new(&max_name).unwrap()) - .with_description(description_typed, false); - - let account = AccountBuilder::new([6u8; 32]) - .account_type(AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Network) - .with_auth_component(NoAuth) - .with_component(network_faucet.with_info(extension)) - .with_component(Ownable2Step::new(owner_account_id)) - .build() - .unwrap(); - - let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); - let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); - let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); - assert_eq!(name_0, name_words[0]); - assert_eq!(name_1, name_words[1]); - for (i, expected) in description.iter().enumerate() { - let chunk = account.storage().get_item(TokenMetadata::description_slot(i)).unwrap(); - assert_eq!(chunk, *expected); - } - let faucet_metadata = - account.storage().get_item(NetworkFungibleFaucet::metadata_slot()).unwrap(); - assert_eq!(faucet_metadata[1], Felt::new(2_000_000)); -} - -/// Tests that a network fungible faucet with description can be read from MASM. -#[tokio::test] -async fn network_faucet_get_name_and_description_from_masm() -> anyhow::Result<()> { - use miden_protocol::account::AccountStorageMode; - use miden_standards::account::faucets::NetworkFungibleFaucet; - - let owner_account_id = AccountId::dummy( - [2; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let max_name = "b".repeat(NAME_UTF8_MAX_BYTES); - let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); - let desc_text = "network faucet description"; - let description_typed = Description::new(desc_text).unwrap(); - - let network_faucet = NetworkFungibleFaucet::new( - "MAS".try_into().unwrap(), - 6, - Felt::new(1_000_000), - TokenName::new("MAS").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let extension = TokenMetadata::new() - .with_name(TokenName::new(&max_name).unwrap()) - .with_description(description_typed, false); - - let account = AccountBuilder::new([7u8; 32]) - .account_type(AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Network) - .with_auth_component(NoAuth) - .with_component(network_faucet.with_info(extension)) - .with_component(Ownable2Step::new(owner_account_id)) - .build()?; - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_name - push.{expected_name_0} - assert_eqw.err="network faucet name chunk 0 does not match" - push.{expected_name_1} - assert_eqw.err="network faucet name chunk 1 does not match" - end - "#, - expected_name_0 = name_words[0], - expected_name_1 = name_words[1], - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: only get_decimals. -#[tokio::test] -async fn faucet_get_decimals_only() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - - let faucet = BasicFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build()?; - - let expected_decimals = Felt::from(decimals).as_canonical_u64(); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_decimals - push.{expected_decimals} - assert_eq.err="decimals does not match" - push.0 - assert_eq.err="clean stack: pad must be 0" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: only get_token_symbol. -#[tokio::test] -async fn faucet_get_token_symbol_only() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - - let faucet = BasicFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build()?; - - let expected_symbol = Felt::from(token_symbol).as_canonical_u64(); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_token_symbol - push.{expected_symbol} - assert_eq.err="token_symbol does not match" - push.0 - assert_eq.err="clean stack: pad must be 0" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: only get_token_supply. -#[tokio::test] -async fn faucet_get_token_supply_only() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - let token_supply = Felt::new(0); // initial supply - - let faucet = BasicFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build()?; - - let expected_token_supply = token_supply.as_canonical_u64(); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_token_supply - push.{expected_token_supply} - assert_eq.err="token_supply does not match" - push.0 - assert_eq.err="clean stack: pad must be 0" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: only get_token_metadata (full word). -#[tokio::test] -async fn faucet_get_token_metadata_only() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - - let faucet = BasicFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build()?; - - let expected_symbol = Felt::from(token_symbol).as_canonical_u64(); - let expected_decimals = Felt::from(decimals).as_canonical_u64(); - let expected_max_supply = max_supply.as_canonical_u64(); - let expected_token_supply = 0u64; - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_token_metadata - # => [token_supply, max_supply, decimals, token_symbol, pad(12)] - push.{expected_token_supply} assert_eq.err="token_supply does not match" - push.{expected_max_supply} assert_eq.err="max_supply does not match" - push.{expected_decimals} assert_eq.err="decimals does not match" - push.{expected_symbol} assert_eq.err="token_symbol does not match" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: get_mutability_config. -#[tokio::test] -async fn metadata_get_config_only() -> anyhow::Result<()> { - let extension = TokenMetadata::new() - .with_description(Description::new("test").unwrap(), true) // mutable - .with_max_supply_mutable(true); - - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(extension)) - .build()?; - - let tx_script = r#" - begin - # Check mutability config - call.::miden::standards::metadata::fungible::get_mutability_config - # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] - push.1 - assert_eq.err="desc_mutable should be 1" - push.0 - assert_eq.err="logo_mutable should be 0" - push.0 - assert_eq.err="extlink_mutable should be 0" - push.1 - assert_eq.err="max_supply_mutable should be 1" - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: only get_owner (account must have ownable, e.g. NetworkFungibleFaucet). -#[tokio::test] -async fn metadata_get_owner_only() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - use miden_standards::account::faucets::NetworkFungibleFaucet; - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - miden_protocol::account::AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - - let faucet = NetworkFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - // TokenMetadata provides the ownable::owner_config slot (single-step) that metadata::fungible::get_owner - // reads from, plus the metadata library. NetworkFungibleFaucet uses ownable2step::owner_config - // (different slot name), so there's no conflict. - let info = TokenMetadata::new() - .with_name(TokenName::new("POL").unwrap()) - .with_owner(owner_account_id); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .with_component(info) - .build()?; - - let expected_prefix = owner_account_id.prefix().as_felt().as_canonical_u64(); - let expected_suffix = owner_account_id.suffix().as_canonical_u64(); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_owner - # => [owner_suffix, owner_prefix, pad(14)] - push.{expected_suffix} - assert_eq.err="owner suffix does not match" - push.{expected_prefix} - assert_eq.err="owner prefix does not match" - push.0 - assert_eq.err="clean stack: pad must be 0" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Isolated test: only get_max_supply. -#[tokio::test] -async fn faucet_get_max_supply_only() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - - let faucet = BasicFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build()?; - - let expected_max_supply = max_supply.as_canonical_u64(); - - let tx_script = format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_max_supply - push.{expected_max_supply} - assert_eq.err="max_supply does not match" - push.0 - assert_eq.err="clean stack: pad must be 0" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Tests that get_decimals and get_token_symbol return the correct individual values from MASM. -#[tokio::test] -async fn faucet_get_decimals_and_symbol_from_masm() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - use miden_protocol::asset::TokenSymbol; - - let token_symbol = TokenSymbol::new("POL").unwrap(); - let decimals: u8 = 8; - let max_supply = Felt::new(1_000_000); - - let faucet = BasicFungibleFaucet::new( - token_symbol, - decimals, - max_supply, - TokenName::new("POL").unwrap(), - None, - None, - None, - ) - .unwrap(); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet) - .build()?; - - // Compute expected felt values - let expected_decimals = Felt::from(decimals).as_canonical_u64(); - let expected_symbol = Felt::from(token_symbol).as_canonical_u64(); - let expected_max_supply = max_supply.as_canonical_u64(); - - let tx_script = format!( - r#" - begin - # Test get_decimals - call.::miden::standards::metadata::fungible::get_decimals - # => [decimals, pad(15)] - push.{expected_decimals} - assert_eq.err="decimals does not match" - # => [pad(15)]; pad to 16 before next call - push.0 - - # Test get_token_symbol - call.::miden::standards::metadata::fungible::get_token_symbol - # => [token_symbol, pad(15)] - push.{expected_symbol} - assert_eq.err="token_symbol does not match" - # => [pad(15)]; pad to 16 before next call - push.0 - - # Test get_max_supply (sanity check) - call.::miden::standards::metadata::fungible::get_max_supply - # => [max_supply, pad(15)] - push.{expected_max_supply} - assert_eq.err="max_supply does not match" - end - "#, - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -/// Tests that BasicFungibleFaucet metadata can be read from MASM using the faucet's procedures. -#[tokio::test] -async fn faucet_metadata_readable_from_masm() -> anyhow::Result<()> { - use miden_protocol::Felt; - use miden_protocol::account::AccountStorageMode; - - let token_name = TokenName::new("readable name").unwrap(); - let name = token_name.to_words(); - let desc_text = "readable description"; - let description_typed = Description::new(desc_text).unwrap(); - - let faucet = BasicFungibleFaucet::new( - "MAS".try_into().unwrap(), - 10, // decimals - Felt::new(999_999), // max_supply - TokenName::new("MAS").unwrap(), - None, - None, - None, - ) - .unwrap(); - let extension = TokenMetadata::new().with_name(token_name).with_description(description_typed, false); - - let account = AccountBuilder::new([3u8; 32]) - .account_type(miden_protocol::account::AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(faucet.with_info(extension)) - .build()?; - - // MASM script to read name via the metadata procedures and verify - let tx_script = format!( - r#" - begin - # Get name and verify - call.::miden::standards::metadata::fungible::get_name - # => [NAME_CHUNK_0, NAME_CHUNK_1, pad(8)] - push.{expected_name_0} - assert_eqw.err="faucet name chunk 0 does not match" - push.{expected_name_1} - assert_eqw.err="faucet name chunk 1 does not match" - end - "#, - expected_name_0 = name[0], - expected_name_1 = name[1], - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - - Ok(()) -} - -// ================================================================================================= -// set_description: mutable flag and verify_owner -// ================================================================================================= - -/// Builds the advice map value for field setters. -fn field_advice_map_value(field: &[Word; 7]) -> Vec { - let mut value = Vec::with_capacity(28); - for word in field.iter() { - value.extend(word.iter()); - } - value -} - -/// When description mutable flag is 0 (immutable), set_description panics. -#[tokio::test] -async fn set_description_immutable_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let description = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "DSC", - 1000, - owner_account_id, - Some(0), - false, - Some((description, false)), // immutable - None, - None, - )?; - let mock_chain = builder.build()?; - - let new_desc = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let tx_script = r#" - begin - call.::miden::standards::metadata::fungible::set_description - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[])? - .tx_script(tx_script) - .extend_advice_map([(DESCRIPTION_DATA_KEY, field_advice_map_value(&new_desc))]) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_DESCRIPTION_NOT_MUTABLE); - - Ok(()) -} - -/// When description mutable flag is 1 and note sender is the owner, set_description -/// succeeds. -#[tokio::test] -async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_desc = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let new_desc = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "DSC", - 1000, - owner_account_id, - Some(0), - false, - Some((initial_desc, true)), // mutable - None, - None, - )?; - let mock_chain = builder.build()?; - - let committed = mock_chain.committed_account(faucet.id())?; - let mut_word = committed.storage().get_item(mutability_config_slot())?; - assert_eq!(mut_word[0], Felt::from(1u32), "committed account must have desc_mutable = 1"); - - let set_desc_note_script_code = r#" - begin - call.::miden::standards::metadata::fungible::set_description - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_desc_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(set_desc_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); - let set_desc_note = NoteBuilder::new(owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([7, 8, 9, 10u32])) - .code(set_desc_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_desc_note])? - .add_note_script(set_desc_note_script) - .extend_advice_map([(DESCRIPTION_DATA_KEY, field_advice_map_value(&new_desc))]) - .with_source_manager(source_manager) - .build()?; - - let executed = tx_context.execute().await?; - let mut updated_faucet = faucet.clone(); - updated_faucet.apply_delta(executed.account_delta())?; - - for (i, expected) in new_desc.iter().enumerate() { - let chunk = updated_faucet.storage().get_item(TokenMetadata::description_slot(i))?; - assert_eq!(chunk, *expected, "description_{i} should be updated"); - } - - Ok(()) -} - -/// When description mutable flag is 1 but note sender is not the owner, set_description -/// panics. -#[tokio::test] -async fn set_description_mutable_non_owner_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let non_owner_account_id = AccountId::dummy( - [2; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_desc = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let new_desc = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "DSC", - 1000, - owner_account_id, - Some(0), - false, - Some((initial_desc, true)), - None, - None, - )?; - let mock_chain = builder.build()?; - - let set_desc_note_script_code = r#" - begin - call.::miden::standards::metadata::fungible::set_description - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_desc_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(set_desc_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); - let set_desc_note = NoteBuilder::new(non_owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([11, 12, 13, 14u32])) - .code(set_desc_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_desc_note])? - .add_note_script(set_desc_note_script) - .extend_advice_map([(DESCRIPTION_DATA_KEY, field_advice_map_value(&new_desc))]) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); - - Ok(()) -} - -// ================================================================================================= -// set_max_supply: mutable flag and verify_owner -// ================================================================================================= - -/// When max_supply_mutable is 0 (immutable), set_max_supply panics. -#[tokio::test] -async fn set_max_supply_immutable_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "MSM", - 1000, - owner_account_id, - Some(0), - false, // max_supply_mutable = false - None, - None, - None, - )?; - let mock_chain = builder.build()?; - - let new_max_supply: u64 = 2000; - let tx_script = format!( - r#" - begin - push.{new_max_supply} - call.::miden::standards::metadata::fungible::set_max_supply - end - "# - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[])? - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_MAX_SUPPLY_IMMUTABLE); - - Ok(()) -} - -/// When max_supply_mutable is 1 and note sender is the owner, set_max_supply succeeds. -#[tokio::test] -async fn set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { - use miden_standards::account::faucets::NetworkFungibleFaucet; - - let mut builder = MockChain::builder(); - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_max_supply: u64 = 1000; - let new_max_supply: u64 = 2000; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "MSM", - initial_max_supply, - owner_account_id, - Some(0), - true, // max_supply_mutable = true - None, - None, - None, - )?; - let mock_chain = builder.build()?; - - let set_max_supply_note_script_code = format!( - r#" - begin - push.{new_max_supply} - swap drop - call.::miden::standards::metadata::fungible::set_max_supply - end - "# - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_max_supply_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(&set_max_supply_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(42u32); 4].into()); - let set_max_supply_note = NoteBuilder::new(owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([20, 21, 22, 23u32])) - .code(&set_max_supply_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_max_supply_note])? - .add_note_script(set_max_supply_note_script) - .with_source_manager(source_manager) - .build()?; - - let executed = tx_context.execute().await?; - let mut updated_faucet = faucet.clone(); - updated_faucet.apply_delta(executed.account_delta())?; - - // Verify the metadata word: [token_supply, max_supply, decimals, symbol] - let metadata_word = updated_faucet.storage().get_item(NetworkFungibleFaucet::metadata_slot())?; - assert_eq!( - metadata_word[1], - Felt::new(new_max_supply), - "max_supply should be updated to {new_max_supply}" - ); - // token_supply should remain 0 - assert_eq!(metadata_word[0], Felt::new(0), "token_supply should remain unchanged"); - - Ok(()) -} - -/// When max_supply_mutable is 1 but note sender is not the owner, set_max_supply panics. -#[tokio::test] -async fn set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let non_owner_account_id = AccountId::dummy( - [2; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "MSM", - 1000, - owner_account_id, - Some(0), - true, // max_supply_mutable = true - None, - None, - None, - )?; - let mock_chain = builder.build()?; - - let new_max_supply: u64 = 2000; - let set_max_supply_note_script_code = format!( - r#" - begin - push.{new_max_supply} - swap drop - call.::miden::standards::metadata::fungible::set_max_supply - end - "# - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_max_supply_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(&set_max_supply_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); - let set_max_supply_note = NoteBuilder::new(non_owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([30, 31, 32, 33u32])) - .code(&set_max_supply_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_max_supply_note])? - .add_note_script(set_max_supply_note_script) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); - - Ok(()) -} - -// ================================================================================================= -// is_max_supply_mutable: getter test -// ================================================================================================= - -/// Tests that all is_*_mutable procedures correctly read the config flags. -/// Each field is tested with flag=1 (mutable) and flag=0 (immutable). -#[tokio::test] -async fn metadata_is_field_mutable_checks() -> anyhow::Result<()> { - let desc = Description::new("test").unwrap(); - let logo = LogoURI::new("https://example.com/logo").unwrap(); - let link = ExternalLink::new("https://example.com").unwrap(); - - let cases: Vec<(TokenMetadata, &str, u8)> = vec![ - (TokenMetadata::new().with_max_supply_mutable(true), "is_max_supply_mutable", 1), - (TokenMetadata::new().with_description(desc.clone(), true), "is_description_mutable", 1), - (TokenMetadata::new().with_description(desc, false), "is_description_mutable", 0), - (TokenMetadata::new().with_logo_uri(logo.clone(), true), "is_logo_uri_mutable", 1), - (TokenMetadata::new().with_logo_uri(logo, false), "is_logo_uri_mutable", 0), - ( - TokenMetadata::new().with_external_link(link.clone(), true), - "is_external_link_mutable", - 1, - ), - (TokenMetadata::new().with_external_link(link, false), "is_external_link_mutable", 0), - ]; - - for (info, proc_name, expected) in cases { - let account = AccountBuilder::new([1u8; 32]) - .account_type(AccountType::FungibleFaucet) - .with_auth_component(NoAuth) - .with_component(build_faucet_with_info(info)) - .build()?; - - let tx_script = format!( - "begin - call.::miden::standards::metadata::fungible::{proc_name} - push.{expected} - assert_eq.err=\"{proc_name} returned unexpected value\" - end" - ); - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_tx_script(&tx_script)?; - - let tx_context = TransactionContextBuilder::new(account) - .tx_script(tx_script) - .with_source_manager(source_manager) - .build()?; - - tx_context.execute().await?; - } - - Ok(()) -} - -// ================================================================================================= -// set_logo_uri: mutable flag and verify_owner -// ================================================================================================= - -/// When logo URI flag is 0 (immutable), set_logo_uri panics. -#[tokio::test] -async fn set_logo_uri_immutable_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let logo = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "LGO", - 1000, - owner_account_id, - Some(0), - false, - None, - Some((logo, false)), // immutable - None, - )?; - let mock_chain = builder.build()?; - - let new_logo = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let tx_script = r#" - begin - call.::miden::standards::metadata::fungible::set_logo_uri - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[])? - .tx_script(tx_script) - .extend_advice_map([(LOGO_URI_DATA_KEY, field_advice_map_value(&new_logo))]) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_LOGO_URI_NOT_MUTABLE); - - Ok(()) -} - -/// When logo URI mutable flag is 1 and note sender is the owner, set_logo_uri succeeds. -#[tokio::test] -async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_logo = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - - let new_logo = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "LGO", - 1000, - owner_account_id, - Some(0), - false, - None, - Some((initial_logo, true)), // mutable - None, - )?; - let mock_chain = builder.build()?; - - let set_logo_note_script_code = r#" - begin - call.::miden::standards::metadata::fungible::set_logo_uri - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_logo_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(set_logo_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(50u32); 4].into()); - let set_logo_note = NoteBuilder::new(owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([40, 41, 42, 43u32])) - .code(set_logo_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_logo_note])? - .add_note_script(set_logo_note_script) - .extend_advice_map([(LOGO_URI_DATA_KEY, field_advice_map_value(&new_logo))]) - .with_source_manager(source_manager) - .build()?; - - let executed = tx_context.execute().await?; - let mut updated_faucet = faucet.clone(); - updated_faucet.apply_delta(executed.account_delta())?; - - for (i, expected) in new_logo.iter().enumerate() { - let chunk = updated_faucet.storage().get_item(TokenMetadata::logo_uri_slot(i))?; - assert_eq!(chunk, *expected, "logo_uri_{i} should be updated"); - } - - Ok(()) -} - -/// When logo URI mutable flag is 1 but note sender is not the owner, set_logo_uri panics. -#[tokio::test] -async fn set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let non_owner_account_id = AccountId::dummy( - [2; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_logo = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let new_logo = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "LGO", - 1000, - owner_account_id, - Some(0), - false, - None, - Some((initial_logo, true)), - None, - )?; - let mock_chain = builder.build()?; - - let set_logo_note_script_code = r#" - begin - call.::miden::standards::metadata::fungible::set_logo_uri - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_logo_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(set_logo_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); - let set_logo_note = NoteBuilder::new(non_owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([50, 51, 52, 53u32])) - .code(set_logo_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_logo_note])? - .add_note_script(set_logo_note_script) - .extend_advice_map([(LOGO_URI_DATA_KEY, field_advice_map_value(&new_logo))]) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); - - Ok(()) -} - -// ================================================================================================= -// set_external_link: mutable flag and verify_owner -// ================================================================================================= - -/// When external link flag is 0 (immutable), set_external_link panics. -#[tokio::test] -async fn set_external_link_immutable_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let link = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "EXL", - 1000, - owner_account_id, - Some(0), - false, - None, - None, - Some((link, false)), // immutable - )?; - let mock_chain = builder.build()?; - - let new_link = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let tx_script = r#" - begin - call.::miden::standards::metadata::fungible::set_external_link - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let tx_script = - CodeBuilder::with_source_manager(source_manager.clone()).compile_tx_script(tx_script)?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[])? - .tx_script(tx_script) - .extend_advice_map([(EXTERNAL_LINK_DATA_KEY, field_advice_map_value(&new_link))]) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_EXTERNAL_LINK_NOT_MUTABLE); - - Ok(()) -} - -/// When external link mutable flag is 1 and note sender is the owner, set_external_link -/// succeeds. -#[tokio::test] -async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_link = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let new_link = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "EXL", - 1000, - owner_account_id, - Some(0), - false, - None, - None, - Some((initial_link, true)), // mutable - )?; - let mock_chain = builder.build()?; - - let set_link_note_script_code = r#" - begin - call.::miden::standards::metadata::fungible::set_external_link - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_link_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(set_link_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(60u32); 4].into()); - let set_link_note = NoteBuilder::new(owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([60, 61, 62, 63u32])) - .code(set_link_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_link_note])? - .add_note_script(set_link_note_script) - .extend_advice_map([(EXTERNAL_LINK_DATA_KEY, field_advice_map_value(&new_link))]) - .with_source_manager(source_manager) - .build()?; - - let executed = tx_context.execute().await?; - let mut updated_faucet = faucet.clone(); - updated_faucet.apply_delta(executed.account_delta())?; - - for (i, expected) in new_link.iter().enumerate() { - let chunk = updated_faucet.storage().get_item(TokenMetadata::external_link_slot(i))?; - assert_eq!(chunk, *expected, "external_link_{i} should be updated"); - } - - Ok(()) -} - -/// When external link mutable flag is 1 but note sender is not the owner, -/// set_external_link panics. -#[tokio::test] -async fn set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); - - let owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - let non_owner_account_id = AccountId::dummy( - [2; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); - - let initial_link = [ - Word::from([1u32, 2, 3, 4]), - Word::from([5u32, 6, 7, 8]), - Word::from([9u32, 10, 11, 12]), - Word::from([13u32, 14, 15, 16]), - Word::from([17u32, 18, 19, 20]), - Word::from([21u32, 22, 23, 24]), - Word::from([25u32, 26, 27, 28]), - ]; - let new_link = [ - Word::from([100u32, 101, 102, 103]), - Word::from([104u32, 105, 106, 107]), - Word::from([108u32, 109, 110, 111]), - Word::from([112u32, 113, 114, 115]), - Word::from([116u32, 117, 118, 119]), - Word::from([120u32, 121, 122, 123]), - Word::from([124u32, 125, 126, 127]), - ]; - - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "EXL", - 1000, - owner_account_id, - Some(0), - false, - None, - None, - Some((initial_link, true)), - )?; - let mock_chain = builder.build()?; - - let set_link_note_script_code = r#" - begin - call.::miden::standards::metadata::fungible::set_external_link - end - "#; - - let source_manager = Arc::new(DefaultSourceManager::default()); - let set_link_note_script = CodeBuilder::with_source_manager(source_manager.clone()) - .compile_note_script(set_link_note_script_code)?; - - let mut rng = RpoRandomCoin::new([Felt::from(99u32); 4].into()); - let set_link_note = NoteBuilder::new(non_owner_account_id, &mut rng) - .note_type(NoteType::Private) - .tag(NoteTag::default().into()) - .serial_number(Word::from([70, 71, 72, 73u32])) - .code(set_link_note_script_code) - .build()?; - - let tx_context = mock_chain - .build_tx_context(faucet.id(), &[], &[set_link_note])? - .add_note_script(set_link_note_script) - .extend_advice_map([(EXTERNAL_LINK_DATA_KEY, field_advice_map_value(&new_link))]) - .with_source_manager(source_manager) - .build()?; - - let result = tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_SENDER_NOT_OWNER); - - Ok(()) -} From b3413966922489184be5475f8b5124e5d57ff719 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 19:20:20 -0300 Subject: [PATCH 44/56] feat: add TokenMetadata and AccountSchemaCommitment components for enhanced account metadata management, split from mod. --- .../src/account/metadata/mod.rs | 497 +----------------- .../src/account/metadata/schema_commitment.rs | 264 ++++++++++ .../src/account/metadata/token_metadata.rs | 262 +++++++++ 3 files changed, 539 insertions(+), 484 deletions(-) create mode 100644 crates/miden-standards/src/account/metadata/schema_commitment.rs create mode 100644 crates/miden-standards/src/account/metadata/token_metadata.rs diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index e24ff76e20..738a868c96 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -70,28 +70,21 @@ //! .build()?; //! ``` -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; - -use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema}; -use miden_protocol::account::{ - Account, - AccountBuilder, - AccountComponent, - AccountId, - AccountStorage, - AccountType, - StorageSlot, - StorageSlotName, +mod schema_commitment; +mod token_metadata; + +pub use schema_commitment::{ + AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment, SCHEMA_COMMITMENT_SLOT_NAME, }; -use miden_protocol::errors::{AccountError, ComponentMetadataError}; +pub use token_metadata::TokenMetadata; + +use alloc::string::String; + +use miden_protocol::account::StorageSlotName; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use thiserror::Error; -use crate::account::components::{metadata_info_component_library, storage_schema_library}; -use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; // CONSTANTS — canonical layout: slots 0–22 // ================================================================================================ @@ -252,12 +245,6 @@ pub const LOGO_URI_DATA_KEY: Word = pub const EXTERNAL_LINK_DATA_KEY: Word = Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); -/// Schema commitment slot name. -pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::metadata::schema_commitment") - .expect("storage slot name should be valid") -}); - // SLOT ACCESSORS // ================================================================================================ @@ -276,391 +263,20 @@ pub fn mutability_config_slot() -> &'static StorageSlotName { &MUTABILITY_CONFIG_SLOT } -// INFO COMPONENT -// ================================================================================================ - -/// A metadata component storing name, config, and optional fields in fixed value slots. -/// -/// ## Storage Layout -/// -/// - Slot 2–3: name (2 Words = 8 felts) -/// - Slot 4: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` -/// - Slot 5–11: description (7 Words) -/// - Slot 12–18: logo_uri (7 Words) -/// - Slot 19–25: external_link (7 Words) -#[derive(Debug, Clone, Default)] -pub struct TokenMetadata { - owner: Option, - name: Option, - description: Option, - logo_uri: Option, - external_link: Option, - description_mutable: bool, - logo_uri_mutable: bool, - external_link_mutable: bool, - max_supply_mutable: bool, -} - -impl TokenMetadata { - /// Creates a new empty token metadata (all fields absent by default). - pub fn new() -> Self { - Self::default() - } - - /// Sets the owner of this metadata component. - /// - /// The owner is stored in the `ownable::owner_config` slot and is used by the - /// `metadata::fungible` MASM procedures to authorize mutations (e.g. - /// `set_description`). - pub fn with_owner(mut self, owner: AccountId) -> Self { - self.owner = Some(owner); - self - } - - /// Sets whether the max supply can be updated by the owner via - /// `set_max_supply`. If `false` (default), the max supply is immutable. - pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { - self.max_supply_mutable = mutable; - self - } - - /// Sets the token name. - pub fn with_name(mut self, name: TokenName) -> Self { - self.name = Some(name); - self - } - - /// Sets the description with mutability. - /// - /// When `mutable` is `true`, the owner can update the description later. - pub fn with_description(mut self, description: Description, mutable: bool) -> Self { - self.description = Some(description); - self.description_mutable = mutable; - self - } - - /// Sets the logo URI with mutability. - /// - /// When `mutable` is `true`, the owner can update the logo URI later. - pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self { - self.logo_uri = Some(logo_uri); - self.logo_uri_mutable = mutable; - self - } - - /// Sets the external link with mutability. - /// - /// When `mutable` is `true`, the owner can update the external link later. - pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self { - self.external_link = Some(external_link); - self.external_link_mutable = mutable; - self - } - - /// Returns the slot name for name chunk 0. - pub fn name_chunk_0_slot() -> &'static StorageSlotName { - &NAME_SLOTS[0] - } - - /// Returns the slot name for name chunk 1. - pub fn name_chunk_1_slot() -> &'static StorageSlotName { - &NAME_SLOTS[1] - } - - /// Returns the slot name for a description chunk by index (0..7). - pub fn description_slot(index: usize) -> &'static StorageSlotName { - &DESCRIPTION_SLOTS[index] - } - - /// Returns the slot name for a logo URI chunk by index (0..7). - pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName { - &LOGO_URI_SLOTS[index] - } - - /// Returns the slot name for an external link chunk by index (0..7). - pub fn external_link_slot(index: usize) -> &'static StorageSlotName { - &EXTERNAL_LINK_SLOTS[index] - } - - /// Reads the name and optional metadata fields from account storage. - /// - /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if - /// at least one word is non-zero. Decoding errors (e.g. invalid UTF-8 in storage) cause the - /// field to be returned as `None`. - pub fn read_metadata_from_storage( - storage: &AccountStorage, - ) -> (Option, Option, Option, Option) { - let name = if let (Ok(chunk_0), Ok(chunk_1)) = ( - storage.get_item(TokenMetadata::name_chunk_0_slot()), - storage.get_item(TokenMetadata::name_chunk_1_slot()), - ) { - let words: [Word; 2] = [chunk_0, chunk_1]; - if words != [Word::default(); 2] { - TokenName::try_from_words(&words).ok() - } else { - None - } - } else { - None - }; - - let read_field = |slots: &[StorageSlotName; 7]| -> Option<[Word; 7]> { - let mut field = [Word::default(); 7]; - let mut any_set = false; - for (i, slot) in field.iter_mut().enumerate() { - if let Ok(chunk) = storage.get_item(&slots[i]) { - *slot = chunk; - if chunk != Word::default() { - any_set = true; - } - } - } - if any_set { Some(field) } else { None } - }; - - let description = - read_field(&DESCRIPTION_SLOTS).and_then(|w| Description::try_from_words(&w).ok()); - let logo_uri = read_field(&LOGO_URI_SLOTS).and_then(|w| LogoURI::try_from_words(&w).ok()); - let external_link = - read_field(&EXTERNAL_LINK_SLOTS).and_then(|w| ExternalLink::try_from_words(&w).ok()); - - (name, description, logo_uri, external_link) - } - - /// Returns the storage slots for this metadata (without creating an `AccountComponent`). - /// - /// These slots are meant to be included directly in a faucet component rather than - /// added as a separate `AccountComponent`. - pub fn storage_slots(&self) -> Vec { - let mut slots: Vec = Vec::new(); - - // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures - // for get_owner and verify_owner (used in set_* mutations). - // Word layout: [0, 0, owner_suffix, owner_prefix] so that after get_item (which places - // word[0] on top), dropping the two leading zeros yields [owner_suffix, owner_prefix]. - // Only included when an owner is explicitly set, to avoid conflicting with components - // (like NetworkFungibleFaucet) that provide their own owner_config slot. - if let Some(id) = self.owner { - let owner_word = - Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()]); - slots.push(StorageSlot::with_value(owner_config_slot().clone(), owner_word)); - } - - let name_words = self.name.as_ref().map(|n| n.to_words()).unwrap_or_default(); - slots.push(StorageSlot::with_value( - TokenMetadata::name_chunk_0_slot().clone(), - name_words[0], - )); - slots.push(StorageSlot::with_value( - TokenMetadata::name_chunk_1_slot().clone(), - name_words[1], - )); - - let mutability_config_word = Word::from([ - Felt::from(self.description_mutable as u32), - Felt::from(self.logo_uri_mutable as u32), - Felt::from(self.external_link_mutable as u32), - Felt::from(self.max_supply_mutable as u32), - ]); - slots.push(StorageSlot::with_value( - mutability_config_slot().clone(), - mutability_config_word, - )); - - let desc_words: [Word; 7] = - self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); - for (i, word) in desc_words.iter().enumerate() { - slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word)); - } - - let logo_words: [Word; 7] = - self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); - for (i, word) in logo_words.iter().enumerate() { - slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word)); - } - - let link_words: [Word; 7] = - self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); - for (i, word) in link_words.iter().enumerate() { - slots - .push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word)); - } - - slots - } -} - -/// Converts [`TokenMetadata`] into a standalone [`AccountComponent`] that includes the metadata -/// MASM library (`metadata_info_component_library`). Use this when adding generic metadata as a -/// separate component (e.g. for non-faucet accounts). -impl From for AccountComponent { - fn from(info: TokenMetadata) -> Self { - let metadata = - AccountComponentMetadata::new("miden::standards::metadata::info", AccountType::all()) - .with_description( - "Component exposing token name, description, logo URI and external link", - ); - - AccountComponent::new(metadata_info_component_library(), info.storage_slots(), metadata) - .expect( - "TokenMetadata component should satisfy the requirements of a valid account component", - ) - } -} - -// SCHEMA COMMITMENT COMPONENT -// ================================================================================================ - -/// An [`AccountComponent`] exposing the account storage schema commitment. -/// -/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`], -/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`] -/// slot. -/// -/// It reexports the `get_schema_commitment` procedure from -/// `miden::standards::metadata::storage_schema`. -/// -/// ## Storage Layout -/// -/// - [`Self::schema_commitment_slot`]: Storage schema commitment. -pub struct AccountSchemaCommitment { - schema_commitment: Word, -} - -impl AccountSchemaCommitment { - /// Creates a new [`AccountSchemaCommitment`] component from storage schemas. - /// - /// The input schemas are merged into a single schema before the final commitment is computed. - /// - /// # Errors - /// - /// Returns an error if the schemas contain conflicting definitions for the same slot name. - pub fn new<'a>( - schemas: impl IntoIterator, - ) -> Result { - Ok(Self { - schema_commitment: compute_schema_commitment(schemas)?, - }) - } - - /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`]. - pub fn from_schema(storage_schema: &StorageSchema) -> Result { - Self::new(core::slice::from_ref(storage_schema)) - } - - /// Returns the [`StorageSlotName`] where the schema commitment is stored. - pub fn schema_commitment_slot() -> &'static StorageSlotName { - &SCHEMA_COMMITMENT_SLOT_NAME - } -} - -impl From for AccountComponent { - fn from(schema_commitment: AccountSchemaCommitment) -> Self { - let metadata = - AccountComponentMetadata::new("miden::metadata::schema_commitment", AccountType::all()) - .with_description("Component exposing the account storage schema commitment"); - - AccountComponent::new( - storage_schema_library(), - vec![StorageSlot::with_value( - AccountSchemaCommitment::schema_commitment_slot().clone(), - schema_commitment.schema_commitment, - )], - metadata, - ) - .expect( - "AccountSchemaCommitment component should satisfy the requirements of a valid account component", - ) - } -} - -// ACCOUNT BUILDER EXTENSION -// ================================================================================================ - -/// An extension trait for [`AccountBuilder`] that provides a convenience method for building an -/// account with an [`AccountSchemaCommitment`] component. -pub trait AccountBuilderSchemaCommitmentExt { - /// Builds an [`Account`] out of the configured builder after computing the storage schema - /// commitment from all components currently in the builder and adding an - /// [`AccountSchemaCommitment`] component. - /// - /// # Errors - /// - /// Returns an error if: - /// - The components' storage schemas contain conflicting definitions for the same slot name. - /// - [`AccountBuilder::build`] fails. - fn build_with_schema_commitment(self) -> Result; -} - -impl AccountBuilderSchemaCommitmentExt for AccountBuilder { - fn build_with_schema_commitment(self) -> Result { - let schema_commitment = - AccountSchemaCommitment::new(self.storage_schemas()).map_err(|err| { - AccountError::other_with_source("failed to compute account schema commitment", err) - })?; - - self.with_component(schema_commitment).build() - } -} - -// HELPERS -// ================================================================================================ - -/// Computes the schema commitment. -/// -/// The account schema commitment is computed from the merged schema commitment. -/// If the passed list of schemas is empty, [`Word::empty()`] is returned. -fn compute_schema_commitment<'a>( - schemas: impl IntoIterator, -) -> Result { - let mut schemas = schemas.into_iter().peekable(); - if schemas.peek().is_none() { - return Ok(Word::empty()); - } - - let mut merged_slots = BTreeMap::new(); - - for schema in schemas { - for (slot_name, slot_schema) in schema.iter() { - match merged_slots.get(slot_name) { - None => { - merged_slots.insert(slot_name.clone(), slot_schema.clone()); - }, - // Slot exists, check if the schema is the same before erroring - Some(existing) => { - if existing != slot_schema { - return Err(ComponentMetadataError::InvalidSchema(format!( - "conflicting definitions for storage slot `{slot_name}`", - ))); - } - }, - } - } - } - - let merged_schema = StorageSchema::new(merged_slots)?; - - Ok(merged_schema.commitment()) -} - // TESTS // ================================================================================================ #[cfg(test)] mod tests { use miden_protocol::Word; - use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; - use miden_protocol::account::component::AccountComponentMetadata; - use miden_protocol::account::{Account, AccountBuilder}; + use miden_protocol::account::AccountBuilder; use super::{ - AccountBuilderSchemaCommitmentExt, - AccountSchemaCommitment, NAME_UTF8_MAX_BYTES, TokenMetadata as InfoType, mutability_config_slot, }; - use crate::account::auth::{AuthSingleSig, NoAuth}; + use crate::account::auth::NoAuth; use crate::account::faucets::{ BasicFungibleFaucet, Description, @@ -684,7 +300,7 @@ mod tests { .unwrap() } - fn build_account_with_metadata(metadata: FungibleTokenMetadata) -> Account { + fn build_account_with_metadata(metadata: FungibleTokenMetadata) -> miden_protocol::account::Account { AccountBuilder::new([1u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .with_auth_component(NoAuth) @@ -798,91 +414,4 @@ mod tests { assert_eq!(name_0, name_words[0]); assert_eq!(name_1, name_words[1]); } - - #[test] - fn storage_schema_commitment_is_order_independent() { - let toml_a = r#" - name = "Component A" - description = "Component A schema" - version = "0.1.0" - supported-types = [] - - [[storage.slots]] - name = "test::slot_a" - type = "word" - "#; - - let toml_b = r#" - name = "Component B" - description = "Component B schema" - version = "0.1.0" - supported-types = [] - - [[storage.slots]] - name = "test::slot_b" - description = "description is committed to" - type = "word" - "#; - - let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); - let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); - - let schema_a = metadata_a.storage_schema().clone(); - let schema_b = metadata_b.storage_schema().clone(); - - // Create one component for each of two different accounts, but switch orderings - let component_a = - AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap(); - let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap(); - - let account_a = AccountBuilder::new([1u8; 32]) - .with_auth_component(NoAuth) - .with_component(component_a) - .build() - .unwrap(); - - let account_b = AccountBuilder::new([2u8; 32]) - .with_auth_component(NoAuth) - .with_component(component_b) - .build() - .unwrap(); - - let slot_name = AccountSchemaCommitment::schema_commitment_slot(); - let commitment_a = account_a.storage().get_item(slot_name).unwrap(); - let commitment_b = account_b.storage().get_item(slot_name).unwrap(); - - assert_eq!(commitment_a, commitment_b); - } - - #[test] - fn storage_schema_commitment_is_empty_for_no_schemas() { - let component = AccountSchemaCommitment::new(&[]).unwrap(); - - assert_eq!(component.schema_commitment, Word::empty()); - } - - #[test] - fn build_with_schema_commitment_adds_schema_commitment_component() { - let auth_component = AuthSingleSig::new( - PublicKeyCommitment::from(Word::empty()), - AuthScheme::EcdsaK256Keccak, - ); - - let account = AccountBuilder::new([1u8; 32]) - .with_auth_component(auth_component) - .build_with_schema_commitment() - .unwrap(); - - // The auth component has 2 slots (public key and scheme ID) and the schema commitment adds - // 1 more. - assert_eq!(account.storage().num_slots(), 3); - - // The auth component's public key slot should be accessible. - assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok()); - - // The schema commitment slot should be non-empty since we have a component with a schema. - let slot_name = AccountSchemaCommitment::schema_commitment_slot(); - let commitment = account.storage().get_item(slot_name).unwrap(); - assert_ne!(commitment, Word::empty()); - } } diff --git a/crates/miden-standards/src/account/metadata/schema_commitment.rs b/crates/miden-standards/src/account/metadata/schema_commitment.rs new file mode 100644 index 0000000000..c01cd73dae --- /dev/null +++ b/crates/miden-standards/src/account/metadata/schema_commitment.rs @@ -0,0 +1,264 @@ +//! Account storage schema commitment component. +//! +//! [`AccountSchemaCommitment`] computes a commitment over the merged storage schemas of all +//! account components and stores the result in a dedicated slot. The companion +//! [`AccountBuilderSchemaCommitmentExt`] trait adds a convenience method to +//! [`AccountBuilder`](miden_protocol::account::AccountBuilder) for building accounts with an +//! automatically computed schema commitment. + +use alloc::collections::BTreeMap; + +use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema}; +use miden_protocol::account::{ + Account, AccountBuilder, AccountComponent, AccountType, StorageSlot, StorageSlotName, +}; +use miden_protocol::errors::{AccountError, ComponentMetadataError}; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::Word; + +use crate::account::components::storage_schema_library; + +// CONSTANTS +// ================================================================================================ + +/// Schema commitment slot name. +pub static SCHEMA_COMMITMENT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::metadata::schema_commitment") + .expect("storage slot name should be valid") +}); + +// SCHEMA COMMITMENT COMPONENT +// ================================================================================================ + +/// An [`AccountComponent`] exposing the account storage schema commitment. +/// +/// The [`AccountSchemaCommitment`] component can be constructed from a list of [`StorageSchema`], +/// from which a commitment is computed and then inserted into the [`SCHEMA_COMMITMENT_SLOT_NAME`] +/// slot. +/// +/// It reexports the `get_schema_commitment` procedure from +/// `miden::standards::metadata::storage_schema`. +/// +/// ## Storage Layout +/// +/// - [`Self::schema_commitment_slot`]: Storage schema commitment. +pub struct AccountSchemaCommitment { + schema_commitment: Word, +} + +impl AccountSchemaCommitment { + /// Creates a new [`AccountSchemaCommitment`] component from storage schemas. + /// + /// The input schemas are merged into a single schema before the final commitment is computed. + /// + /// # Errors + /// + /// Returns an error if the schemas contain conflicting definitions for the same slot name. + pub fn new<'a>( + schemas: impl IntoIterator, + ) -> Result { + Ok(Self { + schema_commitment: compute_schema_commitment(schemas)?, + }) + } + + /// Creates a new [`AccountSchemaCommitment`] component from a [`StorageSchema`]. + pub fn from_schema(storage_schema: &StorageSchema) -> Result { + Self::new(core::slice::from_ref(storage_schema)) + } + + /// Returns the [`StorageSlotName`] where the schema commitment is stored. + pub fn schema_commitment_slot() -> &'static StorageSlotName { + &SCHEMA_COMMITMENT_SLOT_NAME + } +} + +impl From for AccountComponent { + fn from(schema_commitment: AccountSchemaCommitment) -> Self { + let metadata = + AccountComponentMetadata::new("miden::metadata::schema_commitment", AccountType::all()) + .with_description("Component exposing the account storage schema commitment"); + + AccountComponent::new( + storage_schema_library(), + vec![StorageSlot::with_value( + AccountSchemaCommitment::schema_commitment_slot().clone(), + schema_commitment.schema_commitment, + )], + metadata, + ) + .expect( + "AccountSchemaCommitment component should satisfy the requirements of a valid account component", + ) + } +} + +// ACCOUNT BUILDER EXTENSION +// ================================================================================================ + +/// An extension trait for [`AccountBuilder`] that provides a convenience method for building an +/// account with an [`AccountSchemaCommitment`] component. +pub trait AccountBuilderSchemaCommitmentExt { + /// Builds an [`Account`] out of the configured builder after computing the storage schema + /// commitment from all components currently in the builder and adding an + /// [`AccountSchemaCommitment`] component. + /// + /// # Errors + /// + /// Returns an error if: + /// - The components' storage schemas contain conflicting definitions for the same slot name. + /// - [`AccountBuilder::build`] fails. + fn build_with_schema_commitment(self) -> Result; +} + +impl AccountBuilderSchemaCommitmentExt for AccountBuilder { + fn build_with_schema_commitment(self) -> Result { + let schema_commitment = + AccountSchemaCommitment::new(self.storage_schemas()).map_err(|err| { + AccountError::other_with_source("failed to compute account schema commitment", err) + })?; + + self.with_component(schema_commitment).build() + } +} + +// HELPERS +// ================================================================================================ + +/// Computes the schema commitment. +/// +/// The account schema commitment is computed from the merged schema commitment. +/// If the passed list of schemas is empty, [`Word::empty()`] is returned. +fn compute_schema_commitment<'a>( + schemas: impl IntoIterator, +) -> Result { + let mut schemas = schemas.into_iter().peekable(); + if schemas.peek().is_none() { + return Ok(Word::empty()); + } + + let mut merged_slots = BTreeMap::new(); + + for schema in schemas { + for (slot_name, slot_schema) in schema.iter() { + match merged_slots.get(slot_name) { + None => { + merged_slots.insert(slot_name.clone(), slot_schema.clone()); + }, + // Slot exists, check if the schema is the same before erroring + Some(existing) => { + if existing != slot_schema { + return Err(ComponentMetadataError::InvalidSchema(format!( + "conflicting definitions for storage slot `{slot_name}`", + ))); + } + }, + } + } + } + + let merged_schema = StorageSchema::new(merged_slots)?; + + Ok(merged_schema.commitment()) +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::Word; + use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; + use miden_protocol::account::component::AccountComponentMetadata; + use miden_protocol::account::AccountBuilder; + + use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment}; + use crate::account::auth::{AuthSingleSig, NoAuth}; + + #[test] + fn storage_schema_commitment_is_order_independent() { + let toml_a = r#" + name = "Component A" + description = "Component A schema" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "test::slot_a" + type = "word" + "#; + + let toml_b = r#" + name = "Component B" + description = "Component B schema" + version = "0.1.0" + supported-types = [] + + [[storage.slots]] + name = "test::slot_b" + description = "description is committed to" + type = "word" + "#; + + let metadata_a = AccountComponentMetadata::from_toml(toml_a).unwrap(); + let metadata_b = AccountComponentMetadata::from_toml(toml_b).unwrap(); + + let schema_a = metadata_a.storage_schema().clone(); + let schema_b = metadata_b.storage_schema().clone(); + + // Create one component for each of two different accounts, but switch orderings + let component_a = + AccountSchemaCommitment::new(&[schema_a.clone(), schema_b.clone()]).unwrap(); + let component_b = AccountSchemaCommitment::new(&[schema_b, schema_a]).unwrap(); + + let account_a = AccountBuilder::new([1u8; 32]) + .with_auth_component(NoAuth) + .with_component(component_a) + .build() + .unwrap(); + + let account_b = AccountBuilder::new([2u8; 32]) + .with_auth_component(NoAuth) + .with_component(component_b) + .build() + .unwrap(); + + let slot_name = AccountSchemaCommitment::schema_commitment_slot(); + let commitment_a = account_a.storage().get_item(slot_name).unwrap(); + let commitment_b = account_b.storage().get_item(slot_name).unwrap(); + + assert_eq!(commitment_a, commitment_b); + } + + #[test] + fn storage_schema_commitment_is_empty_for_no_schemas() { + let component = AccountSchemaCommitment::new(&[]).unwrap(); + + assert_eq!(component.schema_commitment, Word::empty()); + } + + #[test] + fn build_with_schema_commitment_adds_schema_commitment_component() { + let auth_component = AuthSingleSig::new( + PublicKeyCommitment::from(Word::empty()), + AuthScheme::EcdsaK256Keccak, + ); + + let account = AccountBuilder::new([1u8; 32]) + .with_auth_component(auth_component) + .build_with_schema_commitment() + .unwrap(); + + // The auth component has 2 slots (public key and scheme ID) and the schema commitment adds + // 1 more. + assert_eq!(account.storage().num_slots(), 3); + + // The auth component's public key slot should be accessible. + assert!(account.storage().get_item(AuthSingleSig::public_key_slot()).is_ok()); + + // The schema commitment slot should be non-empty since we have a component with a schema. + let slot_name = AccountSchemaCommitment::schema_commitment_slot(); + let commitment = account.storage().get_item(slot_name).unwrap(); + assert_ne!(commitment, Word::empty()); + } +} diff --git a/crates/miden-standards/src/account/metadata/token_metadata.rs b/crates/miden-standards/src/account/metadata/token_metadata.rs new file mode 100644 index 0000000000..50f071d546 --- /dev/null +++ b/crates/miden-standards/src/account/metadata/token_metadata.rs @@ -0,0 +1,262 @@ +//! Generic metadata component for non-faucet accounts. +//! +//! [`TokenMetadata`] is a builder-pattern struct that stores name, config, and optional +//! fields (description, logo_uri, external_link, owner) in fixed value slots. For faucet +//! accounts, prefer [`FungibleTokenMetadata`](crate::account::faucets::FungibleTokenMetadata) +//! which embeds all metadata in a single component. + +use alloc::vec::Vec; + +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{ + AccountComponent, + AccountId, + AccountStorage, + AccountType, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::{Felt, Word}; + +use super::{ + DESCRIPTION_SLOTS, + EXTERNAL_LINK_SLOTS, + LOGO_URI_SLOTS, + NAME_SLOTS, + mutability_config_slot, + owner_config_slot, +}; +use crate::account::components::metadata_info_component_library; +use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; + +// TOKEN METADATA +// ================================================================================================ + +/// A metadata component storing name, config, and optional fields in fixed value slots. +/// +/// ## Storage Layout +/// +/// - Slot 2–3: name (2 Words = 8 felts) +/// - Slot 4: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +/// - Slot 5–11: description (7 Words) +/// - Slot 12–18: logo_uri (7 Words) +/// - Slot 19–25: external_link (7 Words) +#[derive(Debug, Clone, Default)] +pub struct TokenMetadata { + owner: Option, + name: Option, + description: Option, + logo_uri: Option, + external_link: Option, + description_mutable: bool, + logo_uri_mutable: bool, + external_link_mutable: bool, + max_supply_mutable: bool, +} + +impl TokenMetadata { + /// Creates a new empty token metadata (all fields absent by default). + pub fn new() -> Self { + Self::default() + } + + /// Sets the owner of this metadata component. + /// + /// The owner is stored in the `ownable::owner_config` slot and is used by the + /// `metadata::fungible` MASM procedures to authorize mutations (e.g. + /// `set_description`). + pub fn with_owner(mut self, owner: AccountId) -> Self { + self.owner = Some(owner); + self + } + + /// Sets whether the max supply can be updated by the owner via + /// `set_max_supply`. If `false` (default), the max supply is immutable. + pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { + self.max_supply_mutable = mutable; + self + } + + /// Sets the token name. + pub fn with_name(mut self, name: TokenName) -> Self { + self.name = Some(name); + self + } + + /// Sets the description with mutability. + /// + /// When `mutable` is `true`, the owner can update the description later. + pub fn with_description(mut self, description: Description, mutable: bool) -> Self { + self.description = Some(description); + self.description_mutable = mutable; + self + } + + /// Sets the logo URI with mutability. + /// + /// When `mutable` is `true`, the owner can update the logo URI later. + pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self { + self.logo_uri = Some(logo_uri); + self.logo_uri_mutable = mutable; + self + } + + /// Sets the external link with mutability. + /// + /// When `mutable` is `true`, the owner can update the external link later. + pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self { + self.external_link = Some(external_link); + self.external_link_mutable = mutable; + self + } + + /// Returns the slot name for name chunk 0. + pub fn name_chunk_0_slot() -> &'static StorageSlotName { + &NAME_SLOTS[0] + } + + /// Returns the slot name for name chunk 1. + pub fn name_chunk_1_slot() -> &'static StorageSlotName { + &NAME_SLOTS[1] + } + + /// Returns the slot name for a description chunk by index (0..7). + pub fn description_slot(index: usize) -> &'static StorageSlotName { + &DESCRIPTION_SLOTS[index] + } + + /// Returns the slot name for a logo URI chunk by index (0..7). + pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName { + &LOGO_URI_SLOTS[index] + } + + /// Returns the slot name for an external link chunk by index (0..7). + pub fn external_link_slot(index: usize) -> &'static StorageSlotName { + &EXTERNAL_LINK_SLOTS[index] + } + + /// Reads the name and optional metadata fields from account storage. + /// + /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if + /// at least one word is non-zero. Decoding errors (e.g. invalid UTF-8 in storage) cause the + /// field to be returned as `None`. + pub fn read_metadata_from_storage( + storage: &AccountStorage, + ) -> (Option, Option, Option, Option) { + let name = if let (Ok(chunk_0), Ok(chunk_1)) = ( + storage.get_item(TokenMetadata::name_chunk_0_slot()), + storage.get_item(TokenMetadata::name_chunk_1_slot()), + ) { + let words: [Word; 2] = [chunk_0, chunk_1]; + if words != [Word::default(); 2] { + TokenName::try_from_words(&words).ok() + } else { + None + } + } else { + None + }; + + let read_field = |slots: &[StorageSlotName; 7]| -> Option<[Word; 7]> { + let mut field = [Word::default(); 7]; + let mut any_set = false; + for (i, slot) in field.iter_mut().enumerate() { + if let Ok(chunk) = storage.get_item(&slots[i]) { + *slot = chunk; + if chunk != Word::default() { + any_set = true; + } + } + } + if any_set { Some(field) } else { None } + }; + + let description = + read_field(&DESCRIPTION_SLOTS).and_then(|w| Description::try_from_words(&w).ok()); + let logo_uri = read_field(&LOGO_URI_SLOTS).and_then(|w| LogoURI::try_from_words(&w).ok()); + let external_link = + read_field(&EXTERNAL_LINK_SLOTS).and_then(|w| ExternalLink::try_from_words(&w).ok()); + + (name, description, logo_uri, external_link) + } + + /// Returns the storage slots for this metadata (without creating an `AccountComponent`). + /// + /// These slots are meant to be included directly in a faucet component rather than + /// added as a separate `AccountComponent`. + pub fn storage_slots(&self) -> Vec { + let mut slots: Vec = Vec::new(); + + // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures + // for get_owner and verify_owner (used in set_* mutations). + // Word layout: [0, 0, owner_suffix, owner_prefix] so that after get_item (which places + // word[0] on top), dropping the two leading zeros yields [owner_suffix, owner_prefix]. + // Only included when an owner is explicitly set, to avoid conflicting with components + // (like NetworkFungibleFaucet) that provide their own owner_config slot. + if let Some(id) = self.owner { + let owner_word = + Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()]); + slots.push(StorageSlot::with_value(owner_config_slot().clone(), owner_word)); + } + + let name_words = self.name.as_ref().map(|n| n.to_words()).unwrap_or_default(); + slots.push(StorageSlot::with_value( + TokenMetadata::name_chunk_0_slot().clone(), + name_words[0], + )); + slots.push(StorageSlot::with_value( + TokenMetadata::name_chunk_1_slot().clone(), + name_words[1], + )); + + let mutability_config_word = Word::from([ + Felt::from(self.description_mutable as u32), + Felt::from(self.logo_uri_mutable as u32), + Felt::from(self.external_link_mutable as u32), + Felt::from(self.max_supply_mutable as u32), + ]); + slots.push(StorageSlot::with_value( + mutability_config_slot().clone(), + mutability_config_word, + )); + + let desc_words: [Word; 7] = + self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); + for (i, word) in desc_words.iter().enumerate() { + slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word)); + } + + let logo_words: [Word; 7] = + self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); + for (i, word) in logo_words.iter().enumerate() { + slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word)); + } + + let link_words: [Word; 7] = + self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); + for (i, word) in link_words.iter().enumerate() { + slots + .push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word)); + } + + slots + } +} + +/// Converts [`TokenMetadata`] into a standalone [`AccountComponent`] that includes the metadata +/// MASM library (`metadata_info_component_library`). Use this when adding generic metadata as a +/// separate component (e.g. for non-faucet accounts). +impl From for AccountComponent { + fn from(info: TokenMetadata) -> Self { + let metadata = + AccountComponentMetadata::new("miden::standards::metadata::info", AccountType::all()) + .with_description( + "Component exposing token name, description, logo URI and external link", + ); + + AccountComponent::new(metadata_info_component_library(), info.storage_slots(), metadata) + .expect( + "TokenMetadata component should satisfy the requirements of a valid account component", + ) + } +} From ff473ea88986c331a717d9474705e8b82987b41e Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 19:20:47 -0300 Subject: [PATCH 45/56] chore: lint --- .../src/account/metadata/mod.rs | 22 +++++++++---------- .../src/account/metadata/schema_commitment.rs | 11 +++++++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 738a868c96..02a4d1b8fb 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -73,18 +73,18 @@ mod schema_commitment; mod token_metadata; -pub use schema_commitment::{ - AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment, SCHEMA_COMMITMENT_SLOT_NAME, -}; -pub use token_metadata::TokenMetadata; - use alloc::string::String; use miden_protocol::account::StorageSlotName; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; +pub use schema_commitment::{ + AccountBuilderSchemaCommitmentExt, + AccountSchemaCommitment, + SCHEMA_COMMITMENT_SLOT_NAME, +}; use thiserror::Error; - +pub use token_metadata::TokenMetadata; // CONSTANTS — canonical layout: slots 0–22 // ================================================================================================ @@ -271,11 +271,7 @@ mod tests { use miden_protocol::Word; use miden_protocol::account::AccountBuilder; - use super::{ - NAME_UTF8_MAX_BYTES, - TokenMetadata as InfoType, - mutability_config_slot, - }; + use super::{NAME_UTF8_MAX_BYTES, TokenMetadata as InfoType, mutability_config_slot}; use crate::account::auth::NoAuth; use crate::account::faucets::{ BasicFungibleFaucet, @@ -300,7 +296,9 @@ mod tests { .unwrap() } - fn build_account_with_metadata(metadata: FungibleTokenMetadata) -> miden_protocol::account::Account { + fn build_account_with_metadata( + metadata: FungibleTokenMetadata, + ) -> miden_protocol::account::Account { AccountBuilder::new([1u8; 32]) .account_type(miden_protocol::account::AccountType::FungibleFaucet) .with_auth_component(NoAuth) diff --git a/crates/miden-standards/src/account/metadata/schema_commitment.rs b/crates/miden-standards/src/account/metadata/schema_commitment.rs index c01cd73dae..1746112429 100644 --- a/crates/miden-standards/src/account/metadata/schema_commitment.rs +++ b/crates/miden-standards/src/account/metadata/schema_commitment.rs @@ -8,13 +8,18 @@ use alloc::collections::BTreeMap; +use miden_protocol::Word; use miden_protocol::account::component::{AccountComponentMetadata, StorageSchema}; use miden_protocol::account::{ - Account, AccountBuilder, AccountComponent, AccountType, StorageSlot, StorageSlotName, + Account, + AccountBuilder, + AccountComponent, + AccountType, + StorageSlot, + StorageSlotName, }; use miden_protocol::errors::{AccountError, ComponentMetadataError}; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::Word; use crate::account::components::storage_schema_library; @@ -168,9 +173,9 @@ fn compute_schema_commitment<'a>( #[cfg(test)] mod tests { use miden_protocol::Word; + use miden_protocol::account::AccountBuilder; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; - use miden_protocol::account::AccountBuilder; use super::{AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment}; use crate::account::auth::{AuthSingleSig, NoAuth}; From 9b737404f92a2f8102d3a29c45aec1e973ab254f Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 19:44:16 -0300 Subject: [PATCH 46/56] refactor: replace TokenName::new("") with TokenName::default() for consistency --- crates/miden-agglayer/src/lib.rs | 2 +- .../src/account/faucets/token_metadata.rs | 9 ++++++++- .../miden-standards/src/account/metadata/mod.rs | 15 +++++++++------ .../miden-testing/src/mock_chain/chain_builder.rs | 8 ++++---- .../miden-testing/src/standards/token_metadata.rs | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 0b9fbdf11e..720285efb7 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -346,7 +346,7 @@ impl AggLayerFaucet { scale: u8, ) -> Result { // Use empty name for agglayer faucets (name is stored in Info component, not here). - let name = TokenName::new("").expect("empty string is valid"); + let name = TokenName::default(); let metadata = FungibleTokenMetadata::with_supply( symbol, decimals, diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 4c5c82f8a1..d6e68ac98b 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -41,6 +41,13 @@ const BYTES_PER_FELT: usize = 7; #[derive(Debug, Clone, PartialEq, Eq)] pub struct TokenName(Box); +impl Default for TokenName { + /// Returns an empty token name. + fn default() -> Self { + Self(String::new().into_boxed_str()) + } +} + impl TokenName { /// Maximum byte length for a token name (2 Words = 8 felts × 7 bytes, capacity 55, /// capped at 32). @@ -654,7 +661,7 @@ impl TryFrom for FungibleTokenMetadata { } })?; - let name = TokenName::new("").expect("empty string should be valid"); + let name = TokenName::default(); Self::with_supply(symbol, decimals, max_supply, token_supply, name, None, None, None) } } diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 02a4d1b8fb..2af93f9790 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -107,7 +107,9 @@ pub static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { /// Token name (2 Words = 8 felts), split across 2 slots. /// /// The encoding is not specified; the value is opaque word data. For human-readable names, -/// use [`TokenName::new`] / [`TokenName::to_words`] / [`TokenName::try_from_words`]. +/// use [`TokenName::new`](crate::account::faucets::TokenName::new) / +/// [`TokenName::to_words`](crate::account::faucets::TokenName::to_words) / +/// [`TokenName::try_from_words`](crate::account::faucets::TokenName::try_from_words). pub static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::name_0").expect("valid slot name"), @@ -135,7 +137,8 @@ pub enum NameUtf8Error { /// Bytes are packed 7-bytes-per-felt, length-prefixed, into 8 felts (2 Words). /// Returns an error if the UTF-8 byte length exceeds 32. /// -/// Prefer using [`TokenName::new`] + [`TokenName::to_words`] directly. +/// Prefer using [`TokenName::new`](crate::account::faucets::TokenName::new) + +/// [`TokenName::to_words`](crate::account::faucets::TokenName::to_words) directly. pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { use crate::account::faucets::TokenName; Ok(TokenName::new(s)?.to_words()) @@ -145,7 +148,7 @@ pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { /// /// Assumes the name was encoded with [`name_from_utf8`] (7-bytes-per-felt, length-prefixed). /// -/// Prefer using [`TokenName::try_from_words`] directly. +/// Prefer using [`TokenName::try_from_words`](crate::account::faucets::TokenName::try_from_words) directly. pub fn name_to_utf8(words: &[Word; 2]) -> Result { use crate::account::faucets::TokenName; Ok(TokenName::try_from_words(words)?.as_str().into()) @@ -179,8 +182,9 @@ pub enum FieldBytesError { /// Bytes are packed 7-bytes-per-felt, length-prefixed, into 28 felts (7 Words). /// Returns an error if the length exceeds [`FIELD_MAX_BYTES`]. /// -/// Prefer using [`Description::new`] + [`Description::to_words`] (or `LogoURI` / `ExternalLink`) -/// directly. +/// Prefer using [`Description::new`](crate::account::faucets::Description::new) + +/// [`Description::to_words`](crate::account::faucets::Description::to_words) (or `LogoURI` / +/// `ExternalLink`) directly. pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 7], FieldBytesError> { use crate::account::faucets::Description; let s = core::str::from_utf8(bytes).map_err(|_| FieldBytesError::InvalidUtf8)?; @@ -268,7 +272,6 @@ pub fn mutability_config_slot() -> &'static StorageSlotName { #[cfg(test)] mod tests { - use miden_protocol::Word; use miden_protocol::account::AccountBuilder; use super::{NAME_UTF8_MAX_BYTES, TokenMetadata as InfoType, mutability_config_slot}; diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 7f91d89540..e296615cfa 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -331,7 +331,7 @@ impl MockChainBuilder { max_supply: u64, ) -> anyhow::Result { let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); + .unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; @@ -369,7 +369,7 @@ impl MockChainBuilder { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); + .unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; let metadata = FungibleTokenMetadata::new( @@ -406,7 +406,7 @@ impl MockChainBuilder { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); + .unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; @@ -451,7 +451,7 @@ impl MockChainBuilder { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::new("").expect("empty name should be valid")); + .unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index c40c2cf00c..cee04567a8 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -213,7 +213,7 @@ async fn get_name_zeros_returns_empty() -> anyhow::Result<()> { "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("").unwrap(), + TokenName::default(), None, None, None, From 746c15bec3bf343fda6587f9d09336d0cddbda7b Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 19:51:32 -0300 Subject: [PATCH 47/56] refactor: update metadata field limits and improve code consistency --- .../src/account/faucets/token_metadata.rs | 6 +- .../src/account/metadata/mod.rs | 79 +++++-------------- .../src/standards/token_metadata.rs | 24 +++--- 3 files changed, 33 insertions(+), 76 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index d6e68ac98b..19c677ff15 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -99,7 +99,7 @@ impl TokenName { // DESCRIPTION // ================================================================================================ -/// Token description (max [`FIELD_MAX_BYTES`](metadata::FIELD_MAX_BYTES) bytes UTF-8). +/// Token description (max 195 bytes UTF-8). /// /// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words /// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. @@ -138,7 +138,7 @@ impl Description { // LOGO URI // ================================================================================================ -/// Token logo URI (max [`FIELD_MAX_BYTES`](metadata::FIELD_MAX_BYTES) bytes UTF-8). +/// Token logo URI (max 195 bytes UTF-8). /// /// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words /// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. @@ -177,7 +177,7 @@ impl LogoURI { // EXTERNAL LINK // ================================================================================================ -/// Token external link (max [`FIELD_MAX_BYTES`](metadata::FIELD_MAX_BYTES) bytes UTF-8). +/// Token external link (max 195 bytes UTF-8). /// /// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words /// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 2af93f9790..4f1df4a595 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -49,8 +49,7 @@ //! content, zero-padded. Each 7-byte chunk is stored as a LE u64 with the high byte always zero, //! so it always fits in a Goldilocks field element. //! -//! The name slots hold 2 Words (8 felts, capacity 55 bytes, capped at 32). See -//! [`name_from_utf8`], [`name_to_utf8`] for convenience helpers. +//! The name slots hold 2 Words (8 felts, capacity 55 bytes, capped at 32). //! //! # Example //! @@ -73,8 +72,6 @@ mod schema_commitment; mod token_metadata; -use alloc::string::String; - use miden_protocol::account::StorageSlotName; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; @@ -90,7 +87,7 @@ pub use token_metadata::TokenMetadata; // ================================================================================================ /// Token metadata: `[token_supply, max_supply, decimals, token_symbol]`. -pub static TOKEN_METADATA_SLOT: LazyLock = LazyLock::new(|| { +pub(crate) static TOKEN_METADATA_SLOT: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::metadata::token_metadata") .expect("storage slot name should be valid") }); @@ -99,7 +96,7 @@ pub static TOKEN_METADATA_SLOT: LazyLock = LazyLock::new(|| { /// Referenced here so that faucets and other metadata consumers can locate the owner /// through a single `metadata::owner_config_slot()` accessor, without depending on /// the ownable module directly. -pub static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { +pub(crate) static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::access::ownable::owner_config") .expect("storage slot name should be valid") }); @@ -110,7 +107,7 @@ pub static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { /// use [`TokenName::new`](crate::account::faucets::TokenName::new) / /// [`TokenName::to_words`](crate::account::faucets::TokenName::to_words) / /// [`TokenName::try_from_words`](crate::account::faucets::TokenName::try_from_words). -pub static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { +pub(crate) static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::name_0").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::name_1").expect("valid slot name"), @@ -119,80 +116,44 @@ pub static NAME_SLOTS: LazyLock<[StorageSlotName; 2]> = LazyLock::new(|| { /// Maximum length of a name in bytes when using the UTF-8 encoding (2 Words = 8 felts × 7 bytes /// = 56 byte buffer − 1 length byte = 55 capacity, capped at 32). -pub const NAME_UTF8_MAX_BYTES: usize = 32; +pub(crate) const NAME_UTF8_MAX_BYTES: usize = 32; /// Errors when encoding or decoding the metadata name as UTF-8. #[derive(Debug, Clone, Error)] pub enum NameUtf8Error { - /// Name exceeds [`NAME_UTF8_MAX_BYTES`]. - #[error("name must be at most {NAME_UTF8_MAX_BYTES} UTF-8 bytes, got {0}")] + /// Name exceeds the maximum of 32 UTF-8 bytes. + #[error("name must be at most 32 UTF-8 bytes, got {0}")] TooLong(usize), /// Decoded bytes are not valid UTF-8. #[error("name is not valid UTF-8")] InvalidUtf8, } -/// Encodes a UTF-8 string into the 2-Word name format. -/// -/// Bytes are packed 7-bytes-per-felt, length-prefixed, into 8 felts (2 Words). -/// Returns an error if the UTF-8 byte length exceeds 32. -/// -/// Prefer using [`TokenName::new`](crate::account::faucets::TokenName::new) + -/// [`TokenName::to_words`](crate::account::faucets::TokenName::to_words) directly. -pub fn name_from_utf8(s: &str) -> Result<[Word; 2], NameUtf8Error> { - use crate::account::faucets::TokenName; - Ok(TokenName::new(s)?.to_words()) -} - -/// Decodes the 2-Word name format as UTF-8. -/// -/// Assumes the name was encoded with [`name_from_utf8`] (7-bytes-per-felt, length-prefixed). -/// -/// Prefer using [`TokenName::try_from_words`](crate::account::faucets::TokenName::try_from_words) directly. -pub fn name_to_utf8(words: &[Word; 2]) -> Result { - use crate::account::faucets::TokenName; - Ok(TokenName::try_from_words(words)?.as_str().into()) -} - /// Mutability config slot: `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]`. /// /// Each flag is 0 (immutable) or 1 (mutable / owner can update). -pub static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { +pub(crate) static MUTABILITY_CONFIG_SLOT: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::metadata::mutability_config") .expect("storage slot name should be valid") }); /// Maximum length of a metadata field (description, logo_uri, external_link) in bytes. /// 7 Words = 28 felts × 7 bytes = 196 byte buffer − 1 length byte = 195 bytes. -pub const FIELD_MAX_BYTES: usize = 195; +pub(crate) const FIELD_MAX_BYTES: usize = 195; /// Errors when encoding or decoding metadata fields. #[derive(Debug, Clone, Error)] pub enum FieldBytesError { - /// Field exceeds [`FIELD_MAX_BYTES`]. - #[error("field must be at most {FIELD_MAX_BYTES} bytes, got {0}")] + /// Field exceeds the maximum of 195 bytes. + #[error("field must be at most 195 bytes, got {0}")] TooLong(usize), /// Decoded bytes are not valid UTF-8. #[error("field is not valid UTF-8")] InvalidUtf8, } -/// Encodes a UTF-8 string into 7 Words (28 felts). -/// -/// Bytes are packed 7-bytes-per-felt, length-prefixed, into 28 felts (7 Words). -/// Returns an error if the length exceeds [`FIELD_MAX_BYTES`]. -/// -/// Prefer using [`Description::new`](crate::account::faucets::Description::new) + -/// [`Description::to_words`](crate::account::faucets::Description::to_words) (or `LogoURI` / -/// `ExternalLink`) directly. -pub fn field_from_bytes(bytes: &[u8]) -> Result<[Word; 7], FieldBytesError> { - use crate::account::faucets::Description; - let s = core::str::from_utf8(bytes).map_err(|_| FieldBytesError::InvalidUtf8)?; - Ok(Description::new(s)?.to_words()) -} - /// Description (7 Words = 28 felts), split across 7 slots. -pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { +pub(crate) static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::description_0").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::description_1").expect("valid slot name"), @@ -205,7 +166,7 @@ pub static DESCRIPTION_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| }); /// Logo URI (7 Words = 28 felts), split across 7 slots. -pub static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { +pub(crate) static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::logo_uri_0").expect("valid slot name"), StorageSlotName::new("miden::standards::metadata::logo_uri_1").expect("valid slot name"), @@ -218,7 +179,7 @@ pub static LOGO_URI_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { }); /// External link (7 Words = 28 felts), split across 7 slots. -pub static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { +pub(crate) static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock::new(|| { [ StorageSlotName::new("miden::standards::metadata::external_link_0") .expect("valid slot name"), @@ -253,17 +214,17 @@ pub const EXTERNAL_LINK_DATA_KEY: Word = // ================================================================================================ /// Returns the [`StorageSlotName`] for token metadata (slot 0). -pub fn token_metadata_slot() -> &'static StorageSlotName { +pub(crate) fn token_metadata_slot() -> &'static StorageSlotName { &TOKEN_METADATA_SLOT } /// Returns the [`StorageSlotName`] for owner config (slot 1). -pub fn owner_config_slot() -> &'static StorageSlotName { +pub(crate) fn owner_config_slot() -> &'static StorageSlotName { &OWNER_CONFIG_SLOT } /// Returns the [`StorageSlotName`] for the mutability config Word. -pub fn mutability_config_slot() -> &'static StorageSlotName { +pub(crate) fn mutability_config_slot() -> &'static StorageSlotName { &MUTABILITY_CONFIG_SLOT } @@ -274,7 +235,7 @@ pub fn mutability_config_slot() -> &'static StorageSlotName { mod tests { use miden_protocol::account::AccountBuilder; - use super::{NAME_UTF8_MAX_BYTES, TokenMetadata as InfoType, mutability_config_slot}; + use super::{TokenMetadata as InfoType, mutability_config_slot}; use crate::account::auth::NoAuth; use crate::account::faucets::{ BasicFungibleFaucet, @@ -375,7 +336,7 @@ mod tests { #[test] fn name_max_32_bytes_accepted() { - let s = "a".repeat(NAME_UTF8_MAX_BYTES); + let s = "a".repeat(TokenName::MAX_BYTES); assert_eq!(s.len(), 32); let name = TokenName::new(&s).unwrap(); let words = name.to_words(); @@ -398,7 +359,7 @@ mod tests { #[test] fn description_too_long_rejected() { - let s = "a".repeat(super::FIELD_MAX_BYTES + 1); + let s = "a".repeat(Description::MAX_BYTES + 1); assert!(Description::new(&s).is_err()); } diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index cee04567a8..5774f4673d 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -32,11 +32,8 @@ use miden_standards::account::faucets::{ use miden_standards::account::metadata::{ DESCRIPTION_DATA_KEY, EXTERNAL_LINK_DATA_KEY, - FieldBytesError, LOGO_URI_DATA_KEY, - NAME_UTF8_MAX_BYTES, TokenMetadata, - field_from_bytes, }; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ @@ -616,7 +613,7 @@ fn faucet_with_metadata_storage_layout() { #[test] fn name_32_bytes_accepted() { - let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); + let max_name = "a".repeat(TokenName::MAX_BYTES); let token_name = TokenName::new(&max_name).unwrap(); let metadata = FungibleTokenMetadata::new( "TST".try_into().unwrap(), @@ -637,8 +634,8 @@ fn name_32_bytes_accepted() { .unwrap(); let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); - let decoded = miden_standards::account::metadata::name_to_utf8(&[name_0, name_1]).unwrap(); - assert_eq!(decoded, max_name); + let decoded = TokenName::try_from_words(&[name_0, name_1]).unwrap(); + assert_eq!(decoded.as_str(), max_name); } #[test] @@ -680,10 +677,9 @@ fn description_7_words_full_capacity() { #[test] fn field_over_max_bytes_rejected() { - use miden_standards::account::metadata::FIELD_MAX_BYTES; - let over = FIELD_MAX_BYTES + 1; - let result = field_from_bytes("a".repeat(over).as_bytes()); - assert!(matches!(result, Err(FieldBytesError::TooLong(n)) if n == over)); + let over = Description::MAX_BYTES + 1; + let result = Description::new(&"a".repeat(over)); + assert!(result.is_err()); } // ================================================================================================= @@ -697,7 +693,7 @@ fn verify_faucet_with_max_name_and_description( storage_mode: AccountStorageMode, extra_components: Vec, ) { - let max_name = "a".repeat(NAME_UTF8_MAX_BYTES); + let max_name = "a".repeat(TokenName::MAX_BYTES); let desc_text = "a".repeat(Description::MAX_BYTES); let description = Description::new(&desc_text).unwrap(); let desc_words = description.to_words(); @@ -725,7 +721,7 @@ fn verify_faucet_with_max_name_and_description( let account = builder.build().unwrap(); - let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + let name_words = TokenName::new(&max_name).unwrap().to_words(); let name_0 = account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); let name_1 = account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); assert_eq!(name_0, name_words[0]); @@ -810,8 +806,8 @@ async fn basic_faucet_name_readable_from_masm() -> anyhow::Result<()> { #[tokio::test] async fn network_faucet_name_readable_from_masm() -> anyhow::Result<()> { - let max_name = "b".repeat(NAME_UTF8_MAX_BYTES); - let name_words = miden_standards::account::metadata::name_from_utf8(&max_name).unwrap(); + let max_name = "b".repeat(TokenName::MAX_BYTES); + let name_words = TokenName::new(&max_name).unwrap().to_words(); let network_faucet_metadata = FungibleTokenMetadata::new( "MAS".try_into().unwrap(), From e949534fdcd7f7cf7d1b8b009fb43a28440fb378 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Wed, 11 Mar 2026 19:55:46 -0300 Subject: [PATCH 48/56] chore: lint --- .../src/account/faucets/token_metadata.rs | 2 +- crates/miden-testing/src/mock_chain/chain_builder.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 19c677ff15..6419bab4db 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -99,7 +99,7 @@ impl TokenName { // DESCRIPTION // ================================================================================================ -/// Token description (max 195 bytes UTF-8). +/// Token description (max 195 bytes UTF-8). /// /// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words /// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index e296615cfa..1811ffc60a 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -330,8 +330,7 @@ impl MockChainBuilder { token_symbol: &str, max_supply: u64, ) -> anyhow::Result { - let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::default()); + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; @@ -368,8 +367,7 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; - let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::default()); + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; let metadata = FungibleTokenMetadata::new( @@ -405,8 +403,7 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; - let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::default()); + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; @@ -450,8 +447,7 @@ impl MockChainBuilder { ) -> anyhow::Result { let max_supply = Felt::try_from(max_supply)?; let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; - let name = TokenName::new(token_symbol) - .unwrap_or_else(|_| TokenName::default()); + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; From 2b0cf8366f6cc14264e644bd7fd0ab433b26576e Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 12 Mar 2026 16:40:08 -0300 Subject: [PATCH 49/56] remove duplicates! --- crates/miden-standards/src/account/metadata/mod.rs | 12 ------------ crates/miden-testing/src/standards/token_metadata.rs | 7 ------- 2 files changed, 19 deletions(-) diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 4f1df4a595..1c90fb9dd0 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -344,12 +344,6 @@ mod tests { assert_eq!(decoded.as_str(), s); } - #[test] - fn name_too_long_errors() { - let s = "a".repeat(33); - assert!(TokenName::new(&s).is_err()); - } - #[test] fn description_max_bytes_accepted() { let s = "a".repeat(Description::MAX_BYTES); @@ -357,12 +351,6 @@ mod tests { assert_eq!(desc.to_words().len(), 7); } - #[test] - fn description_too_long_rejected() { - let s = "a".repeat(Description::MAX_BYTES + 1); - assert!(Description::new(&s).is_err()); - } - #[test] fn metadata_info_with_name() { let name = TokenName::new("My Token").unwrap(); diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index 5774f4673d..e8fd374880 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -675,13 +675,6 @@ fn description_7_words_full_capacity() { } } -#[test] -fn field_over_max_bytes_rejected() { - let over = Description::MAX_BYTES + 1; - let result = Description::new(&"a".repeat(over)); - assert!(result.is_err()); -} - // ================================================================================================= // FAUCET INITIALIZATION – basic + network with max name/description // ================================================================================================= From 547b1a804d7fadb7b636fc3d1a8760c3a590ad34 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Thu, 12 Mar 2026 18:07:52 -0300 Subject: [PATCH 50/56] refactor: remove unused scale_down_amount procedure --- crates/miden-agglayer/asm/agglayer/faucet/mod.masm | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index 4151cc6086..fea82cd12e 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -233,12 +233,6 @@ proc get_raw_claim_amount mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 end -# Inputs: [U256[0], U256[1]] -# Outputs: [amount] -proc scale_down_amount - repeat.7 drop end -end - # Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY] # Outputs: [] proc batch_pipe_double_words From b72e05a900a2623da0945b16996b9c5bec37c258 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 16 Mar 2026 07:41:00 -0300 Subject: [PATCH 51/56] refactor: account metadata handling and remove owner dependency in favor of ownable2steps. - Updated account metadata module to clarify ownership management, removing direct references to the owner in metadata. - Simplified `TokenMetadata` struct by eliminating owner-related fields and methods, focusing on name and optional fields. - Adjusted storage layout and accessors to reflect changes in metadata structure. - Removed owner-related tests and mock implementations from testing suite. - Introduced Rpo256 hashing for field data to enhance integrity checks in metadata operations. --- CHANGELOG.md | 4 +- .../miden-protocol/src/asset/token_symbol.rs | 10 +- .../faucets/fungible_token_metadata.masm | 1 - .../asm/standards/metadata/fungible.masm | 102 ++-- .../src/account/components/mod.rs | 7 +- .../src/account/encoding/mod.rs | 275 ++++++++++ .../src/account/faucets/basic_fungible.rs | 3 +- .../src/account/faucets/mod.rs | 2 + .../src/account/faucets/network_fungible.rs | 19 +- .../src/account/faucets/token_metadata.rs | 485 +++++++----------- .../src/account/metadata/mod.rs | 58 +-- .../src/account/metadata/token_metadata.rs | 213 ++++---- crates/miden-standards/src/account/mod.rs | 1 + .../src/mock_chain/chain_builder.rs | 1 - .../src/standards/token_metadata.rs | 90 ++-- 15 files changed, 688 insertions(+), 583 deletions(-) create mode 100644 crates/miden-standards/src/account/encoding/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 537290a335..91761a9cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Made `NoteMetadataHeader` and `NoteMetadata::to_header()` public, added `NoteMetadata::from_header()` constructor, and exported `NoteMetadataHeader` from the `note` module ([#2561](https://github.com/0xMiden/protocol/pull/2561)). - Introduce NOTE_MAX_SIZE (32 KiB) and enforce it on individual output notes ([#2205](https://github.com/0xMiden/miden-base/pull/2205)) - Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). -- Added metadata extension (`Info`) with name and content URI slots, and MASM procedures `get_token_metadata`, `get_max_supply`, `get_decimals`, and `get_token_symbol` for the token metadata standard. Aligned fungible faucet token metadata with the metadata standard: faucet now uses the canonical slot `miden::standards::metadata::token_metadata` so MASM metadata getters work with faucet storage.([#2439](https://github.com/0xMiden/miden-base/pull/2439)) +- Added `FungibleTokenMetadata` component with name, description, logo URI, and external link support, and MASM procedures `get_token_metadata`, `get_max_supply`, `get_decimals`, and `get_token_symbol` for the token metadata standard. Aligned fungible faucet token metadata with the metadata standard: faucet now uses the canonical slot `miden::standards::metadata::token_metadata` so MASM metadata getters work with faucet storage.([#2439](https://github.com/0xMiden/miden-base/pull/2439)) - Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)). - Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)). @@ -49,8 +49,6 @@ - Restructured `miden-agglayer/asm` directory to separate bridge and faucet into per-component libraries, preventing cross-component procedure exposure ([#2294](https://github.com/0xMiden/miden-base/issues/2294)). - `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. - Prefixed standard account component names with `miden::standards::components` ([#2400](https://github.com/0xMiden/miden-base/pull/2400)). -- `TokenMetadata` (faucet) now uses the shared metadata slot from the metadata module for consistency with MASM standards. -- Prefixed standard account component names with `miden::standards::components` ([#2400](https://github.com/0xMiden/miden-base/pull/2400)). - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). - Fixed MASM inline comment casing to adhere to commenting conventions ([#2398](https://github.com/0xMiden/miden-base/pull/2398)). diff --git a/crates/miden-protocol/src/asset/token_symbol.rs b/crates/miden-protocol/src/asset/token_symbol.rs index 7bd96a219f..b891ae790b 100644 --- a/crates/miden-protocol/src/asset/token_symbol.rs +++ b/crates/miden-protocol/src/asset/token_symbol.rs @@ -102,9 +102,9 @@ impl TryFrom for TokenSymbol { /// The alphabet used in the decoding process consists of the Latin capital letters as defined in /// the ASCII table, having the length of 26 characters. /// -/// The encoding is performed by multiplying the intermediate encrypted value by the length of the +/// The encoding is performed by multiplying the intermediate encoded value by the length of the /// used alphabet and adding the relative index of the character to it. At the end of the encoding -/// process the length of the initial token string is added to the encrypted value. +/// process the length of the initial token string is added to the encoded value. /// /// Relative character index is computed by subtracting the index of the character "A" (65) from the /// index of the currently processing character, e.g., `A = 65 - 65 = 0`, `B = 66 - 65 = 1`, `...` , @@ -148,14 +148,14 @@ const fn encode_symbol_to_felt(s: &str) -> Result { /// The alphabet used in the decoding process consists of the Latin capital letters as defined in /// the ASCII table, having the length of 26 characters. /// -/// The decoding is performed by getting the modulus of the intermediate encrypted value by the +/// The decoding is performed by getting the modulus of the intermediate encoded value by the /// length of the used alphabet and then dividing the intermediate value by the length of the /// alphabet to shift to the next character. At the beginning of the decoding process the length of -/// the initial token string is obtained from the encrypted value. After that the value obtained +/// the initial token string is obtained from the encoded value. After that the value obtained /// after taking the modulus represents the relative character index, which then gets converted to /// the ASCII index. /// -/// Final ASCII character idex is computed by adding the index of the character "A" (65) to the +/// Final ASCII character index is computed by adding the index of the character "A" (65) to the /// index of the currently processing character, e.g., `A = 0 + 65 = 65`, `B = 1 + 65 = 66`, `...` , /// `Z = 25 + 65 = 90`. /// diff --git a/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm b/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm index 9066b76f61..87d1aa532d 100644 --- a/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm +++ b/crates/miden-standards/asm/account_components/faucets/fungible_token_metadata.masm @@ -18,4 +18,3 @@ pub use ::miden::standards::metadata::fungible::set_description pub use ::miden::standards::metadata::fungible::set_logo_uri pub use ::miden::standards::metadata::fungible::set_external_link pub use ::miden::standards::metadata::fungible::set_max_supply -pub use ::miden::standards::metadata::fungible::get_owner diff --git a/crates/miden-standards/asm/standards/metadata/fungible.masm b/crates/miden-standards/asm/standards/metadata/fungible.masm index 2d865545cf..5590c75cd5 100644 --- a/crates/miden-standards/asm/standards/metadata/fungible.masm +++ b/crates/miden-standards/asm/standards/metadata/fungible.masm @@ -1,12 +1,12 @@ # miden::standards::metadata::fungible # # Metadata for fungible-style accounts: slots, getters (name, description, logo_uri, -# external_link, token_metadata), get_owner, and optional setters. -# Depends on ownable for owner and mutable fields. +# external_link, token_metadata), and owner-gated setters. +# Depends on ownable2step for ownership checks. use miden::protocol::active_account use miden::protocol::native_account -use miden::standards::access::ownable +use miden::standards::access::ownable2step # ================================================================================================= # CONSTANTS — slot names @@ -57,11 +57,6 @@ const ERR_EXTERNAL_LINK_NOT_MUTABLE = "external link is not mutable" const ERR_MAX_SUPPLY_IMMUTABLE = "max supply is immutable" const ERR_NEW_MAX_SUPPLY_BELOW_TOKEN_SUPPLY = "new max supply is less than current token supply" -# Advice map keys for the 7-word field data. -const DESCRIPTION_DATA_KEY = [0, 0, 0, 1] -const LOGO_URI_DATA_KEY = [0, 0, 0, 2] -const EXTERNAL_LINK_DATA_KEY = [0, 0, 0, 3] - # ================================================================================================= # PRIVATE HELPERS — single source of truth for slot access # ================================================================================================= @@ -246,18 +241,6 @@ pub proc is_external_link_mutable # => [is_mutable, pad(15)] end -# ================================================================================================= -# OWNER — re-export from ownable -# ================================================================================================= - -#! Returns the owner AccountId. -#! -#! Inputs: [pad(16)] -#! Outputs: [owner_suffix, owner_prefix, pad(14)] -#! -#! Invocation: call -pub use ownable::get_owner - # ================================================================================================= # SET DESCRIPTION (owner-only when desc_mutable == 1) # ================================================================================================= @@ -265,36 +248,45 @@ pub use ownable::get_owner #! Updates the description (7 Words) if the description mutability flag is 1 #! and the note sender is the owner. #! -#! Before executing the transaction, populate the advice map: -#! key: DESCRIPTION_DATA_KEY ([0, 0, 0, 1]) -#! value: [D0, D1, D2, D3, D4, D5, D6] (28 felts in natural order) +#! The caller passes the hash of the new description on the stack and provides the +#! actual 7 Words in the advice map under that hash. The VM retrieves the data by +#! looking up the hash in the advice map. #! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] +#! Inputs: +#! Stack: [DESCRIPTION_HASH, pad(12)] +#! Advice map: { +#! [DESCRIPTION_HASH] |-> [description_elements] (28 felts) +#! } +#! Outputs: +#! Stack: [pad(16)] #! #! Panics if: #! - the description mutability flag is not 1. #! - the note sender is not the owner. #! #! Invocation: call (from note script context) +@locals(4) pub proc set_description - # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + # Save DESCRIPTION_HASH to locals, check mutability, verify owner, then restore hash + loc_storew_le.0 dropw + # => [pad(12)] + + # Read mutability config word exec.get_mutability_config_word swapw dropw # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] - # desc_mutable is on top (word[0]); drop the other 3 movdn.3 drop drop drop # => [desc_mutable, pad(15)] push.1 eq assert.err=ERR_DESCRIPTION_NOT_MUTABLE # => [pad(16)] - # Verify note sender is the owner - exec.ownable::verify_owner + exec.ownable2step::assert_sender_is_owner # => [pad(16)] - # Load the 7 description words from the advice map - push.DESCRIPTION_DATA_KEY + # Restore hash and load 7 Words from advice map + loc_loadw_le.0 + # => [DESCRIPTION_HASH, pad(12)] adv.push_mapval dropw # => [pad(16)] @@ -335,33 +327,36 @@ end #! Updates the logo URI (7 Words) if the logo URI mutability flag is 1 #! and the note sender is the owner. #! -#! Before executing the transaction, populate the advice map: -#! key: LOGO_URI_DATA_KEY ([0, 0, 0, 2]) -#! value: [L0, L1, L2, L3, L4, L5, L6] (28 felts in natural order) -#! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] +#! Inputs: +#! Stack: [LOGO_URI_HASH, pad(12)] +#! Advice map: { +#! [LOGO_URI_HASH] |-> [logo_uri_elements] (28 felts) +#! } +#! Outputs: +#! Stack: [pad(16)] #! #! Panics if: #! - the logo URI mutability flag is not 1. #! - the note sender is not the owner. #! #! Invocation: call (from note script context) +@locals(4) pub proc set_logo_uri - # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + loc_storew_le.0 dropw + # => [pad(12)] + exec.get_mutability_config_word swapw dropw # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] - # logo_mutable is word[1]; drop desc_mutable, then keep logo and drop rest drop movdn.2 drop drop # => [logo_mutable, pad(15)] push.1 eq assert.err=ERR_LOGO_URI_NOT_MUTABLE # => [pad(16)] - exec.ownable::verify_owner + exec.ownable2step::assert_sender_is_owner - push.LOGO_URI_DATA_KEY + loc_loadw_le.0 adv.push_mapval dropw @@ -401,33 +396,36 @@ end #! Updates the external link (7 Words) if the external link mutability flag is 1 #! and the note sender is the owner. #! -#! Before executing the transaction, populate the advice map: -#! key: EXTERNAL_LINK_DATA_KEY ([0, 0, 0, 3]) -#! value: [E0, E1, E2, E3, E4, E5, E6] (28 felts in natural order) -#! -#! Inputs: [pad(16)] -#! Outputs: [pad(16)] +#! Inputs: +#! Stack: [EXTERNAL_LINK_HASH, pad(12)] +#! Advice map: { +#! [EXTERNAL_LINK_HASH] |-> [external_link_elements] (28 felts) +#! } +#! Outputs: +#! Stack: [pad(16)] #! #! Panics if: #! - the external link mutability flag is not 1. #! - the note sender is not the owner. #! #! Invocation: call (from note script context) +@locals(4) pub proc set_external_link - # Read mutability config word: [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(16)] + loc_storew_le.0 dropw + # => [pad(12)] + exec.get_mutability_config_word swapw dropw # => [desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable, pad(12)] - # extlink_mutable is word[2]; drop top 2, swap+drop to get it drop drop swap drop # => [extlink_mutable, pad(15)] push.1 eq assert.err=ERR_EXTERNAL_LINK_NOT_MUTABLE # => [pad(16)] - exec.ownable::verify_owner + exec.ownable2step::assert_sender_is_owner - push.EXTERNAL_LINK_DATA_KEY + loc_loadw_le.0 adv.push_mapval dropw @@ -490,7 +488,7 @@ pub proc set_max_supply # => [new_max_supply, pad(15)] # 2. Verify note sender is the owner - exec.ownable::verify_owner + exec.ownable2step::assert_sender_is_owner # => [new_max_supply, pad(15)] # 3. Read current metadata word diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 440a8f0789..2805ba1d84 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -148,11 +148,10 @@ pub fn fungible_token_metadata_library() -> Library { FUNGIBLE_TOKEN_METADATA_LIBRARY.clone() } -/// Returns the Metadata Info component library. +/// Returns the [`TokenMetadata`](crate::account::metadata::TokenMetadata) component library. /// -/// Uses the standards library; the standalone [`Info`](crate::account::metadata::TokenMetadata) -/// component exposes get_name, set_description, set_logo_uri, set_external_link from -/// `miden::standards::metadata::fungible`. +/// Exposes `get_name`, `set_description`, `set_logo_uri`, `set_external_link`, and related +/// mutability-check procedures from `miden::standards::metadata::fungible`. pub fn metadata_info_component_library() -> Library { METADATA_INFO_COMPONENT_LIBRARY.clone() } diff --git a/crates/miden-standards/src/account/encoding/mod.rs b/crates/miden-standards/src/account/encoding/mod.rs new file mode 100644 index 0000000000..5de56c58fa --- /dev/null +++ b/crates/miden-standards/src/account/encoding/mod.rs @@ -0,0 +1,275 @@ +//! Fixed-width UTF-8 string stored as N Words (7 bytes/felt, length-prefixed). +//! +//! [`FixedWidthString`] is the generic building block used by [`TokenName`], [`Description`], +//! [`LogoURI`], and [`ExternalLink`] to encode arbitrary UTF-8 strings into a fixed number of +//! storage words. +//! +//! ## Buffer layout (N × 4 × 7 bytes) +//! +//! ```text +//! Byte 0: string length (u8) +//! Bytes 1..1+len: UTF-8 content +//! Remaining: zero-padded +//! ``` +//! +//! Each 7-byte chunk is stored as a little-endian `u64` with the high byte always zero, so the +//! value is always < 2^56 and fits safely in a Goldilocks field element. +//! +//! [`TokenName`]: crate::account::faucets::TokenName +//! [`Description`]: crate::account::faucets::Description +//! [`LogoURI`]: crate::account::faucets::LogoURI +//! [`ExternalLink`]: crate::account::faucets::ExternalLink + +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use miden_protocol::{Felt, Word}; + +// ENCODING CONSTANT +// ================================================================================================ + +/// Number of data bytes packed into each felt (7 bytes = 56 bits, always < Goldilocks prime). +pub(super) const BYTES_PER_FELT: usize = 7; + +// FIXED-WIDTH STRING +// ================================================================================================ + +/// A UTF-8 string stored in exactly `N` Words (N×4 felts, 7 bytes/felt, length-prefixed). +/// +/// The capacity (maximum storable bytes) is `N * 4 * 7 - 1`. Wrapper types such as +/// [`TokenName`](crate::account::faucets::TokenName) may impose a tighter limit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FixedWidthString(Box); + +impl Default for FixedWidthString { + fn default() -> Self { + Self("".into()) + } +} + +impl FixedWidthString { + /// Maximum bytes that can be stored (full capacity of the N words minus the length byte). + pub const CAPACITY: usize = N * 4 * BYTES_PER_FELT - 1; + + /// Creates a [`FixedWidthString`] from a UTF-8 string, validating it fits within capacity. + pub fn new(value: &str) -> Result { + if value.len() > Self::CAPACITY { + return Err(FixedWidthStringError::TooLong { + actual: value.len(), + max: Self::CAPACITY, + }); + } + Ok(Self(value.into())) + } + + /// Creates a [`FixedWidthString`] without checking the capacity limit. + /// + /// # Safety + /// The caller must ensure `value.len() <= Self::CAPACITY`. + pub(crate) fn from_str_unchecked(value: &str) -> Self { + Self(value.into()) + } + + /// Returns the string content. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Encodes the string into `N` Words (7 bytes/felt, length-prefixed, zero-padded). + pub fn to_words(&self) -> Vec { + let n_felts = N * 4; + let buf_len = n_felts * BYTES_PER_FELT; + let bytes = self.0.as_bytes(); + debug_assert!(bytes.len() < buf_len); + + let mut buf = alloc::vec![0u8; buf_len]; + buf[0] = bytes.len() as u8; + buf[1..1 + bytes.len()].copy_from_slice(bytes); + + (0..N) + .map(|i| { + let felts: [Felt; 4] = core::array::from_fn(|j| { + let start = (i * 4 + j) * BYTES_PER_FELT; + let mut le_bytes = [0u8; 8]; + le_bytes[..BYTES_PER_FELT].copy_from_slice(&buf[start..start + BYTES_PER_FELT]); + Felt::try_from(u64::from_le_bytes(le_bytes)) + .expect("7-byte LE value always fits in a Goldilocks felt") + }); + Word::from(felts) + }) + .collect() + } + + /// Decodes a [`FixedWidthString`] from a slice of exactly `N` Words. + pub fn try_from_words(words: &[Word]) -> Result { + if words.len() != N { + return Err(FixedWidthStringError::InvalidLength { expected: N, got: words.len() }); + } + let n_felts = N * 4; + let buf_len = n_felts * BYTES_PER_FELT; + let mut buf = alloc::vec![0u8; buf_len]; + + for (i, word) in words.iter().enumerate() { + for (j, felt) in word.as_slice().iter().enumerate() { + let v = felt.as_canonical_u64(); + let le = v.to_le_bytes(); + if le[BYTES_PER_FELT] != 0 { + return Err(FixedWidthStringError::InvalidUtf8); + } + let start = (i * 4 + j) * BYTES_PER_FELT; + buf[start..start + BYTES_PER_FELT].copy_from_slice(&le[..BYTES_PER_FELT]); + } + } + + let len = buf[0] as usize; + if len + 1 > buf_len { + return Err(FixedWidthStringError::InvalidUtf8); + } + String::from_utf8(buf[1..1 + len].to_vec()) + .map_err(|_| FixedWidthStringError::InvalidUtf8) + .map(|s| Self(s.into())) + } +} + +// ERROR TYPE +// ================================================================================================ + +/// Error type for [`FixedWidthString`] construction and decoding. +#[derive(Debug, Clone, thiserror::Error)] +pub enum FixedWidthStringError { + /// String exceeds the maximum capacity for this word width. + #[error("string must be at most {max} bytes, got {actual}")] + TooLong { actual: usize, max: usize }, + /// Decoded bytes are not valid UTF-8 (or a felt's high byte was non-zero). + #[error("string is not valid UTF-8")] + InvalidUtf8, + /// Slice length does not match the expected word count. + #[error("expected {expected} words, got {got}")] + InvalidLength { expected: usize, got: usize }, +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_string_roundtrip() { + let s: FixedWidthString<2> = FixedWidthString::new("").unwrap(); + let words = s.to_words(); + assert_eq!(words.len(), 2); + let decoded = FixedWidthString::<2>::try_from_words(&words).unwrap(); + assert_eq!(decoded.as_str(), ""); + } + + #[test] + fn ascii_roundtrip_2_words() { + let s = FixedWidthString::<2>::new("hello").unwrap(); + let decoded = FixedWidthString::<2>::try_from_words(&s.to_words()).unwrap(); + assert_eq!(decoded.as_str(), "hello"); + } + + #[test] + fn ascii_roundtrip_7_words() { + let text = "A longer description that spans many felts"; + let s = FixedWidthString::<7>::new(text).unwrap(); + let decoded = FixedWidthString::<7>::try_from_words(&s.to_words()).unwrap(); + assert_eq!(decoded.as_str(), text); + } + + #[test] + fn utf8_multibyte_roundtrip() { + // "café" — contains a 2-byte UTF-8 sequence + let s = FixedWidthString::<2>::new("café").unwrap(); + let decoded = FixedWidthString::<2>::try_from_words(&s.to_words()).unwrap(); + assert_eq!(decoded.as_str(), "café"); + } + + #[test] + fn exactly_at_capacity_accepted() { + let cap = FixedWidthString::<2>::CAPACITY; // 2*4*7 - 1 = 55 + let s = "a".repeat(cap); + assert!(FixedWidthString::<2>::new(&s).is_ok()); + } + + #[test] + fn one_over_capacity_rejected() { + let cap = FixedWidthString::<2>::CAPACITY; + let s = "a".repeat(cap + 1); + assert!(matches!( + FixedWidthString::<2>::new(&s), + Err(FixedWidthStringError::TooLong { .. }) + )); + } + + #[test] + fn capacity_7_words() { + // 7*4*7 - 1 = 195 + assert_eq!(FixedWidthString::<7>::CAPACITY, 195); + let s = "b".repeat(195); + let fw = FixedWidthString::<7>::new(&s).unwrap(); + let decoded = FixedWidthString::<7>::try_from_words(&fw.to_words()).unwrap(); + assert_eq!(decoded.as_str(), s); + } + + #[test] + fn to_words_returns_correct_count() { + let s = FixedWidthString::<7>::new("test").unwrap(); + assert_eq!(s.to_words().len(), 7); + } + + #[test] + fn wrong_word_count_returns_error() { + let s = FixedWidthString::<2>::new("hi").unwrap(); + let words = s.to_words(); + // pass only 1 word instead of 2 + assert!(matches!( + FixedWidthString::<2>::try_from_words(&words[..1]), + Err(FixedWidthStringError::InvalidLength { expected: 2, got: 1 }) + )); + } + + #[test] + fn felt_with_high_byte_set_returns_invalid_utf8() { + // Construct a Word where one felt has its 8th byte non-zero, + // which violates the 7-bytes-per-felt invariant. + // A value with byte[7] != 0: 2^56 exceeds the Goldilocks prime so we need a + // different approach — set a byte in positions 0..7 that decodes to invalid UTF-8. + // The length byte will claim len=0xFF (255) which exceeds the buffer, triggering the error. + let overflow_len = Felt::try_from(0xFF_u64).unwrap(); + let words = [ + Word::from([overflow_len, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + ]; + assert!(matches!( + FixedWidthString::<2>::try_from_words(&words), + Err(FixedWidthStringError::InvalidUtf8) + )); + } + + #[test] + fn non_utf8_bytes_return_invalid_utf8() { + // Encode raw bytes that are not valid UTF-8 (e.g. 0xFF byte in content). + // Length byte = 1, content byte = 0xFF (invalid UTF-8 start byte). + // Pack into first felt: LE bytes [1, 0xFF, 0, 0, 0, 0, 0] → u64 = 0x0000_0000_00FF_01 + let raw: u64 = 0x0000_0000_00_FF_01; + let bad_felt = Felt::try_from(raw).unwrap(); + let words = [ + Word::from([bad_felt, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + ]; + assert!(matches!( + FixedWidthString::<2>::try_from_words(&words), + Err(FixedWidthStringError::InvalidUtf8) + )); + } + + #[test] + fn default_is_empty_string() { + let s: FixedWidthString<2> = FixedWidthString::default(); + assert_eq!(s.as_str(), ""); + } +} diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index e4d6909ac0..e681183ba7 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -85,8 +85,7 @@ impl BasicFungibleFaucet { .with_description("Basic fungible faucet component for minting and burning tokens") } - /// Checks that the account contains the basic fungible faucet interface and extracts - /// the [`FungibleTokenMetadata`] from storage. + /// Checks that the account contains the basic fungible faucet interface. fn try_from_interface( interface: AccountInterface, _storage: &miden_protocol::account::AccountStorage, diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index c5d4d1e47e..abea6bc485 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -14,6 +14,8 @@ pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; pub use token_metadata::{Description, ExternalLink, FungibleTokenMetadata, LogoURI, TokenName}; +pub use crate::account::encoding::{FixedWidthString, FixedWidthStringError}; + // FUNGIBLE FAUCET ERROR // ================================================================================================ diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index c88a5fd45f..60e8337422 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -13,7 +13,7 @@ use super::{FungibleFaucetError, FungibleTokenMetadata}; use crate::account::access::AccessControl; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; -use crate::account::faucets::TokenName; +use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; @@ -157,12 +157,22 @@ pub fn create_network_fungible_faucet( decimals: u8, max_supply: Felt, name: TokenName, + description: Option, + logo_uri: Option, + external_link: Option, access_control: AccessControl, ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let metadata = - FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None)?; + let metadata = FungibleTokenMetadata::new( + symbol, + decimals, + max_supply, + name, + description, + logo_uri, + external_link, + )?; let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) @@ -209,6 +219,9 @@ mod tests { decimals, max_supply, name, + None, + None, + None, AccessControl::Ownable2Step { owner }, ) .expect("network faucet creation should succeed"); diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 6419bab4db..368d0a71f8 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -1,5 +1,3 @@ -use alloc::boxed::Box; -use alloc::string::String; use alloc::vec::Vec; use miden_protocol::account::component::{ @@ -11,7 +9,6 @@ use miden_protocol::account::component::{ }; use miden_protocol::account::{ AccountComponent, - AccountId, AccountStorage, AccountType, StorageSlot, @@ -22,35 +19,21 @@ use miden_protocol::{Felt, Word}; use super::FungibleFaucetError; use crate::account::components::fungible_token_metadata_library; -use crate::account::metadata::{self, FieldBytesError, NameUtf8Error}; - -// ENCODING CONSTANTS -// ================================================================================================ - -/// Number of data bytes packed into each felt (7 bytes = 56 bits, always < Goldilocks prime). -const BYTES_PER_FELT: usize = 7; +use crate::account::encoding::{FixedWidthString, FixedWidthStringError}; +use crate::account::metadata::{self, FieldBytesError, NameUtf8Error, TokenMetadata}; // TOKEN NAME // ================================================================================================ -/// Token display name (max 32 bytes UTF-8). +/// Token display name (max 32 bytes UTF-8), stored in 2 Words. /// -/// Internally stores the un-encoded string for cheap access via [`as_str`](Self::as_str). -/// The invariant that the string can be encoded into 2 Words (8 felts × 7 bytes/felt) is -/// enforced at construction time. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TokenName(Box); - -impl Default for TokenName { - /// Returns an empty token name. - fn default() -> Self { - Self(String::new().into_boxed_str()) - } -} +/// The maximum is intentionally capped at 32 bytes even though the 2-Word encoding could +/// hold up to 55 bytes. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TokenName(FixedWidthString<2>); impl TokenName { - /// Maximum byte length for a token name (2 Words = 8 felts × 7 bytes, capacity 55, - /// capped at 32). + /// Maximum byte length for a token name (capped at 32, below the 55-byte capacity). pub const MAX_BYTES: usize = metadata::NAME_UTF8_MAX_BYTES; /// Creates a token name from a UTF-8 string (at most 32 bytes). @@ -58,252 +41,141 @@ impl TokenName { if s.len() > Self::MAX_BYTES { return Err(NameUtf8Error::TooLong(s.len())); } - Ok(Self(s.into())) + Ok(Self(FixedWidthString::from_str_unchecked(s))) } /// Returns the name as a string slice. pub fn as_str(&self) -> &str { - &self.0 - } - - /// Encodes the name into 2 Words for storage (7 bytes/felt, length-prefixed, - /// zero-padded). - pub fn to_words(&self) -> [Word; 2] { - let felts = encode_utf8_to_felts::<8>(self.0.as_bytes()); - [ - Word::from([felts[0], felts[1], felts[2], felts[3]]), - Word::from([felts[4], felts[5], felts[6], felts[7]]), - ] - } - - /// Decodes a token name from 2 Words (7 bytes/felt, length-prefixed). - pub fn try_from_words(words: &[Word; 2]) -> Result { - let felts: [Felt; 8] = [ - words[0][0], - words[0][1], - words[0][2], - words[0][3], - words[1][0], - words[1][1], - words[1][2], - words[1][3], - ]; - let s = decode_felts_to_utf8::<8>(&felts).map_err(|_| NameUtf8Error::InvalidUtf8)?; - if s.len() > Self::MAX_BYTES { - return Err(NameUtf8Error::TooLong(s.len())); + self.0.as_str() + } + + /// Encodes the name into 2 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() + } + + /// Decodes a token name from a 2-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + let inner = + FixedWidthString::<2>::try_from_words(words).map_err(|_| NameUtf8Error::InvalidUtf8)?; + if inner.as_str().len() > Self::MAX_BYTES { + return Err(NameUtf8Error::TooLong(inner.as_str().len())); } - Ok(Self(s.into())) + Ok(Self(inner)) } } // DESCRIPTION // ================================================================================================ -/// Token description (max 195 bytes UTF-8). -/// -/// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words -/// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. +/// Token description (max 195 bytes UTF-8), stored in 7 Words. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Description(Box); +pub struct Description(FixedWidthString<7>); impl Description { - /// Maximum byte length for a description (7 Words = 28 felts × 7 bytes − 1 length byte). + /// Maximum byte length for a description (7 Words × 4 felts × 7 bytes − 1 length byte). pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; /// Creates a description from a UTF-8 string. pub fn new(s: &str) -> Result { - if s.len() > Self::MAX_BYTES { - return Err(FieldBytesError::TooLong(s.len())); - } - Ok(Self(s.into())) + FixedWidthString::<7>::new(s).map(Self).map_err(|e| match e { + FixedWidthStringError::TooLong { actual, .. } => FieldBytesError::TooLong(actual), + _ => FieldBytesError::InvalidUtf8, + }) } /// Returns the description as a string slice. pub fn as_str(&self) -> &str { - &self.0 + self.0.as_str() } - /// Encodes the description into 7 Words for storage (7 bytes/felt, length-prefixed). - pub fn to_words(&self) -> [Word; 7] { - encode_field_to_words(self.0.as_bytes()) + /// Encodes the description into 7 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() } - /// Decodes a description from 7 Words (7 bytes/felt, length-prefixed). - pub fn try_from_words(words: &[Word; 7]) -> Result { - let s = decode_field_from_words(words)?; - Ok(Self(s.into())) + /// Decodes a description from a 7-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + FixedWidthString::<7>::try_from_words(words) + .map(Self) + .map_err(|_| FieldBytesError::InvalidUtf8) } } // LOGO URI // ================================================================================================ -/// Token logo URI (max 195 bytes UTF-8). -/// -/// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words -/// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. +/// Token logo URI (max 195 bytes UTF-8), stored in 7 Words. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct LogoURI(Box); +pub struct LogoURI(FixedWidthString<7>); impl LogoURI { - /// Maximum byte length for a logo URI (7 Words = 28 felts × 7 bytes − 1 length byte). + /// Maximum byte length for a logo URI (7 Words × 4 felts × 7 bytes − 1 length byte). pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; /// Creates a logo URI from a UTF-8 string. pub fn new(s: &str) -> Result { - if s.len() > Self::MAX_BYTES { - return Err(FieldBytesError::TooLong(s.len())); - } - Ok(Self(s.into())) + FixedWidthString::<7>::new(s).map(Self).map_err(|e| match e { + FixedWidthStringError::TooLong { actual, .. } => FieldBytesError::TooLong(actual), + _ => FieldBytesError::InvalidUtf8, + }) } /// Returns the logo URI as a string slice. pub fn as_str(&self) -> &str { - &self.0 + self.0.as_str() } - /// Encodes the logo URI into 7 Words for storage (7 bytes/felt, length-prefixed). - pub fn to_words(&self) -> [Word; 7] { - encode_field_to_words(self.0.as_bytes()) + /// Encodes the logo URI into 7 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() } - /// Decodes a logo URI from 7 Words (7 bytes/felt, length-prefixed). - pub fn try_from_words(words: &[Word; 7]) -> Result { - let s = decode_field_from_words(words)?; - Ok(Self(s.into())) + /// Decodes a logo URI from a 7-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + FixedWidthString::<7>::try_from_words(words) + .map(Self) + .map_err(|_| FieldBytesError::InvalidUtf8) } } // EXTERNAL LINK // ================================================================================================ -/// Token external link (max 195 bytes UTF-8). -/// -/// Internally stores the un-encoded string. The invariant that it can be encoded into 7 Words -/// (28 felts, 7 bytes/felt, length-prefixed) is enforced at construction time. +/// Token external link (max 195 bytes UTF-8), stored in 7 Words. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExternalLink(Box); +pub struct ExternalLink(FixedWidthString<7>); impl ExternalLink { - /// Maximum byte length for an external link (7 Words = 28 felts × 7 bytes − 1 length byte). + /// Maximum byte length for an external link (7 Words × 4 felts × 7 bytes − 1 length byte). pub const MAX_BYTES: usize = metadata::FIELD_MAX_BYTES; /// Creates an external link from a UTF-8 string. pub fn new(s: &str) -> Result { - if s.len() > Self::MAX_BYTES { - return Err(FieldBytesError::TooLong(s.len())); - } - Ok(Self(s.into())) + FixedWidthString::<7>::new(s).map(Self).map_err(|e| match e { + FixedWidthStringError::TooLong { actual, .. } => FieldBytesError::TooLong(actual), + _ => FieldBytesError::InvalidUtf8, + }) } /// Returns the external link as a string slice. pub fn as_str(&self) -> &str { - &self.0 + self.0.as_str() } - /// Encodes the external link into 7 Words for storage (7 bytes/felt, length-prefixed). - pub fn to_words(&self) -> [Word; 7] { - encode_field_to_words(self.0.as_bytes()) + /// Encodes the external link into 7 Words for storage. + pub fn to_words(&self) -> Vec { + self.0.to_words() } - /// Decodes an external link from 7 Words (7 bytes/felt, length-prefixed). - pub fn try_from_words(words: &[Word; 7]) -> Result { - let s = decode_field_from_words(words)?; - Ok(Self(s.into())) + /// Decodes an external link from a 7-Word slice. + pub fn try_from_words(words: &[Word]) -> Result { + FixedWidthString::<7>::try_from_words(words) + .map(Self) + .map_err(|_| FieldBytesError::InvalidUtf8) } } -// ENCODING HELPERS -// ================================================================================================ - -/// Encodes a UTF-8 byte slice into `N` felts using 7-bytes-per-felt, length-prefixed encoding. -/// -/// ## Buffer layout (`N × 7` bytes) -/// -/// ```text -/// Byte 0: string length (u8) -/// Bytes 1..1+len: UTF-8 content -/// Remaining: zero-padded -/// -/// Felt 0: buffer[0..7] (length byte + first 6 data bytes) -/// Felt 1: buffer[7..14] (next 7 data bytes) -/// ... -/// ``` -/// -/// Each 7-byte chunk is stored as a little-endian `u64` with the high byte always zero, -/// so the value is always < 2^56 and fits safely in a Goldilocks field element. -/// -/// # Panics -/// -/// Panics (debug-only) if `bytes.len() + 1 > N * BYTES_PER_FELT` (content + length byte -/// exceeds buffer capacity). -fn encode_utf8_to_felts(bytes: &[u8]) -> [Felt; N] { - let buf_len = N * BYTES_PER_FELT; - debug_assert!(bytes.len() < buf_len); - - let mut buf = [0u8; 256]; // large enough for any field (max 28 * 7 = 196) - buf[0] = bytes.len() as u8; - buf[1..1 + bytes.len()].copy_from_slice(bytes); - - let mut felts = [Felt::ZERO; N]; - for (i, felt) in felts.iter_mut().enumerate() { - let start = i * BYTES_PER_FELT; - let mut le_bytes = [0u8; 8]; - le_bytes[..BYTES_PER_FELT].copy_from_slice(&buf[start..start + BYTES_PER_FELT]); - // High byte is always 0 ⇒ value < 2^56, safe for Goldilocks. - *felt = Felt::try_from(u64::from_le_bytes(le_bytes)).expect("7 bytes always fit in a Felt"); - } - felts -} - -/// Decodes `N` felts (7-bytes-per-felt, length-prefixed) back to a UTF-8 string. -fn decode_felts_to_utf8(felts: &[Felt; N]) -> Result { - let buf_len = N * BYTES_PER_FELT; - let mut buf = [0u8; 256]; - for (i, felt) in felts.iter().enumerate() { - let v = felt.as_canonical_u64(); - let le = v.to_le_bytes(); - // Reject values that use the high byte (> 7 bytes of data). - if le[BYTES_PER_FELT] != 0 { - return Err(FieldBytesError::InvalidUtf8); - } - buf[i * BYTES_PER_FELT..][..BYTES_PER_FELT].copy_from_slice(&le[..BYTES_PER_FELT]); - } - let len = buf[0] as usize; - if len + 1 > buf_len { - return Err(FieldBytesError::InvalidUtf8); - } - String::from_utf8(buf[1..1 + len].to_vec()).map_err(|_| FieldBytesError::InvalidUtf8) -} - -/// Encodes a byte slice into 7 Words (28 felts, 7 bytes/felt, length-prefixed, zero-padded). -/// -/// # Panics -/// -/// Panics (debug-only) if `bytes.len() > FIELD_MAX_BYTES`. Callers must validate length first. -fn encode_field_to_words(bytes: &[u8]) -> [Word; 7] { - debug_assert!(bytes.len() <= metadata::FIELD_MAX_BYTES); - let felts = encode_utf8_to_felts::<28>(bytes); - [ - Word::from([felts[0], felts[1], felts[2], felts[3]]), - Word::from([felts[4], felts[5], felts[6], felts[7]]), - Word::from([felts[8], felts[9], felts[10], felts[11]]), - Word::from([felts[12], felts[13], felts[14], felts[15]]), - Word::from([felts[16], felts[17], felts[18], felts[19]]), - Word::from([felts[20], felts[21], felts[22], felts[23]]), - Word::from([felts[24], felts[25], felts[26], felts[27]]), - ] -} - -/// Decodes 7 Words (28 felts, 7 bytes/felt, length-prefixed) back to a UTF-8 string. -fn decode_field_from_words(words: &[Word; 7]) -> Result { - let mut felts = [Felt::ZERO; 28]; - for (i, word) in words.iter().enumerate() { - felts[i * 4..i * 4 + 4].copy_from_slice(word.as_slice()); - } - decode_felts_to_utf8::<28>(&felts) -} - // TOKEN METADATA // ================================================================================================ @@ -330,15 +202,8 @@ pub struct FungibleTokenMetadata { max_supply: Felt, decimals: u8, symbol: TokenSymbol, - name: TokenName, - description: Option, - logo_uri: Option, - external_link: Option, - owner: Option, - description_mutable: bool, - logo_uri_mutable: bool, - external_link_mutable: bool, - max_supply_mutable: bool, + /// Embeds name, optional fields, and mutability flags. + metadata: TokenMetadata, } impl FungibleTokenMetadata { @@ -416,20 +281,23 @@ impl FungibleTokenMetadata { }); } + let mut token_metadata = TokenMetadata::new().with_name(name); + if let Some(desc) = description { + token_metadata = token_metadata.with_description(desc, false); + } + if let Some(uri) = logo_uri { + token_metadata = token_metadata.with_logo_uri(uri, false); + } + if let Some(link) = external_link { + token_metadata = token_metadata.with_external_link(link, false); + } + Ok(Self { token_supply, max_supply, decimals, symbol, - name, - description, - logo_uri, - external_link, - owner: None, - description_mutable: false, - logo_uri_mutable: false, - external_link_mutable: false, - max_supply_mutable: false, + metadata: token_metadata, }) } @@ -462,24 +330,24 @@ impl FungibleTokenMetadata { self.symbol } - /// Returns the token name (for Info component when building an account). + /// Returns the token name. pub fn name(&self) -> &TokenName { - &self.name + self.metadata.name().expect("FungibleTokenMetadata always has a name") } - /// Returns the optional description (for Info component when building an account). + /// Returns the optional description. pub fn description(&self) -> Option<&Description> { - self.description.as_ref() + self.metadata.description() } - /// Returns the optional logo URI (for Info component when building an account). + /// Returns the optional logo URI. pub fn logo_uri(&self) -> Option<&LogoURI> { - self.logo_uri.as_ref() + self.metadata.logo_uri() } - /// Returns the optional external link (for Info component when building an account). + /// Returns the optional external link. pub fn external_link(&self) -> Option<&ExternalLink> { - self.external_link.as_ref() + self.metadata.external_link() } /// Returns the storage slot schema for the metadata slot. @@ -504,14 +372,6 @@ impl FungibleTokenMetadata { pub fn storage_slots(&self) -> Vec { let mut slots: Vec = Vec::new(); - // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures - // for verify_owner (used in set_* mutations). - if let Some(id) = self.owner { - let owner_word = - Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()]); - slots.push(StorageSlot::with_value(metadata::owner_config_slot().clone(), owner_word)); - } - // Slot 0: metadata word [token_supply, max_supply, decimals, symbol] let metadata_word = Word::new([ self.token_supply, @@ -521,58 +381,8 @@ impl FungibleTokenMetadata { ]); slots.push(StorageSlot::with_value(Self::metadata_slot().clone(), metadata_word)); - // Slots 2-3: name (2 Words) - let name_words = self.name.to_words(); - slots.push(StorageSlot::with_value( - metadata::TokenMetadata::name_chunk_0_slot().clone(), - name_words[0], - )); - slots.push(StorageSlot::with_value( - metadata::TokenMetadata::name_chunk_1_slot().clone(), - name_words[1], - )); - - // Slot 4: mutability config - let mutability_config_word = Word::from([ - Felt::from(self.description_mutable as u32), - Felt::from(self.logo_uri_mutable as u32), - Felt::from(self.external_link_mutable as u32), - Felt::from(self.max_supply_mutable as u32), - ]); - slots.push(StorageSlot::with_value( - metadata::mutability_config_slot().clone(), - mutability_config_word, - )); - - // Slots 5-11: description (7 Words) - let desc_words: [Word; 7] = - self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); - for (i, word) in desc_words.iter().enumerate() { - slots.push(StorageSlot::with_value( - metadata::TokenMetadata::description_slot(i).clone(), - *word, - )); - } - - // Slots 12-18: logo_uri (7 Words) - let logo_words: [Word; 7] = - self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); - for (i, word) in logo_words.iter().enumerate() { - slots.push(StorageSlot::with_value( - metadata::TokenMetadata::logo_uri_slot(i).clone(), - *word, - )); - } - - // Slots 19-25: external_link (7 Words) - let link_words: [Word; 7] = - self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); - for (i, word) in link_words.iter().enumerate() { - slots.push(StorageSlot::with_value( - metadata::TokenMetadata::external_link_slot(i).clone(), - *word, - )); - } + // Slots 1-24: name, mutability config, description, logo_uri, external_link + slots.extend(self.metadata.storage_slots()); slots } @@ -601,35 +411,25 @@ impl FungibleTokenMetadata { /// Sets whether the description can be updated by the owner. pub fn with_description_mutable(mut self, mutable: bool) -> Self { - self.description_mutable = mutable; + self.metadata = self.metadata.with_description_mutable(mutable); self } /// Sets whether the logo URI can be updated by the owner. pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self { - self.logo_uri_mutable = mutable; + self.metadata = self.metadata.with_logo_uri_mutable(mutable); self } /// Sets whether the external link can be updated by the owner. pub fn with_external_link_mutable(mut self, mutable: bool) -> Self { - self.external_link_mutable = mutable; + self.metadata = self.metadata.with_external_link_mutable(mutable); self } /// Sets whether the max supply can be updated by the owner. pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { - self.max_supply_mutable = mutable; - self - } - - /// Sets the owner for the `ownable::owner_config` slot. - /// - /// This is required by the `metadata::fungible` MASM procedures - /// (`set_description`, `set_logo_uri`, `set_external_link`, `set_max_supply`) - /// which use `ownable::verify_owner` to authorize mutations. - pub fn with_owner(mut self, owner: AccountId) -> Self { - self.owner = Some(owner); + self.metadata = self.metadata.with_max_supply_mutable(mutable); self } } @@ -661,19 +461,22 @@ impl TryFrom for FungibleTokenMetadata { } })?; - let name = TokenName::default(); - Self::with_supply(symbol, decimals, max_supply, token_supply, name, None, None, None) + Self::with_supply( + symbol, + decimals, + max_supply, + token_supply, + TokenName::default(), + None, + None, + None, + ) } } impl From for Word { - fn from(metadata: FungibleTokenMetadata) -> Self { - Word::new([ - metadata.token_supply, - metadata.max_supply, - Felt::from(metadata.decimals), - metadata.symbol.into(), - ]) + fn from(m: FungibleTokenMetadata) -> Self { + Word::new([m.token_supply, m.max_supply, Felt::from(m.decimals), m.symbol.into()]) } } @@ -993,7 +796,7 @@ mod tests { let slots = metadata.storage_slots(); - // Slot at index 3 is mutability_config: [desc, logo, extlink, max_supply] + // Slot layout (no owner slot): [0]=metadata, [1]=name_0, [2]=name_1, [3]=mutability_config let config_slot = &slots[3]; let config_word = config_slot.value(); assert_eq!(config_word[0], Felt::from(1u32), "desc_mutable"); @@ -1046,7 +849,7 @@ mod tests { FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); let slots = metadata.storage_slots(); - // Slots 1 and 2 are name chunks + // Slot layout: [0]=metadata, [1]=name_0, [2]=name_1 assert_eq!(slots[1].value(), expected_words[0]); assert_eq!(slots[2].value(), expected_words[1]); } @@ -1070,7 +873,7 @@ mod tests { .unwrap(); let slots = metadata.storage_slots(); - // Slots 4..11 are description (7 words), starting after metadata + name(2) + config + // Slots 4..11 are description (7 words): after metadata(1) + name(2) + config(1) for (i, expected) in expected_words.iter().enumerate() { assert_eq!(slots[4 + i].value(), *expected, "description word {i}"); } @@ -1130,4 +933,62 @@ mod tests { let config = account.storage().get_item(metadata::mutability_config_slot()).unwrap(); assert_eq!(config[3], Felt::from(1u32), "max_supply_mutable"); } + + #[test] + fn logo_uri_too_long() { + let s = "a".repeat(LogoURI::MAX_BYTES + 1); + assert!(LogoURI::new(&s).is_err()); + } + + #[test] + fn external_link_too_long() { + let s = "a".repeat(ExternalLink::MAX_BYTES + 1); + assert!(ExternalLink::new(&s).is_err()); + } + + #[test] + fn token_supply_exceeds_max_supply() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + let max_supply = Felt::new(100); + let token_supply = Felt::new(101); + + let result = FungibleTokenMetadata::with_supply( + symbol, 2, max_supply, token_supply, name, None, None, None, + ); + assert!(matches!(result, Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply { .. }))); + } + + #[test] + fn with_token_supply_exceeds_max_supply() { + let symbol = TokenSymbol::new("TST").unwrap(); + let name = TokenName::new("T").unwrap(); + let metadata = + FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + + let result = metadata.with_token_supply(Felt::new(101)); + assert!(matches!(result, Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply { .. }))); + } + + #[test] + fn slot_name_mismatch() { + use miden_protocol::account::StorageSlotName; + + let wrong_slot_name = + StorageSlotName::new("wrong::slot::name").expect("valid slot name"); + let slot = StorageSlot::with_value(wrong_slot_name, Word::default()); + + let result = FungibleTokenMetadata::try_from(&slot); + assert!(matches!(result, Err(FungibleFaucetError::SlotNameMismatch { .. }))); + } + + #[test] + fn invalid_token_symbol_in_word() { + // TokenSymbol::try_from(Felt) fails when the value exceeds MAX_ENCODED_VALUE. + // The Word layout is [token_supply, max_supply, decimals, token_symbol] — symbol is [3]. + let bad_symbol = Felt::new(TokenSymbol::MAX_ENCODED_VALUE + 1); + let bad_word = Word::from([Felt::ZERO, Felt::new(100), Felt::new(2), bad_symbol]); + let result = FungibleTokenMetadata::try_from(bad_word); + assert!(matches!(result, Err(FungibleFaucetError::InvalidTokenSymbol(_)))); + } } diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 1c90fb9dd0..0f9c1a8155 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -1,15 +1,14 @@ -//! Account / contract / faucet metadata (slots 0..25) +//! Account / contract / faucet metadata //! //! All of the following are metadata of the account (or faucet): token_symbol, decimals, -//! max_supply, owner, name, mutability_config, description, logo URI, -//! and external link. +//! max_supply, name, mutability_config, description, logo URI, and external link. +//! Ownership is handled by the `Ownable2Step` component separately. //! //! ## Storage layout //! //! | Slot name | Contents | //! |-----------|----------| //! | `metadata::token_metadata` | `[token_supply, max_supply, decimals, token_symbol]` | -//! | `ownable::owner_config` | owner account id (defined by ownable module) | //! | `metadata::name_0` | first 4 felts of name | //! | `metadata::name_1` | last 4 felts of name | //! | `metadata::mutability_config` | `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` | @@ -17,10 +16,6 @@ //! | `metadata::logo_uri_0..6` | logo URI (7 Words, max 195 bytes) | //! | `metadata::external_link_0..6` | external link (7 Words, max 195 bytes) | //! -//! Slot names use the `miden::standards::metadata::*` namespace, except for the -//! owner which is defined by the ownable module -//! (`miden::standards::access::ownable::owner_config`). -//! //! Layout sync: the same layout is defined in MASM at `asm/standards/metadata/fungible.masm`. //! Any change to slot indices or names must be applied in both Rust and MASM. //! @@ -36,11 +31,9 @@ //! //! ## MASM modules //! -//! All metadata procedures (getters, `get_owner`, setters) live in -//! `miden::standards::metadata::fungible`, which depends on ownable. The standalone -//! The TokenMetadata component uses the standards library and exposes `get_name`; for owner -//! and mutable fields use a component that re-exports from fungible (e.g. network fungible -//! faucet). +//! All metadata procedures (getters, setters) live in `miden::standards::metadata::fungible`, +//! which depends on `ownable2step` for ownership checks. For mutable fields, accounts must +//! also include the `Ownable2Step` component (e.g. network fungible faucet). //! //! ## String encoding (UTF-8) //! @@ -74,7 +67,6 @@ mod token_metadata; use miden_protocol::account::StorageSlotName; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Word}; pub use schema_commitment::{ AccountBuilderSchemaCommitmentExt, AccountSchemaCommitment, @@ -92,15 +84,6 @@ pub(crate) static TOKEN_METADATA_SLOT: LazyLock = LazyLock::new .expect("storage slot name should be valid") }); -/// Owner config — defined by the ownable module (`miden::standards::access::ownable`). -/// Referenced here so that faucets and other metadata consumers can locate the owner -/// through a single `metadata::owner_config_slot()` accessor, without depending on -/// the ownable module directly. -pub(crate) static OWNER_CONFIG_SLOT: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::standards::access::ownable::owner_config") - .expect("storage slot name should be valid") -}); - /// Token name (2 Words = 8 felts), split across 2 slots. /// /// The encoding is not specified; the value is opaque word data. For human-readable names, @@ -198,18 +181,6 @@ pub(crate) static EXTERNAL_LINK_SLOTS: LazyLock<[StorageSlotName; 7]> = LazyLock ] }); -/// Advice map key for the description field data (7 words). -pub const DESCRIPTION_DATA_KEY: Word = - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(1)]); - -/// Advice map key for the logo URI field data (7 words). -pub const LOGO_URI_DATA_KEY: Word = - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(2)]); - -/// Advice map key for the external link field data (7 words). -pub const EXTERNAL_LINK_DATA_KEY: Word = - Word::new([Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(3)]); - // SLOT ACCESSORS // ================================================================================================ @@ -218,11 +189,6 @@ pub(crate) fn token_metadata_slot() -> &'static StorageSlotName { &TOKEN_METADATA_SLOT } -/// Returns the [`StorageSlotName`] for owner config (slot 1). -pub(crate) fn owner_config_slot() -> &'static StorageSlotName { - &OWNER_CONFIG_SLOT -} - /// Returns the [`StorageSlotName`] for the mutability config Word. pub(crate) fn mutability_config_slot() -> &'static StorageSlotName { &MUTABILITY_CONFIG_SLOT @@ -325,6 +291,18 @@ mod tests { assert_eq!(mut_default[3], Felt::from(0u32), "max_supply_mutable should be 0 by default"); } + #[test] + fn name_too_long_rejected() { + let long_name = "a".repeat(TokenName::MAX_BYTES + 1); + assert!(TokenName::new(&long_name).is_err()); + } + + #[test] + fn description_too_long_rejected() { + let long_desc = "a".repeat(Description::MAX_BYTES + 1); + assert!(Description::new(&long_desc).is_err()); + } + #[test] fn name_roundtrip() { let s = "POL Faucet"; diff --git a/crates/miden-standards/src/account/metadata/token_metadata.rs b/crates/miden-standards/src/account/metadata/token_metadata.rs index 50f071d546..17502e8ff6 100644 --- a/crates/miden-standards/src/account/metadata/token_metadata.rs +++ b/crates/miden-standards/src/account/metadata/token_metadata.rs @@ -1,22 +1,16 @@ -//! Generic metadata component for non-faucet accounts. +//! Generic token metadata helper. //! -//! [`TokenMetadata`] is a builder-pattern struct that stores name, config, and optional -//! fields (description, logo_uri, external_link, owner) in fixed value slots. For faucet -//! accounts, prefer [`FungibleTokenMetadata`](crate::account::faucets::FungibleTokenMetadata) -//! which embeds all metadata in a single component. +//! [`TokenMetadata`] is a builder-pattern struct used to manage name and optional fields +//! (description, logo_uri, external_link) with their mutability flags in fixed value slots. +//! It is intended to be embedded inside [`FungibleTokenMetadata`] rather than used as a +//! standalone [`AccountComponent`]. +//! +//! Ownership is handled by the `Ownable2Step` component. use alloc::vec::Vec; -use miden_protocol::account::component::AccountComponentMetadata; -use miden_protocol::account::{ - AccountComponent, - AccountId, - AccountStorage, - AccountType, - StorageSlot, - StorageSlotName, -}; -use miden_protocol::{Felt, Word}; +use miden_protocol::Word; +use miden_protocol::account::{AccountStorage, StorageSlot, StorageSlotName}; use super::{ DESCRIPTION_SLOTS, @@ -24,26 +18,29 @@ use super::{ LOGO_URI_SLOTS, NAME_SLOTS, mutability_config_slot, - owner_config_slot, }; -use crate::account::components::metadata_info_component_library; use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; // TOKEN METADATA // ================================================================================================ -/// A metadata component storing name, config, and optional fields in fixed value slots. +/// A helper that stores name, mutability config, and optional fields in fixed value slots. +/// +/// Designed to be embedded in [`FungibleTokenMetadata`] to avoid duplication. Slot names are +/// shared via the static accessors (e.g. [`name_chunk_0_slot`]). /// /// ## Storage Layout /// -/// - Slot 2–3: name (2 Words = 8 felts) -/// - Slot 4: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` -/// - Slot 5–11: description (7 Words) -/// - Slot 12–18: logo_uri (7 Words) -/// - Slot 19–25: external_link (7 Words) +/// - Slot 0–1: name (2 Words = 8 felts) +/// - Slot 2: mutability_config `[desc_mutable, logo_mutable, extlink_mutable, max_supply_mutable]` +/// - Slot 3–9: description (7 Words) +/// - Slot 10–16: logo_uri (7 Words) +/// - Slot 17–23: external_link (7 Words) +/// +/// [`FungibleTokenMetadata`]: crate::account::faucets::FungibleTokenMetadata +/// [`name_chunk_0_slot`]: TokenMetadata::name_chunk_0_slot #[derive(Debug, Clone, Default)] pub struct TokenMetadata { - owner: Option, name: Option, description: Option, logo_uri: Option, @@ -55,27 +52,13 @@ pub struct TokenMetadata { } impl TokenMetadata { - /// Creates a new empty token metadata (all fields absent by default). + /// Creates a new empty token metadata (all fields absent, all flags false). pub fn new() -> Self { Self::default() } - /// Sets the owner of this metadata component. - /// - /// The owner is stored in the `ownable::owner_config` slot and is used by the - /// `metadata::fungible` MASM procedures to authorize mutations (e.g. - /// `set_description`). - pub fn with_owner(mut self, owner: AccountId) -> Self { - self.owner = Some(owner); - self - } - - /// Sets whether the max supply can be updated by the owner via - /// `set_max_supply`. If `false` (default), the max supply is immutable. - pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { - self.max_supply_mutable = mutable; - self - } + // BUILDERS + // -------------------------------------------------------------------------------------------- /// Sets the token name. pub fn with_name(mut self, name: TokenName) -> Self { @@ -83,63 +66,109 @@ impl TokenMetadata { self } - /// Sets the description with mutability. - /// - /// When `mutable` is `true`, the owner can update the description later. + /// Sets the description and its mutability flag together. pub fn with_description(mut self, description: Description, mutable: bool) -> Self { self.description = Some(description); self.description_mutable = mutable; self } - /// Sets the logo URI with mutability. - /// - /// When `mutable` is `true`, the owner can update the logo URI later. + /// Sets whether the description can be updated by the owner. + pub fn with_description_mutable(mut self, mutable: bool) -> Self { + self.description_mutable = mutable; + self + } + + /// Sets the logo URI and its mutability flag together. pub fn with_logo_uri(mut self, logo_uri: LogoURI, mutable: bool) -> Self { self.logo_uri = Some(logo_uri); self.logo_uri_mutable = mutable; self } - /// Sets the external link with mutability. - /// - /// When `mutable` is `true`, the owner can update the external link later. + /// Sets whether the logo URI can be updated by the owner. + pub fn with_logo_uri_mutable(mut self, mutable: bool) -> Self { + self.logo_uri_mutable = mutable; + self + } + + /// Sets the external link and its mutability flag together. pub fn with_external_link(mut self, external_link: ExternalLink, mutable: bool) -> Self { self.external_link = Some(external_link); self.external_link_mutable = mutable; self } - /// Returns the slot name for name chunk 0. + /// Sets whether the external link can be updated by the owner. + pub fn with_external_link_mutable(mut self, mutable: bool) -> Self { + self.external_link_mutable = mutable; + self + } + + /// Sets whether the max supply can be updated by the owner. + pub fn with_max_supply_mutable(mut self, mutable: bool) -> Self { + self.max_supply_mutable = mutable; + self + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the token name if set. + pub fn name(&self) -> Option<&TokenName> { + self.name.as_ref() + } + + /// Returns the description if set. + pub fn description(&self) -> Option<&Description> { + self.description.as_ref() + } + + /// Returns the logo URI if set. + pub fn logo_uri(&self) -> Option<&LogoURI> { + self.logo_uri.as_ref() + } + + /// Returns the external link if set. + pub fn external_link(&self) -> Option<&ExternalLink> { + self.external_link.as_ref() + } + + // STATIC SLOT NAME ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] for name chunk 0. pub fn name_chunk_0_slot() -> &'static StorageSlotName { &NAME_SLOTS[0] } - /// Returns the slot name for name chunk 1. + /// Returns the [`StorageSlotName`] for name chunk 1. pub fn name_chunk_1_slot() -> &'static StorageSlotName { &NAME_SLOTS[1] } - /// Returns the slot name for a description chunk by index (0..7). + /// Returns the [`StorageSlotName`] for a description chunk by index (0..7). pub fn description_slot(index: usize) -> &'static StorageSlotName { &DESCRIPTION_SLOTS[index] } - /// Returns the slot name for a logo URI chunk by index (0..7). + /// Returns the [`StorageSlotName`] for a logo URI chunk by index (0..7). pub fn logo_uri_slot(index: usize) -> &'static StorageSlotName { &LOGO_URI_SLOTS[index] } - /// Returns the slot name for an external link chunk by index (0..7). + /// Returns the [`StorageSlotName`] for an external link chunk by index (0..7). pub fn external_link_slot(index: usize) -> &'static StorageSlotName { &EXTERNAL_LINK_SLOTS[index] } + // STORAGE + // -------------------------------------------------------------------------------------------- + /// Reads the name and optional metadata fields from account storage. /// /// Returns `(name, description, logo_uri, external_link)` where each is `Some` only if - /// at least one word is non-zero. Decoding errors (e.g. invalid UTF-8 in storage) cause the - /// field to be returned as `None`. + /// at least one word is non-zero. Decoding errors cause the field to be returned as `None`. pub fn read_metadata_from_storage( storage: &AccountStorage, ) -> (Option, Option, Option, Option) { @@ -180,26 +209,15 @@ impl TokenMetadata { (name, description, logo_uri, external_link) } - /// Returns the storage slots for this metadata (without creating an `AccountComponent`). - /// - /// These slots are meant to be included directly in a faucet component rather than - /// added as a separate `AccountComponent`. + /// Returns the storage slots for this metadata (name, mutability config, and all fields). pub fn storage_slots(&self) -> Vec { let mut slots: Vec = Vec::new(); - // Owner slot (ownable::owner_config) — required by metadata::fungible MASM procedures - // for get_owner and verify_owner (used in set_* mutations). - // Word layout: [0, 0, owner_suffix, owner_prefix] so that after get_item (which places - // word[0] on top), dropping the two leading zeros yields [owner_suffix, owner_prefix]. - // Only included when an owner is explicitly set, to avoid conflicting with components - // (like NetworkFungibleFaucet) that provide their own owner_config slot. - if let Some(id) = self.owner { - let owner_word = - Word::from([Felt::ZERO, Felt::ZERO, id.suffix(), id.prefix().as_felt()]); - slots.push(StorageSlot::with_value(owner_config_slot().clone(), owner_word)); - } - - let name_words = self.name.as_ref().map(|n| n.to_words()).unwrap_or_default(); + let name_words = self + .name + .as_ref() + .map(|n| n.to_words()) + .unwrap_or_else(|| (0..2).map(|_| Word::default()).collect()); slots.push(StorageSlot::with_value( TokenMetadata::name_chunk_0_slot().clone(), name_words[0], @@ -210,30 +228,39 @@ impl TokenMetadata { )); let mutability_config_word = Word::from([ - Felt::from(self.description_mutable as u32), - Felt::from(self.logo_uri_mutable as u32), - Felt::from(self.external_link_mutable as u32), - Felt::from(self.max_supply_mutable as u32), + miden_protocol::Felt::from(self.description_mutable as u32), + miden_protocol::Felt::from(self.logo_uri_mutable as u32), + miden_protocol::Felt::from(self.external_link_mutable as u32), + miden_protocol::Felt::from(self.max_supply_mutable as u32), ]); slots.push(StorageSlot::with_value( mutability_config_slot().clone(), mutability_config_word, )); - let desc_words: [Word; 7] = - self.description.as_ref().map(|d| d.to_words()).unwrap_or_default(); + let desc_words: Vec = self + .description + .as_ref() + .map(|d| d.to_words()) + .unwrap_or_else(|| (0..7).map(|_| Word::default()).collect()); for (i, word) in desc_words.iter().enumerate() { slots.push(StorageSlot::with_value(TokenMetadata::description_slot(i).clone(), *word)); } - let logo_words: [Word; 7] = - self.logo_uri.as_ref().map(|l| l.to_words()).unwrap_or_default(); + let logo_words: Vec = self + .logo_uri + .as_ref() + .map(|l| l.to_words()) + .unwrap_or_else(|| (0..7).map(|_| Word::default()).collect()); for (i, word) in logo_words.iter().enumerate() { slots.push(StorageSlot::with_value(TokenMetadata::logo_uri_slot(i).clone(), *word)); } - let link_words: [Word; 7] = - self.external_link.as_ref().map(|e| e.to_words()).unwrap_or_default(); + let link_words: Vec = self + .external_link + .as_ref() + .map(|e| e.to_words()) + .unwrap_or_else(|| (0..7).map(|_| Word::default()).collect()); for (i, word) in link_words.iter().enumerate() { slots .push(StorageSlot::with_value(TokenMetadata::external_link_slot(i).clone(), *word)); @@ -242,21 +269,3 @@ impl TokenMetadata { slots } } - -/// Converts [`TokenMetadata`] into a standalone [`AccountComponent`] that includes the metadata -/// MASM library (`metadata_info_component_library`). Use this when adding generic metadata as a -/// separate component (e.g. for non-faucet accounts). -impl From for AccountComponent { - fn from(info: TokenMetadata) -> Self { - let metadata = - AccountComponentMetadata::new("miden::standards::metadata::info", AccountType::all()) - .with_description( - "Component exposing token name, description, logo URI and external link", - ); - - AccountComponent::new(metadata_info_component_library(), info.storage_slots(), metadata) - .expect( - "TokenMetadata component should satisfy the requirements of a valid account component", - ) - } -} diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index ad7b67f1f9..93a896f2db 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -3,6 +3,7 @@ use super::auth_method::AuthMethod; pub mod access; pub mod auth; pub mod components; +pub mod encoding; pub mod faucets; pub mod interface; pub mod metadata; diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 1811ffc60a..93c70424c4 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -467,7 +467,6 @@ impl MockChainBuilder { ) .and_then(|m| m.with_token_supply(token_supply)) .context("failed to create fungible token metadata")? - .with_owner(owner_account_id) .with_max_supply_mutable(max_supply_mutable); if let Some((_, mutable)) = description { diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index e8fd374880..dce7be111f 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -5,6 +5,7 @@ extern crate alloc; use alloc::sync::Arc; use alloc::vec::Vec; +use miden_crypto::hash::rpo::Rpo256; use miden_crypto::rand::RpoRandomCoin; use miden_protocol::account::{ AccountBuilder, @@ -29,12 +30,7 @@ use miden_standards::account::faucets::{ NetworkFungibleFaucet, TokenName, }; -use miden_standards::account::metadata::{ - DESCRIPTION_DATA_KEY, - EXTERNAL_LINK_DATA_KEY, - LOGO_URI_DATA_KEY, - TokenMetadata, -}; +use miden_standards::account::metadata::TokenMetadata; use miden_standards::code_builder::CodeBuilder; use miden_standards::errors::standards::{ ERR_DESCRIPTION_NOT_MUTABLE, @@ -141,6 +137,12 @@ fn field_advice_map_value(field: &[Word; 7]) -> Vec { value } +/// Compute the Rpo256 hash of the field data (used as the advice map key). +fn compute_field_hash(data: &[Word; 7]) -> Word { + let felts = field_advice_map_value(data); + Word::from(*Rpo256::hash_elements(&felts)) +} + /// Execute a tx script against the given account and assert success. async fn execute_tx_script( account: miden_protocol::account::Account, @@ -529,43 +531,6 @@ async fn is_field_mutable_checks() -> anyhow::Result<()> { Ok(()) } -// ================================================================================================= -// GETTER TESTS – owner -// ================================================================================================= - -#[tokio::test] -async fn get_owner() -> anyhow::Result<()> { - let owner = owner_account_id(); - - let metadata = build_pol_faucet_metadata().with_owner(owner); - - let account = AccountBuilder::new([4u8; 32]) - .account_type(AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_auth_component(NoAuth) - .with_component(metadata) - .with_component(NetworkFungibleFaucet) - .build()?; - - let expected_prefix = owner.prefix().as_felt().as_canonical_u64(); - let expected_suffix = owner.suffix().as_canonical_u64(); - - execute_tx_script( - account, - format!( - r#" - begin - call.::miden::standards::metadata::fungible::get_owner - push.{expected_suffix} assert_eq.err="owner suffix does not match" - push.{expected_prefix} assert_eq.err="owner prefix does not match" - push.0 assert_eq.err="clean stack: pad must be 0" - end - "# - ), - ) - .await -} - // ================================================================================================= // STORAGE LAYOUT TESTS // ================================================================================================= @@ -877,13 +842,11 @@ fn external_link_config(data: [Word; 7], mutable: bool) -> FieldSetterFaucetArgs async fn test_field_setter_immutable_fails( proc_name: &str, - advice_key: Word, immutable_error: MasmError, args: FieldSetterFaucetArgs, ) -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner = owner_account_id(); - let new_data = new_field_data(); let faucet = builder.add_existing_network_faucet_with_metadata_info( "FLD", @@ -912,7 +875,6 @@ async fn test_field_setter_immutable_fails( let tx_context = mock_chain .build_tx_context(faucet.id(), &[], &[])? .tx_script(tx_script) - .extend_advice_map([(advice_key, field_advice_map_value(&new_data))]) .with_source_manager(source_manager) .build()?; @@ -924,7 +886,6 @@ async fn test_field_setter_immutable_fails( async fn test_field_setter_owner_succeeds( proc_name: &str, - advice_key: Word, args: FieldSetterFaucetArgs, slot_fn: fn(usize) -> &'static miden_protocol::account::StorageSlotName, ) -> anyhow::Result<()> { @@ -944,12 +905,19 @@ async fn test_field_setter_owner_succeeds( )?; let mock_chain = builder.build()?; + let hash = compute_field_hash(&new_data); + let note_script_code = format!( r#" - begin - call.::miden::standards::metadata::fungible::{proc_name} - end - "# + begin + push.{h0}.{h1}.{h2}.{h3} + call.::miden::standards::metadata::fungible::{proc_name} + end +"#, + h0 = hash[0].as_canonical_u64(), + h1 = hash[1].as_canonical_u64(), + h2 = hash[2].as_canonical_u64(), + h3 = hash[3].as_canonical_u64(), ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -967,7 +935,7 @@ async fn test_field_setter_owner_succeeds( let tx_context = mock_chain .build_tx_context(faucet.id(), &[], &[note])? .add_note_script(note_script) - .extend_advice_map([(advice_key, field_advice_map_value(&new_data))]) + .extend_advice_map([(hash, field_advice_map_value(&new_data))]) .with_source_manager(source_manager) .build()?; @@ -985,7 +953,6 @@ async fn test_field_setter_owner_succeeds( async fn test_field_setter_non_owner_fails( proc_name: &str, - advice_key: Word, args: FieldSetterFaucetArgs, ) -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -1005,12 +972,19 @@ async fn test_field_setter_non_owner_fails( )?; let mock_chain = builder.build()?; + let hash = compute_field_hash(&new_data); + let note_script_code = format!( r#" - begin - call.::miden::standards::metadata::fungible::{proc_name} - end - "# + begin + push.{h0}.{h1}.{h2}.{h3} + call.::miden::standards::metadata::fungible::{proc_name} + end +"#, + h0 = hash[0].as_canonical_u64(), + h1 = hash[1].as_canonical_u64(), + h2 = hash[2].as_canonical_u64(), + h3 = hash[3].as_canonical_u64(), ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -1028,7 +1002,7 @@ async fn test_field_setter_non_owner_fails( let tx_context = mock_chain .build_tx_context(faucet.id(), &[], &[note])? .add_note_script(note_script) - .extend_advice_map([(advice_key, field_advice_map_value(&new_data))]) + .extend_advice_map([(hash, field_advice_map_value(&new_data))]) .with_source_manager(source_manager) .build()?; From 5402e9e3ce5f571138c3f59d908a96c66c6b6e6f Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 16 Mar 2026 07:50:44 -0300 Subject: [PATCH 52/56] refactor: simplify network fungible faucet creation by consolidating metadata parameters --- .../src/account/faucets/network_fungible.rs | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 60e8337422..de5001df81 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -6,14 +6,12 @@ use miden_protocol::account::{ AccountStorageMode, AccountType, }; -use miden_protocol::asset::TokenSymbol; -use miden_protocol::{Felt, Word}; +use miden_protocol::Word; use super::{FungibleFaucetError, FungibleTokenMetadata}; use crate::account::access::AccessControl; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; -use crate::account::faucets::{Description, ExternalLink, LogoURI, TokenName}; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::procedure_digest; @@ -134,7 +132,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { } /// Creates a new faucet account with network fungible faucet interface and provided metadata -/// (token symbol, decimals, max supply) and access control. +/// and access control. /// /// The network faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -153,27 +151,11 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// its auth ([`NoAuth`]). pub fn create_network_fungible_faucet( init_seed: [u8; 32], - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - name: TokenName, - description: Option, - logo_uri: Option, - external_link: Option, + metadata: FungibleTokenMetadata, access_control: AccessControl, ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); - let metadata = FungibleTokenMetadata::new( - symbol, - decimals, - max_supply, - name, - description, - logo_uri, - external_link, - )?; - let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) @@ -196,14 +178,11 @@ mod tests { use super::*; use crate::account::access::Ownable2Step; - use crate::account::faucets::TokenName; + use crate::account::faucets::{FungibleTokenMetadata, TokenName}; #[test] fn test_create_network_fungible_faucet() { let init_seed = [7u8; 32]; - let symbol = TokenSymbol::new("NET").expect("token symbol should be valid"); - let decimals = 8u8; - let max_supply = Felt::new(1_000); let owner = AccountId::dummy( [1u8; 15], @@ -212,16 +191,20 @@ mod tests { AccountStorageMode::Private, ); - let name = TokenName::new("NET").expect("valid name"); - let account = create_network_fungible_faucet( - init_seed, - symbol, - decimals, - max_supply, - name, + let metadata = FungibleTokenMetadata::new( + TokenSymbol::new("NET").expect("valid symbol"), + 8u8, + Felt::new(1_000), + TokenName::new("NET").expect("valid name"), None, None, None, + ) + .expect("valid metadata"); + + let account = create_network_fungible_faucet( + init_seed, + metadata, AccessControl::Ownable2Step { owner }, ) .expect("network faucet creation should succeed"); From dc96cb0ad06633508f22549c6d10709ffcd66c98 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 16 Mar 2026 08:22:01 -0300 Subject: [PATCH 53/56] refactor: clone token symbol in FungibleTokenMetadata creation to avoid ownership issues --- .../src/account/faucets/basic_fungible.rs | 2 +- .../src/account/faucets/network_fungible.rs | 2 ++ .../src/account/faucets/token_metadata.rs | 28 ++++++++++++------- .../src/standards/token_metadata.rs | 16 ++--------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index e681183ba7..6c77bf5908 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -240,7 +240,7 @@ mod tests { let token_name = TokenName::new(token_name_string).unwrap(); let description = Description::new(description_string).unwrap(); let metadata = FungibleTokenMetadata::new( - token_symbol, + token_symbol.clone(), decimals, max_supply, token_name, diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 9f122b67cc..47d7897a8f 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -174,7 +174,9 @@ pub fn create_network_fungible_faucet( #[cfg(test)] mod tests { + use miden_protocol::Felt; use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; + use miden_protocol::asset::TokenSymbol; use super::*; use crate::account::access::Ownable2Step; diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 3eb273dea5..835d01659a 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -562,7 +562,7 @@ mod tests { let name = TokenName::new("TEST").unwrap(); let metadata = FungibleTokenMetadata::new( - symbol, + symbol.clone(), decimals, max_supply, name.clone(), @@ -591,7 +591,7 @@ mod tests { let name = TokenName::new("TEST").unwrap(); let metadata = FungibleTokenMetadata::with_supply( - symbol, + symbol.clone(), decimals, max_supply, token_supply, @@ -617,7 +617,7 @@ mod tests { let description = Description::new("A polygon token").unwrap(); let metadata = FungibleTokenMetadata::new( - symbol, + symbol.clone(), decimals, max_supply, name.clone(), @@ -627,12 +627,12 @@ mod tests { ) .unwrap(); - assert_eq!(metadata.symbol(), symbol); + assert_eq!(metadata.symbol(), &symbol); assert_eq!(metadata.name(), &name); assert_eq!(metadata.description(), Some(&description)); let word: Word = metadata.into(); let restored = FungibleTokenMetadata::try_from(word).unwrap(); - assert_eq!(restored.symbol(), symbol); + assert_eq!(restored.symbol(), &symbol); assert!(restored.description().is_none()); } @@ -741,9 +741,16 @@ mod tests { let max_supply = Felt::new(123); let name = TokenName::new("POL").unwrap(); - let original = - FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None) - .unwrap(); + let original = FungibleTokenMetadata::new( + symbol.clone(), + decimals, + max_supply, + name, + None, + None, + None, + ) + .unwrap(); let slot: StorageSlot = original.into(); let restored = FungibleTokenMetadata::try_from(&slot).unwrap(); @@ -763,7 +770,7 @@ mod tests { let name = TokenName::new("POL").unwrap(); let original = FungibleTokenMetadata::with_supply( - symbol, + symbol.clone(), decimals, max_supply, token_supply, @@ -829,7 +836,8 @@ mod tests { let name = TokenName::new("polygon").unwrap(); let metadata = - FungibleTokenMetadata::new(symbol, 2, Felt::new(123), name, None, None, None).unwrap(); + FungibleTokenMetadata::new(symbol.clone(), 2, Felt::new(123), name, None, None, None) + .unwrap(); let slots = metadata.storage_slots(); // First slot is the metadata word [token_supply, max_supply, decimals, symbol] diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index dce7be111f..5c9af4fedf 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -1018,7 +1018,6 @@ async fn test_field_setter_non_owner_fails( async fn set_description_immutable_fails() -> anyhow::Result<()> { test_field_setter_immutable_fails( "set_description", - DESCRIPTION_DATA_KEY, ERR_DESCRIPTION_NOT_MUTABLE, description_config(initial_field_data(), false), ) @@ -1029,7 +1028,6 @@ async fn set_description_immutable_fails() -> anyhow::Result<()> { async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { test_field_setter_owner_succeeds( "set_description", - DESCRIPTION_DATA_KEY, description_config(initial_field_data(), true), TokenMetadata::description_slot, ) @@ -1040,7 +1038,6 @@ async fn set_description_mutable_owner_succeeds() -> anyhow::Result<()> { async fn set_description_mutable_non_owner_fails() -> anyhow::Result<()> { test_field_setter_non_owner_fails( "set_description", - DESCRIPTION_DATA_KEY, description_config(initial_field_data(), true), ) .await @@ -1052,7 +1049,6 @@ async fn set_description_mutable_non_owner_fails() -> anyhow::Result<()> { async fn set_logo_uri_immutable_fails() -> anyhow::Result<()> { test_field_setter_immutable_fails( "set_logo_uri", - LOGO_URI_DATA_KEY, ERR_LOGO_URI_NOT_MUTABLE, logo_uri_config(initial_field_data(), false), ) @@ -1063,7 +1059,6 @@ async fn set_logo_uri_immutable_fails() -> anyhow::Result<()> { async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { test_field_setter_owner_succeeds( "set_logo_uri", - LOGO_URI_DATA_KEY, logo_uri_config(initial_field_data(), true), TokenMetadata::logo_uri_slot, ) @@ -1072,12 +1067,8 @@ async fn set_logo_uri_mutable_owner_succeeds() -> anyhow::Result<()> { #[tokio::test] async fn set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { - test_field_setter_non_owner_fails( - "set_logo_uri", - LOGO_URI_DATA_KEY, - logo_uri_config(initial_field_data(), true), - ) - .await + test_field_setter_non_owner_fails("set_logo_uri", logo_uri_config(initial_field_data(), true)) + .await } // --- set_external_link --- @@ -1086,7 +1077,6 @@ async fn set_logo_uri_mutable_non_owner_fails() -> anyhow::Result<()> { async fn set_external_link_immutable_fails() -> anyhow::Result<()> { test_field_setter_immutable_fails( "set_external_link", - EXTERNAL_LINK_DATA_KEY, ERR_EXTERNAL_LINK_NOT_MUTABLE, external_link_config(initial_field_data(), false), ) @@ -1097,7 +1087,6 @@ async fn set_external_link_immutable_fails() -> anyhow::Result<()> { async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { test_field_setter_owner_succeeds( "set_external_link", - EXTERNAL_LINK_DATA_KEY, external_link_config(initial_field_data(), true), TokenMetadata::external_link_slot, ) @@ -1108,7 +1097,6 @@ async fn set_external_link_mutable_owner_succeeds() -> anyhow::Result<()> { async fn set_external_link_mutable_non_owner_fails() -> anyhow::Result<()> { test_field_setter_non_owner_fails( "set_external_link", - EXTERNAL_LINK_DATA_KEY, external_link_config(initial_field_data(), true), ) .await From 62aeac90362e40f517ea315ced3e08a1675b41d3 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 16 Mar 2026 11:29:00 -0300 Subject: [PATCH 54/56] refactor: update comments for clarity and fixed hash handling in token metadata tests --- .../src/account/components/mod.rs | 4 +-- .../src/account/encoding/mod.rs | 4 +-- .../src/standards/token_metadata.rs | 28 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 2805ba1d84..0fadf1a3a9 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -109,8 +109,8 @@ static FUNGIBLE_TOKEN_METADATA_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Fungible Token Metadata library is well-formed") }); -// Metadata Info component uses the standards library (get_name, set_description, etc. from -// metadata). +// [`TokenMetadata`](crate::account::metadata::TokenMetadata) component library — exposes +// procedures from `miden::standards::metadata::fungible` (get_name, set_description, etc.). static METADATA_INFO_COMPONENT_LIBRARY: LazyLock = LazyLock::new(|| Library::from(crate::StandardsLib::default())); diff --git a/crates/miden-standards/src/account/encoding/mod.rs b/crates/miden-standards/src/account/encoding/mod.rs index 250d953f91..3b21701370 100644 --- a/crates/miden-standards/src/account/encoding/mod.rs +++ b/crates/miden-standards/src/account/encoding/mod.rs @@ -254,8 +254,8 @@ mod tests { fn non_utf8_bytes_return_invalid_utf8() { // Encode raw bytes that are not valid UTF-8 (e.g. 0xFF byte in content). // Length byte = 1, content byte = 0xFF (invalid UTF-8 start byte). - // Pack into first felt: LE bytes [1, 0xFF, 0, 0, 0, 0, 0] → u64 = 0x0000_0000_00FF_01 - let raw: u64 = 0x0000_0000_00_ff_01; + // Pack into first felt: LE bytes [1, 0xFF, 0, 0, 0, 0, 0] → u64 = 0x0000_0000_0000_ff01 + let raw: u64 = 0x0000_0000_0000_ff01; let bad_felt = Felt::try_from(raw).unwrap(); let words = [ Word::from([bad_felt, Felt::ZERO, Felt::ZERO, Felt::ZERO]), diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index 5c9af4fedf..e4ab2cb504 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -906,18 +906,23 @@ async fn test_field_setter_owner_succeeds( let mock_chain = builder.build()?; let hash = compute_field_hash(&new_data); + let hash_elems = hash.as_elements(); + // Push hash as 4 Felts so advice map key matches; dropw after call so stack depth is 16 + // (setter leaves 20). Use `debug.stack` in the script and run with --nocapture to trace. let note_script_code = format!( r#" begin - push.{h0}.{h1}.{h2}.{h3} + dropw push.{e3} push.{e2} push.{e1} push.{e0} call.::miden::standards::metadata::fungible::{proc_name} + dropw end "#, - h0 = hash[0].as_canonical_u64(), - h1 = hash[1].as_canonical_u64(), - h2 = hash[2].as_canonical_u64(), - h3 = hash[3].as_canonical_u64(), + e0 = hash_elems[0], + e1 = hash_elems[1], + e2 = hash_elems[2], + e3 = hash_elems[3], + proc_name = proc_name, ); let source_manager = Arc::new(DefaultSourceManager::default()); @@ -973,18 +978,21 @@ async fn test_field_setter_non_owner_fails( let mock_chain = builder.build()?; let hash = compute_field_hash(&new_data); + let hash_elems = hash.as_elements(); let note_script_code = format!( r#" begin - push.{h0}.{h1}.{h2}.{h3} + dropw push.{e3} push.{e2} push.{e1} push.{e0} call.::miden::standards::metadata::fungible::{proc_name} + dropw end "#, - h0 = hash[0].as_canonical_u64(), - h1 = hash[1].as_canonical_u64(), - h2 = hash[2].as_canonical_u64(), - h3 = hash[3].as_canonical_u64(), + e0 = hash_elems[0], + e1 = hash_elems[1], + e2 = hash_elems[2], + e3 = hash_elems[3], + proc_name = proc_name, ); let source_manager = Arc::new(DefaultSourceManager::default()); From bdc17fad2f9a2a45118403158e25fe84e232f12f Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 16 Mar 2026 11:41:01 -0300 Subject: [PATCH 55/56] refactor: streamline fungible token metadata handling in tests and simplify faucet creation --- .../src/account/interface/test.rs | 10 +- .../src/mock_chain/chain_builder.rs | 48 +--------- .../src/standards/token_metadata.rs | 95 ++++++++++++------- 3 files changed, 66 insertions(+), 87 deletions(-) diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 374908aad9..9df6bc7e79 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -23,7 +23,7 @@ use miden_protocol::{Felt, Word}; use crate::AuthMethod; use crate::account::auth::{AuthMultisig, AuthMultisigConfig, AuthSingleSig, NoAuth}; -use crate::account::faucets::BasicFungibleFaucet; +use crate::account::faucets::{BasicFungibleFaucet, FungibleTokenMetadata, TokenName}; use crate::account::interface::{ AccountComponentInterface, AccountInterface, @@ -55,11 +55,11 @@ fn test_basic_wallet_default_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - crate::account::faucets::FungibleTokenMetadata::new( + FungibleTokenMetadata::new( TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), - crate::account::faucets::TokenName::new("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, @@ -326,11 +326,11 @@ fn test_basic_fungible_faucet_custom_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - crate::account::faucets::FungibleTokenMetadata::new( + FungibleTokenMetadata::new( TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), - crate::account::faucets::TokenName::new("POL").unwrap(), + TokenName::new("POL").unwrap(), None, None, None, diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 93c70424c4..817ed06ac2 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -49,10 +49,7 @@ use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word}; use miden_standards::account::access::Ownable2Step; use miden_standards::account::faucets::{ BasicFungibleFaucet, - Description, - ExternalLink, FungibleTokenMetadata, - LogoURI, NetworkFungibleFaucet, TokenName, }; @@ -430,55 +427,14 @@ impl MockChainBuilder { self.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) } - /// Adds an existing network fungible faucet account with a metadata Info component + /// Adds an existing network fungible faucet account with the given metadata component /// (for testing metadata::fungible procedures: owner can update description / logo_uri / /// external_link / max supply when mutable). - #[allow(clippy::too_many_arguments)] pub fn add_existing_network_faucet_with_metadata_info( &mut self, - token_symbol: &str, - max_supply: u64, owner_account_id: AccountId, - token_supply: Option, - max_supply_mutable: bool, - description: Option<([Word; 7], bool)>, - logo_uri: Option<([Word; 7], bool)>, - external_link: Option<([Word; 7], bool)>, + metadata: FungibleTokenMetadata, ) -> anyhow::Result { - let max_supply = Felt::try_from(max_supply)?; - let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; - let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); - let token_symbol = - TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - - let mut metadata = FungibleTokenMetadata::new( - token_symbol, - DEFAULT_FAUCET_DECIMALS, - max_supply, - name, - description.map(|(words, _)| { - Description::try_from_words(&words).expect("valid description words") - }), - logo_uri - .map(|(words, _)| LogoURI::try_from_words(&words).expect("valid logo_uri words")), - external_link.map(|(words, _)| { - ExternalLink::try_from_words(&words).expect("valid external_link words") - }), - ) - .and_then(|m| m.with_token_supply(token_supply)) - .context("failed to create fungible token metadata")? - .with_max_supply_mutable(max_supply_mutable); - - if let Some((_, mutable)) = description { - metadata = metadata.with_description_mutable(mutable); - } - if let Some((_, mutable)) = logo_uri { - metadata = metadata.with_logo_uri_mutable(mutable); - } - if let Some((_, mutable)) = external_link { - metadata = metadata.with_external_link_mutable(mutable); - } - let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) .with_component(metadata) diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index e4ab2cb504..95efeee3c1 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -46,6 +46,53 @@ use crate::{MockChain, TransactionContextBuilder, assert_transaction_executor_er // SHARED HELPERS // ================================================================================================ +/// Builds [`FungibleTokenMetadata`] for tests that use raw word arrays + mutability flags +/// (e.g. from [`description_config`] / [`logo_uri_config`] / [`external_link_config`]). +fn network_faucet_metadata( + token_symbol: &str, + max_supply: u64, + token_supply: Option, + max_supply_mutable: bool, + description: Option<([Word; 7], bool)>, + logo_uri: Option<([Word; 7], bool)>, + external_link: Option<([Word; 7], bool)>, +) -> anyhow::Result { + let max_supply = Felt::try_from(max_supply)?; + let token_supply = Felt::try_from(token_supply.unwrap_or(0))?; + let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); + let token_symbol = TokenSymbol::new(token_symbol)?; + + let mut metadata = FungibleTokenMetadata::new( + token_symbol, + 10, + max_supply, + name, + description.map(|(words, _)| { + Description::try_from_words(&words).expect("valid description words") + }), + logo_uri.map(|(words, _)| { + LogoURI::try_from_words(&words).expect("valid logo_uri words") + }), + external_link.map(|(words, _)| { + ExternalLink::try_from_words(&words).expect("valid external_link words") + }), + ) + .and_then(|m| m.with_token_supply(token_supply))? + .with_max_supply_mutable(max_supply_mutable); + + if let Some((_, mutable)) = description { + metadata = metadata.with_description_mutable(mutable); + } + if let Some((_, mutable)) = logo_uri { + metadata = metadata.with_logo_uri_mutable(mutable); + } + if let Some((_, mutable)) = external_link { + metadata = metadata.with_external_link_mutable(mutable); + } + + Ok(metadata) +} + fn initial_field_data() -> [Word; 7] { [ Word::from([1u32, 2, 3, 4]), @@ -848,16 +895,16 @@ async fn test_field_setter_immutable_fails( let mut builder = MockChain::builder(); let owner = owner_account_id(); - let faucet = builder.add_existing_network_faucet_with_metadata_info( + let metadata = network_faucet_metadata( "FLD", 1000, - owner, Some(0), false, args.description, args.logo_uri, args.external_link, )?; + let faucet = builder.add_existing_network_faucet_with_metadata_info(owner, metadata)?; let mock_chain = builder.build()?; let tx_script_code = format!( @@ -893,16 +940,16 @@ async fn test_field_setter_owner_succeeds( let owner = owner_account_id(); let new_data = new_field_data(); - let faucet = builder.add_existing_network_faucet_with_metadata_info( + let metadata = network_faucet_metadata( "FLD", 1000, - owner, Some(0), false, args.description, args.logo_uri, args.external_link, )?; + let faucet = builder.add_existing_network_faucet_with_metadata_info(owner, metadata)?; let mock_chain = builder.build()?; let hash = compute_field_hash(&new_data); @@ -965,16 +1012,16 @@ async fn test_field_setter_non_owner_fails( let non_owner = non_owner_account_id(); let new_data = new_field_data(); - let faucet = builder.add_existing_network_faucet_with_metadata_info( + let metadata = network_faucet_metadata( "FLD", 1000, - owner, Some(0), false, args.description, args.logo_uri, args.external_link, )?; + let faucet = builder.add_existing_network_faucet_with_metadata_info(owner, metadata)?; let mock_chain = builder.build()?; let hash = compute_field_hash(&new_data); @@ -1119,16 +1166,8 @@ async fn set_max_supply_immutable_fails() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let owner = owner_account_id(); - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "MSM", - 1000, - owner, - Some(0), - false, - None, - None, - None, - )?; + let metadata = network_faucet_metadata("MSM", 1000, Some(0), false, None, None, None)?; + let faucet = builder.add_existing_network_faucet_with_metadata_info(owner, metadata)?; let mock_chain = builder.build()?; let tx_script_code = r#" @@ -1160,16 +1199,8 @@ async fn set_max_supply_mutable_owner_succeeds() -> anyhow::Result<()> { let owner = owner_account_id(); let new_max_supply: u64 = 2000; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "MSM", - 1000, - owner, - Some(0), - true, - None, - None, - None, - )?; + let metadata = network_faucet_metadata("MSM", 1000, Some(0), true, None, None, None)?; + let faucet = builder.add_existing_network_faucet_with_metadata_info(owner, metadata)?; let mock_chain = builder.build()?; let note_script_code = format!( @@ -1218,16 +1249,8 @@ async fn set_max_supply_mutable_non_owner_fails() -> anyhow::Result<()> { let non_owner = non_owner_account_id(); let new_max_supply: u64 = 2000; - let faucet = builder.add_existing_network_faucet_with_metadata_info( - "MSM", - 1000, - owner, - Some(0), - true, - None, - None, - None, - )?; + let metadata = network_faucet_metadata("MSM", 1000, Some(0), true, None, None, None)?; + let faucet = builder.add_existing_network_faucet_with_metadata_info(owner, metadata)?; let mock_chain = builder.build()?; let note_script_code = format!( From e9430200acf0a3ae1b85cd1134ac04946b44a1b9 Mon Sep 17 00:00:00 2001 From: Arthur Abeilice Date: Mon, 16 Mar 2026 12:03:45 -0300 Subject: [PATCH 56/56] Refactor FungibleTokenMetadata creation to use builder pattern - Introduced FungibleTokenMetadataBuilder to simplify the creation of FungibleTokenMetadata instances. - Updated all instances of FungibleTokenMetadata creation across the codebase to utilize the new builder pattern. - Enhanced readability and maintainability by allowing optional fields to be set in a chainable manner. - Adjusted tests and mock implementations to accommodate the new builder approach. --- crates/miden-agglayer/src/lib.rs | 20 +- .../src/account/faucets/basic_fungible.rs | 78 +++- .../src/account/faucets/mod.rs | 9 +- .../src/account/faucets/network_fungible.rs | 10 +- .../src/account/faucets/token_metadata.rs | 343 ++++++++++++------ .../src/account/interface/test.rs | 18 +- .../src/account/metadata/mod.rs | 17 +- .../src/mock_chain/chain_builder.rs | 32 +- .../src/standards/token_metadata.rs | 208 +++++------ 9 files changed, 428 insertions(+), 307 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 720285efb7..8cf1a8fd3e 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -24,7 +24,12 @@ use miden_protocol::asset::TokenSymbol; use miden_protocol::block::account_tree::AccountIdKey; use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::{FungibleFaucetError, FungibleTokenMetadata, TokenName}; +use miden_standards::account::faucets::{ + FungibleFaucetError, + FungibleTokenMetadata, + FungibleTokenMetadataBuilder, + TokenName, +}; use miden_utils_sync::LazyLock; pub mod b2agg_note; @@ -347,16 +352,9 @@ impl AggLayerFaucet { ) -> Result { // Use empty name for agglayer faucets (name is stored in Info component, not here). let name = TokenName::default(); - let metadata = FungibleTokenMetadata::with_supply( - symbol, - decimals, - max_supply, - token_supply, - name, - None, - None, - None, - )?; + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, decimals, max_supply) + .token_supply(token_supply) + .build()?; Ok(Self { metadata, bridge_account_id, diff --git a/crates/miden-standards/src/account/faucets/basic_fungible.rs b/crates/miden-standards/src/account/faucets/basic_fungible.rs index 6c77bf5908..fa18f6aa28 100644 --- a/crates/miden-standards/src/account/faucets/basic_fungible.rs +++ b/crates/miden-standards/src/account/faucets/basic_fungible.rs @@ -1,4 +1,6 @@ -use miden_protocol::Word; +use alloc::string::ToString; + +use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ Account, @@ -7,8 +9,9 @@ use miden_protocol::account::{ AccountStorageMode, AccountType, }; +use miden_protocol::{Felt, Word}; -use super::{FungibleFaucetError, FungibleTokenMetadata}; +use super::{FungibleFaucetError, FungibleTokenMetadata, FungibleTokenMetadataBuilder, TokenName}; use crate::account::AuthMethod; use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig}; use crate::account::components::basic_fungible_faucet_library; @@ -85,6 +88,35 @@ impl BasicFungibleFaucet { .with_description("Basic fungible faucet component for minting and burning tokens") } + /// Creates a minimal basic fungible faucet [`Account`]. + /// + /// This is a convenience constructor used by downstream crates/tests which need a quick way + /// to instantiate a faucet account. + /// + /// The created account uses: + /// - a deterministic seed derived from `(symbol, decimals, max_supply)` + /// - [`AccountStorageMode::Private`] + /// - a default single-signature authentication method (Falcon512Poseidon2) + #[allow(clippy::new_ret_no_self)] + pub fn new( + symbol: miden_protocol::asset::TokenSymbol, + decimals: u8, + max_supply: Felt, + ) -> Result { + let metadata = + FungibleTokenMetadataBuilder::new(TokenName::default(), symbol, decimals, max_supply) + .build()?; + + let init_seed = derive_faucet_seed(metadata.symbol(), decimals, max_supply); + + let pub_key = PublicKeyCommitment::from(Word::from([1u32, 2, 3, 4])); + let auth_method = AuthMethod::SingleSig { + approver: (pub_key, AuthScheme::Falcon512Poseidon2), + }; + + create_basic_fungible_faucet(init_seed, metadata, AccountStorageMode::Private, auth_method) + } + /// Checks that the account contains the basic fungible faucet interface. fn try_from_interface( interface: AccountInterface, @@ -98,6 +130,29 @@ impl BasicFungibleFaucet { } } +fn derive_faucet_seed( + symbol: &miden_protocol::asset::TokenSymbol, + decimals: u8, + max_supply: Felt, +) -> [u8; 32] { + let mut seed = [0u8; 32]; + + let symbol_str = symbol.to_string(); + let symbol_bytes = symbol_str.as_bytes(); + let symbol_len = core::cmp::min(symbol_bytes.len(), 12); + seed[..symbol_len].copy_from_slice(&symbol_bytes[..symbol_len]); + + seed[12] = decimals; + seed[13..21].copy_from_slice(&max_supply.as_canonical_u64().to_le_bytes()); + + for (offset, byte) in seed[21..].iter_mut().enumerate() { + let i = 21 + offset; + *byte = (i as u8).wrapping_mul(31) ^ 0xa5; + } + + seed +} + impl From for AccountComponent { fn from(_faucet: BasicFungibleFaucet) -> Self { let metadata = BasicFungibleFaucet::component_metadata(); @@ -211,7 +266,7 @@ mod tests { create_basic_fungible_faucet, }; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; - use crate::account::faucets::{Description, TokenName}; + use crate::account::faucets::{Description, FungibleTokenMetadataBuilder, TokenName}; use crate::account::metadata::TokenMetadata; use crate::account::wallets::BasicWallet; @@ -239,15 +294,14 @@ mod tests { let token_name = TokenName::new(token_name_string).unwrap(); let description = Description::new(description_string).unwrap(); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + token_name, token_symbol.clone(), decimals, max_supply, - token_name, - Some(description), - None, - None, ) + .description(description) + .build() .unwrap(); let faucet_account = create_basic_fungible_faucet(init_seed, metadata, storage_mode, auth_method).unwrap(); @@ -321,15 +375,13 @@ mod tests { // valid account let token_symbol = miden_protocol::asset::TokenSymbol::new("POL").expect("invalid token symbol"); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + TokenName::new("POL").unwrap(), token_symbol, 10, Felt::new(100), - TokenName::new("POL").unwrap(), - None, - None, - None, ) + .build() .expect("failed to create token metadata"); let faucet_account = AccountBuilder::new(mock_seed) diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index abea6bc485..8e6c0f6423 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -12,7 +12,14 @@ mod token_metadata; pub use basic_fungible::{BasicFungibleFaucet, create_basic_fungible_faucet}; pub use network_fungible::{NetworkFungibleFaucet, create_network_fungible_faucet}; -pub use token_metadata::{Description, ExternalLink, FungibleTokenMetadata, LogoURI, TokenName}; +pub use token_metadata::{ + Description, + ExternalLink, + FungibleTokenMetadata, + FungibleTokenMetadataBuilder, + LogoURI, + TokenName, +}; pub use crate::account::encoding::{FixedWidthString, FixedWidthStringError}; diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 47d7897a8f..8c966400f5 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -180,7 +180,7 @@ mod tests { use super::*; use crate::account::access::Ownable2Step; - use crate::account::faucets::{FungibleTokenMetadata, TokenName}; + use crate::account::faucets::{FungibleTokenMetadataBuilder, TokenName}; #[test] fn test_create_network_fungible_faucet() { @@ -193,15 +193,13 @@ mod tests { AccountStorageMode::Private, ); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + TokenName::new("NET").expect("valid name"), TokenSymbol::new("NET").expect("valid symbol"), 8u8, Felt::new(1_000), - TokenName::new("NET").expect("valid name"), - None, - None, - None, ) + .build() .expect("valid metadata"); let account = create_network_fungible_faucet( diff --git a/crates/miden-standards/src/account/faucets/token_metadata.rs b/crates/miden-standards/src/account/faucets/token_metadata.rs index 835d01659a..ed25822423 100644 --- a/crates/miden-standards/src/account/faucets/token_metadata.rs +++ b/crates/miden-standards/src/account/faucets/token_metadata.rs @@ -196,6 +196,132 @@ impl ExternalLink { /// The schema type for token symbols. const TOKEN_SYMBOL_TYPE: &str = "miden::standards::fungible_faucets::metadata::token_symbol"; +/// Builder for [`FungibleTokenMetadata`] to avoid unwieldy optional arguments. +/// +/// Required fields are set in [`Self::new`]; optional fields and token supply +/// can be set via chainable methods. Token supply defaults to zero. +/// +/// # Example +/// +/// ``` +/// # use miden_protocol::asset::TokenSymbol; +/// # use miden_protocol::Felt; +/// # use miden_standards::account::faucets::{ +/// # Description, FungibleTokenMetadataBuilder, LogoURI, TokenName, +/// # }; +/// let name = TokenName::new("My Token").unwrap(); +/// let symbol = TokenSymbol::new("MTK").unwrap(); +/// let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 8, Felt::new(1_000_000)) +/// .token_supply(Felt::new(100)) +/// .description(Description::new("A test token").unwrap()) +/// .logo_uri(LogoURI::new("https://example.com/logo.png").unwrap()) +/// .build() +/// .unwrap(); +/// ``` +#[derive(Debug, Clone)] +pub struct FungibleTokenMetadataBuilder { + name: TokenName, + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + token_supply: Felt, + description: Option, + logo_uri: Option, + external_link: Option, + description_mutable: bool, + logo_uri_mutable: bool, + external_link_mutable: bool, + max_supply_mutable: bool, +} + +impl FungibleTokenMetadataBuilder { + /// Creates a new builder with required fields. Token supply defaults to zero. + pub fn new(name: TokenName, symbol: TokenSymbol, decimals: u8, max_supply: Felt) -> Self { + Self { + name, + symbol, + decimals, + max_supply, + token_supply: Felt::ZERO, + description: None, + logo_uri: None, + external_link: None, + description_mutable: false, + logo_uri_mutable: false, + external_link_mutable: false, + max_supply_mutable: false, + } + } + + /// Sets the initial token supply (default is zero). + pub fn token_supply(mut self, token_supply: Felt) -> Self { + self.token_supply = token_supply; + self + } + + /// Sets the optional description. + pub fn description(mut self, description: Description) -> Self { + self.description = Some(description); + self + } + + /// Sets the optional logo URI. + pub fn logo_uri(mut self, logo_uri: LogoURI) -> Self { + self.logo_uri = Some(logo_uri); + self + } + + /// Sets the optional external link. + pub fn external_link(mut self, external_link: ExternalLink) -> Self { + self.external_link = Some(external_link); + self + } + + /// Sets whether the description can be updated by the owner. + pub fn description_mutable(mut self, mutable: bool) -> Self { + self.description_mutable = mutable; + self + } + + /// Sets whether the logo URI can be updated by the owner. + pub fn logo_uri_mutable(mut self, mutable: bool) -> Self { + self.logo_uri_mutable = mutable; + self + } + + /// Sets whether the external link can be updated by the owner. + pub fn external_link_mutable(mut self, mutable: bool) -> Self { + self.external_link_mutable = mutable; + self + } + + /// Sets whether the max supply can be updated by the owner. + pub fn max_supply_mutable(mut self, mutable: bool) -> Self { + self.max_supply_mutable = mutable; + self + } + + /// Builds [`FungibleTokenMetadata`]. + pub fn build(self) -> Result { + let mut meta = FungibleTokenMetadata::with_supply( + self.symbol, + self.decimals, + self.max_supply, + self.token_supply, + self.name, + self.description, + self.logo_uri, + self.external_link, + )?; + meta = meta + .with_description_mutable(self.description_mutable) + .with_logo_uri_mutable(self.logo_uri_mutable) + .with_external_link_mutable(self.external_link_mutable) + .with_max_supply_mutable(self.max_supply_mutable); + Ok(meta) + } +} + #[derive(Debug, Clone)] pub struct FungibleTokenMetadata { token_supply: Felt, @@ -461,16 +587,9 @@ impl TryFrom for FungibleTokenMetadata { } })?; - Self::with_supply( - symbol, - decimals, - max_supply, - token_supply, - TokenName::default(), - None, - None, - None, - ) + FungibleTokenMetadataBuilder::new(TokenName::default(), symbol, decimals, max_supply) + .token_supply(token_supply) + .build() } } @@ -561,16 +680,10 @@ mod tests { let max_supply = Felt::new(1_000_000); let name = TokenName::new("TEST").unwrap(); - let metadata = FungibleTokenMetadata::new( - symbol.clone(), - decimals, - max_supply, - name.clone(), - None, - None, - None, - ) - .unwrap(); + let metadata = + FungibleTokenMetadataBuilder::new(name.clone(), symbol.clone(), decimals, max_supply) + .build() + .unwrap(); assert_eq!(metadata.symbol(), &symbol); assert_eq!(metadata.decimals(), decimals); @@ -590,17 +703,11 @@ mod tests { let token_supply = Felt::new(500_000); let name = TokenName::new("TEST").unwrap(); - let metadata = FungibleTokenMetadata::with_supply( - symbol.clone(), - decimals, - max_supply, - token_supply, - name, - None, - None, - None, - ) - .unwrap(); + let metadata = + FungibleTokenMetadataBuilder::new(name, symbol.clone(), decimals, max_supply) + .token_supply(token_supply) + .build() + .unwrap(); assert_eq!(metadata.symbol(), &symbol); assert_eq!(metadata.decimals(), decimals); @@ -608,6 +715,39 @@ mod tests { assert_eq!(metadata.token_supply(), token_supply); } + #[test] + fn token_metadata_builder_with_optionals() { + let symbol = TokenSymbol::new("MTK").unwrap(); + let name = TokenName::new("My Token").unwrap(); + let description = Description::new("A test token").unwrap(); + let logo_uri = LogoURI::new("https://example.com/logo.png").unwrap(); + let external_link = ExternalLink::new("https://example.com").unwrap(); + + let metadata = FungibleTokenMetadataBuilder::new( + name.clone(), + symbol.clone(), + 8, + Felt::new(1_000_000), + ) + .token_supply(Felt::new(100)) + .description(description.clone()) + .logo_uri(logo_uri.clone()) + .external_link(external_link.clone()) + .description_mutable(true) + .max_supply_mutable(true) + .build() + .unwrap(); + + assert_eq!(metadata.token_supply(), Felt::new(100)); + assert_eq!(metadata.description(), Some(&description)); + assert_eq!(metadata.logo_uri(), Some(&logo_uri)); + assert_eq!(metadata.external_link(), Some(&external_link)); + let slots = metadata.storage_slots(); + let config_word = slots[3].value(); + assert_eq!(config_word[0], Felt::from(1u32), "desc_mutable"); + assert_eq!(config_word[3], Felt::from(1u32), "max_supply_mutable"); + } + #[test] fn token_metadata_with_name_and_description() { let symbol = TokenSymbol::new("POL").unwrap(); @@ -616,16 +756,11 @@ mod tests { let name = TokenName::new("polygon").unwrap(); let description = Description::new("A polygon token").unwrap(); - let metadata = FungibleTokenMetadata::new( - symbol.clone(), - decimals, - max_supply, - name.clone(), - Some(description.clone()), - None, - None, - ) - .unwrap(); + let metadata = + FungibleTokenMetadataBuilder::new(name.clone(), symbol.clone(), decimals, max_supply) + .description(description.clone()) + .build() + .unwrap(); assert_eq!(metadata.symbol(), &symbol); assert_eq!(metadata.name(), &name); @@ -696,8 +831,7 @@ mod tests { let max_supply = Felt::new(1_000_000); let name = TokenName::new("TEST").unwrap(); - let result = - FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); + let result = FungibleTokenMetadataBuilder::new(name, symbol, decimals, max_supply).build(); assert!(matches!(result, Err(FungibleFaucetError::TooManyDecimals { .. }))); } @@ -710,8 +844,7 @@ mod tests { let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT + 1); let name = TokenName::new("TEST").unwrap(); - let result = - FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None); + let result = FungibleTokenMetadataBuilder::new(name, symbol, decimals, max_supply).build(); assert!(matches!(result, Err(FungibleFaucetError::MaxSupplyTooLarge { .. }))); } @@ -723,9 +856,9 @@ mod tests { let max_supply = Felt::new(123); let name = TokenName::new("POL").unwrap(); - let metadata = - FungibleTokenMetadata::new(symbol, decimals, max_supply, name, None, None, None) - .unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, decimals, max_supply) + .build() + .unwrap(); let word: Word = metadata.into(); assert_eq!(word[0], Felt::ZERO); @@ -741,16 +874,10 @@ mod tests { let max_supply = Felt::new(123); let name = TokenName::new("POL").unwrap(); - let original = FungibleTokenMetadata::new( - symbol.clone(), - decimals, - max_supply, - name, - None, - None, - None, - ) - .unwrap(); + let original = + FungibleTokenMetadataBuilder::new(name, symbol.clone(), decimals, max_supply) + .build() + .unwrap(); let slot: StorageSlot = original.into(); let restored = FungibleTokenMetadata::try_from(&slot).unwrap(); @@ -769,17 +896,11 @@ mod tests { let token_supply = Felt::new(500); let name = TokenName::new("POL").unwrap(); - let original = FungibleTokenMetadata::with_supply( - symbol.clone(), - decimals, - max_supply, - token_supply, - name, - None, - None, - None, - ) - .unwrap(); + let original = + FungibleTokenMetadataBuilder::new(name, symbol.clone(), decimals, max_supply) + .token_supply(token_supply) + .build() + .unwrap(); let word: Word = original.into(); let restored = FungibleTokenMetadata::try_from(word).unwrap(); @@ -794,13 +915,13 @@ mod tests { let symbol = TokenSymbol::new("TST").unwrap(); let name = TokenName::new("T").unwrap(); - let metadata = - FungibleTokenMetadata::new(symbol, 2, Felt::new(1_000), name, None, None, None) - .unwrap() - .with_description_mutable(true) - .with_logo_uri_mutable(true) - .with_external_link_mutable(false) - .with_max_supply_mutable(true); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 2, Felt::new(1_000)) + .description_mutable(true) + .logo_uri_mutable(true) + .external_link_mutable(false) + .max_supply_mutable(true) + .build() + .unwrap(); let slots = metadata.storage_slots(); @@ -818,9 +939,9 @@ mod tests { let symbol = TokenSymbol::new("TST").unwrap(); let name = TokenName::new("T").unwrap(); - let metadata = - FungibleTokenMetadata::new(symbol, 2, Felt::new(1_000), name, None, None, None) - .unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 2, Felt::new(1_000)) + .build() + .unwrap(); let slots = metadata.storage_slots(); let config_word = slots[3].value(); @@ -835,9 +956,9 @@ mod tests { let symbol = TokenSymbol::new("POL").unwrap(); let name = TokenName::new("polygon").unwrap(); - let metadata = - FungibleTokenMetadata::new(symbol.clone(), 2, Felt::new(123), name, None, None, None) - .unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol.clone(), 2, Felt::new(123)) + .build() + .unwrap(); let slots = metadata.storage_slots(); // First slot is the metadata word [token_supply, max_supply, decimals, symbol] @@ -854,8 +975,9 @@ mod tests { let name = TokenName::new("my token").unwrap(); let expected_words = name.to_words(); - let metadata = - FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 2, Felt::new(100)) + .build() + .unwrap(); let slots = metadata.storage_slots(); // Slot layout: [0]=metadata, [1]=name_0, [2]=name_1 @@ -870,16 +992,10 @@ mod tests { let description = Description::new("A cool token").unwrap(); let expected_words = description.to_words(); - let metadata = FungibleTokenMetadata::new( - symbol, - 2, - Felt::new(100), - name, - Some(description), - None, - None, - ) - .unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 2, Felt::new(100)) + .description(description) + .build() + .unwrap(); let slots = metadata.storage_slots(); // Slots 4..11 are description (7 words): after metadata(1) + name(2) + config(1) @@ -893,8 +1009,9 @@ mod tests { let symbol = TokenSymbol::new("TST").unwrap(); let name = TokenName::new("T").unwrap(); - let metadata = - FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 2, Felt::new(100)) + .build() + .unwrap(); let slots = metadata.storage_slots(); // 1 metadata + 2 name + 1 config + 7 description + 7 logo + 7 external_link = 25 @@ -912,17 +1029,11 @@ mod tests { let name = TokenName::new("test token").unwrap(); let description = Description::new("A test").unwrap(); - let metadata = FungibleTokenMetadata::new( - symbol, - 4, - Felt::new(10_000), - name, - Some(description), - None, - None, - ) - .unwrap() - .with_max_supply_mutable(true); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 4, Felt::new(10_000)) + .description(description) + .max_supply_mutable(true) + .build() + .unwrap(); // Should build an account successfully with FungibleTokenMetadata as a component let account = AccountBuilder::new([1u8; 32]) @@ -962,16 +1073,9 @@ mod tests { let max_supply = Felt::new(100); let token_supply = Felt::new(101); - let result = FungibleTokenMetadata::with_supply( - symbol, - 2, - max_supply, - token_supply, - name, - None, - None, - None, - ); + let result = FungibleTokenMetadataBuilder::new(name, symbol, 2, max_supply) + .token_supply(token_supply) + .build(); assert!(matches!(result, Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply { .. }))); } @@ -979,8 +1083,9 @@ mod tests { fn with_token_supply_exceeds_max_supply() { let symbol = TokenSymbol::new("TST").unwrap(); let name = TokenName::new("T").unwrap(); - let metadata = - FungibleTokenMetadata::new(symbol, 2, Felt::new(100), name, None, None, None).unwrap(); + let metadata = FungibleTokenMetadataBuilder::new(name, symbol, 2, Felt::new(100)) + .build() + .unwrap(); let result = metadata.with_token_supply(Felt::new(101)); assert!(matches!(result, Err(FungibleFaucetError::TokenSupplyExceedsMaxSupply { .. }))); diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 9df6bc7e79..f33a3d8f87 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -23,7 +23,7 @@ use miden_protocol::{Felt, Word}; use crate::AuthMethod; use crate::account::auth::{AuthMultisig, AuthMultisigConfig, AuthSingleSig, NoAuth}; -use crate::account::faucets::{BasicFungibleFaucet, FungibleTokenMetadata, TokenName}; +use crate::account::faucets::{BasicFungibleFaucet, FungibleTokenMetadataBuilder, TokenName}; use crate::account::interface::{ AccountComponentInterface, AccountInterface, @@ -55,15 +55,13 @@ fn test_basic_wallet_default_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("POL").unwrap(), TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), - TokenName::new("POL").unwrap(), - None, - None, - None, ) + .build() .expect("failed to create token metadata"), ) .with_component(BasicFungibleFaucet) @@ -326,15 +324,13 @@ fn test_basic_fungible_faucet_custom_notes() { .account_type(AccountType::FungibleFaucet) .with_auth_component(get_mock_falcon_auth_component()) .with_component( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("POL").unwrap(), TokenSymbol::new("POL").expect("invalid token symbol"), 10, Felt::new(100), - TokenName::new("POL").unwrap(), - None, - None, - None, ) + .build() .expect("failed to create token metadata"), ) .with_component(BasicFungibleFaucet) diff --git a/crates/miden-standards/src/account/metadata/mod.rs b/crates/miden-standards/src/account/metadata/mod.rs index 0f9c1a8155..e33086c8fa 100644 --- a/crates/miden-standards/src/account/metadata/mod.rs +++ b/crates/miden-standards/src/account/metadata/mod.rs @@ -55,7 +55,7 @@ //! .with_description(Description::new("A cool token").unwrap(), true) //! .with_logo_uri(LogoURI::new("https://example.com/logo.png").unwrap(), false); //! -//! let metadata = FungibleTokenMetadata::new(/* ... */).unwrap(); +//! let metadata = FungibleTokenMetadataBuilder::new(name, symbol, decimals, max_supply).build().unwrap(); //! let account = AccountBuilder::new(seed) //! .with_component(metadata) //! .with_component(BasicFungibleFaucet) @@ -207,6 +207,7 @@ mod tests { BasicFungibleFaucet, Description, FungibleTokenMetadata, + FungibleTokenMetadataBuilder, TokenName, }; @@ -214,16 +215,16 @@ mod tests { name: TokenName, description: Option, ) -> FungibleTokenMetadata { - FungibleTokenMetadata::new( + let mut builder = FungibleTokenMetadataBuilder::new( + name, miden_protocol::asset::TokenSymbol::new("TST").unwrap(), 2, miden_protocol::Felt::new(1_000), - name, - description, - None, - None, - ) - .unwrap() + ); + if let Some(desc) = description { + builder = builder.description(desc); + } + builder.build().unwrap() } fn build_account_with_metadata( diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 817ed06ac2..8ae8c1bb93 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -49,7 +49,7 @@ use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word}; use miden_standards::account::access::Ownable2Step; use miden_standards::account::faucets::{ BasicFungibleFaucet, - FungibleTokenMetadata, + FungibleTokenMetadataBuilder, NetworkFungibleFaucet, TokenName, }; @@ -331,15 +331,13 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol) .with_context(|| format!("invalid token symbol: {token_symbol}"))?; let max_supply_felt = Felt::try_from(max_supply)?; - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + name, token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply_felt, - name, - None, - None, - None, ) + .build() .context("failed to create FungibleTokenMetadata")?; let account_builder = AccountBuilder::new(self.rng.random()) @@ -367,16 +365,14 @@ impl MockChainBuilder { let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + name, token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, - name, - None, - None, - None, ) - .and_then(|m| m.with_token_supply(token_supply)) + .token_supply(token_supply) + .build() .context("failed to create fungible token metadata")?; let account_builder = AccountBuilder::new(self.rng.random()) @@ -404,16 +400,14 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + name, token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply, - name, - None, - None, - None, ) - .and_then(|m| m.with_token_supply(token_supply)) + .token_supply(token_supply) + .build() .context("failed to create fungible token metadata")?; let account_builder = AccountBuilder::new(self.rng.random()) @@ -433,7 +427,7 @@ impl MockChainBuilder { pub fn add_existing_network_faucet_with_metadata_info( &mut self, owner_account_id: AccountId, - metadata: FungibleTokenMetadata, + metadata: miden_standards::account::faucets::FungibleTokenMetadata, ) -> anyhow::Result { let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index 95efeee3c1..474a88d9ac 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -26,6 +26,7 @@ use miden_standards::account::faucets::{ Description, ExternalLink, FungibleTokenMetadata, + FungibleTokenMetadataBuilder, LogoURI, NetworkFungibleFaucet, TokenName, @@ -62,35 +63,26 @@ fn network_faucet_metadata( let name = TokenName::new(token_symbol).unwrap_or_else(|_| TokenName::default()); let token_symbol = TokenSymbol::new(token_symbol)?; - let mut metadata = FungibleTokenMetadata::new( - token_symbol, - 10, - max_supply, - name, - description.map(|(words, _)| { - Description::try_from_words(&words).expect("valid description words") - }), - logo_uri.map(|(words, _)| { - LogoURI::try_from_words(&words).expect("valid logo_uri words") - }), - external_link.map(|(words, _)| { - ExternalLink::try_from_words(&words).expect("valid external_link words") - }), - ) - .and_then(|m| m.with_token_supply(token_supply))? - .with_max_supply_mutable(max_supply_mutable); - - if let Some((_, mutable)) = description { - metadata = metadata.with_description_mutable(mutable); + let mut builder = FungibleTokenMetadataBuilder::new(name, token_symbol, 10, max_supply) + .token_supply(token_supply) + .max_supply_mutable(max_supply_mutable); + if let Some((words, mutable)) = description { + builder = builder + .description(Description::try_from_words(&words).expect("valid description words")) + .description_mutable(mutable); } - if let Some((_, mutable)) = logo_uri { - metadata = metadata.with_logo_uri_mutable(mutable); + if let Some((words, mutable)) = logo_uri { + builder = builder + .logo_uri(LogoURI::try_from_words(&words).expect("valid logo_uri words")) + .logo_uri_mutable(mutable); } - if let Some((_, mutable)) = external_link { - metadata = metadata.with_external_link_mutable(mutable); + if let Some((words, mutable)) = external_link { + builder = builder + .external_link(ExternalLink::try_from_words(&words).expect("valid external_link words")) + .external_link_mutable(mutable); } - Ok(metadata) + Ok(builder.build()?) } fn initial_field_data() -> [Word; 7] { @@ -137,29 +129,25 @@ fn non_owner_account_id() -> AccountId { /// Build a minimal faucet metadata (no optional fields). fn build_faucet_metadata() -> FungibleTokenMetadata { - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - None, - None, - None, ) + .build() .unwrap() } /// Build a standard POL faucet metadata (used by scalar getter tests). fn build_pol_faucet_metadata() -> FungibleTokenMetadata { - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("POL").unwrap(), TokenSymbol::new("POL").unwrap(), 8, Felt::new(1_000_000), - TokenName::new("POL").unwrap(), - None, - None, - None, ) + .build() .unwrap() } @@ -215,15 +203,13 @@ async fn get_name_from_masm() -> anyhow::Result<()> { let token_name = TokenName::new("test name").unwrap(); let name = token_name.to_words(); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + token_name, "TST".try_into().unwrap(), 2, Felt::new(1_000), - token_name, - None, - None, - None, ) + .build() .unwrap(); let account = AccountBuilder::new([1u8; 32]) @@ -255,15 +241,13 @@ async fn get_name_from_masm() -> anyhow::Result<()> { #[tokio::test] async fn get_name_zeros_returns_empty() -> anyhow::Result<()> { // Build a faucet with an empty name to verify get_name returns zero words. - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + TokenName::default(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::default(), - None, - None, - None, ) + .build() .unwrap(); let account = AccountBuilder::new([1u8; 32]) @@ -416,18 +400,17 @@ async fn faucet_get_decimals_symbol_and_max_supply() -> anyhow::Result<()> { #[tokio::test] async fn get_mutability_config() -> anyhow::Result<()> { - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - Some(Description::new("test").unwrap()), - None, - None, ) - .unwrap() - .with_description_mutable(true) - .with_max_supply_mutable(true); + .description(Description::new("test").unwrap()) + .description_mutable(true) + .max_supply_mutable(true) + .build() + .unwrap(); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -466,88 +449,82 @@ async fn is_field_mutable_checks() -> anyhow::Result<()> { 1, ), ( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - Some(desc.clone()), - None, - None, ) - .unwrap() - .with_description_mutable(true), + .description(desc.clone()) + .description_mutable(true) + .build() + .unwrap(), "is_description_mutable", 1, ), ( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - Some(desc), - None, - None, ) + .description(desc) + .build() .unwrap(), "is_description_mutable", 0, ), ( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - None, - Some(logo.clone()), - None, ) - .unwrap() - .with_logo_uri_mutable(true), + .logo_uri(logo.clone()) + .logo_uri_mutable(true) + .build() + .unwrap(), "is_logo_uri_mutable", 1, ), ( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - None, - Some(logo), - None, ) + .logo_uri(logo) + .build() .unwrap(), "is_logo_uri_mutable", 0, ), ( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - None, - None, - Some(link.clone()), ) - .unwrap() - .with_external_link_mutable(true), + .external_link(link.clone()) + .external_link_mutable(true) + .build() + .unwrap(), "is_external_link_mutable", 1, ), ( - FungibleTokenMetadata::new( + FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - None, - None, - Some(link), ) + .external_link(link) + .build() .unwrap(), "is_external_link_mutable", 0, @@ -589,15 +566,14 @@ fn faucet_with_metadata_storage_layout() { let description = Description::new(desc_text).unwrap(); let desc_words = description.to_words(); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + token_name, "TST".try_into().unwrap(), 8, Felt::new(1_000_000), - token_name, - Some(description), - None, - None, ) + .description(description) + .build() .unwrap(); let account = AccountBuilder::new([1u8; 32]) @@ -627,15 +603,13 @@ fn faucet_with_metadata_storage_layout() { fn name_32_bytes_accepted() { let max_name = "a".repeat(TokenName::MAX_BYTES); let token_name = TokenName::new(&max_name).unwrap(); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + token_name, "TST".try_into().unwrap(), 2, Felt::new(1_000), - token_name, - None, - None, - None, ) + .build() .unwrap(); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -664,15 +638,14 @@ fn description_7_words_full_capacity() { let desc_text = "a".repeat(Description::MAX_BYTES); let description = Description::new(&desc_text).unwrap(); let desc_words = description.to_words(); - let metadata = FungibleTokenMetadata::new( + let metadata = FungibleTokenMetadataBuilder::new( + TokenName::new("T").unwrap(), "TST".try_into().unwrap(), 2, Felt::new(1_000), - TokenName::new("T").unwrap(), - Some(description), - None, - None, ) + .description(description) + .build() .unwrap(); let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::FungibleFaucet) @@ -703,15 +676,14 @@ fn verify_faucet_with_max_name_and_description( let description = Description::new(&desc_text).unwrap(); let desc_words = description.to_words(); - let faucet_metadata = FungibleTokenMetadata::new( + let faucet_metadata = FungibleTokenMetadataBuilder::new( + TokenName::new(&max_name).unwrap(), symbol.try_into().unwrap(), 6, Felt::new(max_supply), - TokenName::new(&max_name).unwrap(), - Some(description), - None, - None, ) + .description(description) + .build() .unwrap(); let mut builder = AccountBuilder::new(seed) @@ -771,15 +743,14 @@ async fn basic_faucet_name_readable_from_masm() -> anyhow::Result<()> { let token_name = TokenName::new("readable name").unwrap(); let name = token_name.to_words(); - let faucet_metadata = FungibleTokenMetadata::new( + let faucet_metadata = FungibleTokenMetadataBuilder::new( + token_name, "MAS".try_into().unwrap(), 10, Felt::new(999_999), - token_name, - Some(Description::new("readable description").unwrap()), - None, - None, ) + .description(Description::new("readable description").unwrap()) + .build() .unwrap(); let account = AccountBuilder::new([3u8; 32]) @@ -814,15 +785,14 @@ async fn network_faucet_name_readable_from_masm() -> anyhow::Result<()> { let max_name = "b".repeat(TokenName::MAX_BYTES); let name_words = TokenName::new(&max_name).unwrap().to_words(); - let network_faucet_metadata = FungibleTokenMetadata::new( + let network_faucet_metadata = FungibleTokenMetadataBuilder::new( + TokenName::new(&max_name).unwrap(), "MAS".try_into().unwrap(), 6, Felt::new(1_000_000), - TokenName::new(&max_name).unwrap(), - Some(Description::new("network faucet description").unwrap()), - None, - None, ) + .description(Description::new("network faucet description").unwrap()) + .build() .unwrap(); let account = AccountBuilder::new([7u8; 32])