From 59204515c8b397f9f7462a4f453229af182310d7 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 6 Aug 2026 16:48:44 +0300 Subject: [PATCH 01/11] refactor(agglayer): use the standard `FungibleFaucet` for AggLayer faucets The dedicated `agglayer::faucet` component had become vestigial: it re-exported `mint_and_send`, `receive_and_burn` and `has_procedure` from the standards library, a strict subset of what the standard `FungibleFaucet` component already exports, and `AggLayerFaucet` was a newtype over `FungibleFaucet` emitting its storage slots verbatim. The re-exports resolve to the same standards procedures, so the mint/burn MAST roots are unchanged by the swap and the reflective faucet-kind detection in the unified MINT/BURN note scripts keeps working. Critically, `AggLayerFaucet::new` fabricated the token name from the symbol, which left the AggLayer metadata hash preimage `abi.encode(name, symbol, decimals)` unrecoverable from faucet storage. The builders now take a real token name, which is what #2586 needs to verify the registered hash on-chain. - Remove the `miden-agglayer-faucet` MASM package, `asm/agglayer/faucet/`, `src/faucet.rs`, `AggLayerFaucet`, `AgglayerFaucetError` and the build.rs- generated `FAUCET_CODE_COMMITMENT`. Faucet identity is now established through `FungibleFaucet::try_from(&Account)`, which checks the interface rather than a commitment that had to hand-mirror the component stack. - `AggLayerFaucet::account_builder` / `::allowed_notes` become the free functions `agglayer_faucet_account_builder` / `agglayer_faucet_allowed_notes`, since the type they hung off no longer exists. The account's component stack is otherwise unchanged: Ownable2Step for the owner-gated mint and burn policies, RBAC plus `Authority::RbacControlled` for authority-gated configuration, the token policy manager, and `ConstantFeeManager`. - Add `MetadataHash::from_fungible_faucet` so call sites derive the registered hash from the faucet's own stored metadata instead of repeating three literals. The faucet code commitment changes. Closes #2585 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + .../src/context_setups/mod.rs | 8 +- crates/miden-agglayer/SPEC.md | 57 ++- .../asm/agglayer/faucet/mod.masm | 6 - crates/miden-agglayer/asm/agglayer/mod.masm | 1 - .../asm/components/faucet/faucet.masm | 14 - .../asm/components/faucet/miden-project.toml | 14 - .../asm/components/miden-project.toml | 2 +- crates/miden-agglayer/build.rs | 34 +- .../src/eth_types/metadata_hash.rs | 33 ++ crates/miden-agglayer/src/faucet.rs | 353 ------------------ crates/miden-agglayer/src/lib.rs | 207 ++++++---- crates/miden-agglayer/src/testing/mod.rs | 9 +- .../miden-testing/tests/agglayer/bridge_in.rs | 16 + .../tests/agglayer/bridge_out.rs | 25 +- .../tests/agglayer/faucet_helpers.rs | 38 +- .../tests/agglayer/fee_policy.rs | 22 +- .../agglayer/network_account_regression.rs | 4 +- .../tests/agglayer/rbac_rotation.rs | 1 + .../tests/agglayer/test_utils.rs | 14 +- 20 files changed, 311 insertions(+), 548 deletions(-) delete mode 100644 crates/miden-agglayer/asm/agglayer/faucet/mod.masm delete mode 100644 crates/miden-agglayer/asm/components/faucet/faucet.masm delete mode 100644 crates/miden-agglayer/asm/components/faucet/miden-project.toml delete mode 100644 crates/miden-agglayer/src/faucet.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0301ff4379..787cbf8d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - [BREAKING] Narrowed the block header version field from 32 to 8 bits and set it to the only supported version instead of taking it as a `BlockHeader::new` parameter ([#3695](https://github.com/0xMiden/protocol/pull/3695)). - [BREAKING] Serialize the version in `Account`, `AccountHeader`, `PartialNoteMetadata` and `AssetId` ([#3697](https://github.com/0xMiden/protocol/pull/3697)). - [BREAKING] Moved the account delta and patch domain separators into the hasher capacity word. Added a version to their commitments ([#3698](https://github.com/0xMiden/protocol/pull/3698)). +- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`: the `AggLayerFaucet` / `AgglayerFaucetError` types and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` / `AggLayerFaucet::allowed_notes` became the free functions `agglayer_faucet_account_builder` / `agglayer_faucet_allowed_notes`, and both they and `create_existing_agglayer_faucet` now take a token name. The faucet stores the real token name instead of a copy of its symbol, so the AggLayer metadata hash preimage `abi.encode(name, symbol, decimals)` is recoverable from faucet storage; the faucet code commitment changes ([#2585](https://github.com/0xMiden/protocol/issues/2585)). ### Fixes diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index c3153831a0..916e5defe0 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -28,6 +28,7 @@ use miden_protocol::testing::account_id::{ACCOUNT_ID_FEE_FAUCET, ACCOUNT_ID_SEND use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; use miden_standards::account::auth::SponsorshipPolicy; +use miden_standards::account::faucets::FungibleFaucet; use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager}; use miden_standards::code_builder::CodeBuilder; use miden_standards::interop::eth::EthAddress; @@ -498,6 +499,7 @@ pub async fn tx_consume_claim_note( let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -509,6 +511,7 @@ pub async fn tx_consume_claim_note( let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -706,6 +709,7 @@ pub async fn tx_consume_b2agg_note( let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "AggLayer Token", "AGG", 8, FungibleAsset::MAX_AMOUNT.into(), @@ -716,7 +720,8 @@ pub async fn tx_consume_b2agg_note( builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - let metadata_hash = MetadataHash::from_token_info("AGG", "AGG", 8); + // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. + let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { faucet_account_id: faucet.id(), @@ -791,6 +796,7 @@ fn setup_faucet_registration( let agglayer_faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "AggLayer Token", "AGG", 8, FungibleAsset::MAX_AMOUNT.into(), diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 76da54c200..8fb945a544 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -21,7 +21,7 @@ implementation are called out inline with `TODO (Future)` markers. |--------|-------------|--------------| | **User** | End-user Miden account that holds assets and initiates bridge-out deposits, or receives assets from a bridge-in claim. | Any account with `basic_wallet` component | | **AggLayer Bridge** | Onchain bridge account that manages the Local Exit Tree (LET), faucet registry, and GER state. Consumes B2AGG, CONFIG, and UPDATE_GER notes. | Network-mode account with a single `bridge` component | -| **AggLayer Faucet** | Fungible faucet that represents a single bridged token. Mints on bridge-in claims, burns on bridge-out. Each foreign token has its own faucet instance. | `FungibleFaucet`, network-mode, with `agglayer_faucet` component | +| **AggLayer Faucet** | Fungible faucet that represents a single bridged token. Mints on bridge-in claims, burns on bridge-out. Each foreign token has its own faucet instance. | `FungibleFaucet`, network-mode, owned by the bridge | | **Integration Service** (offchain) | Observes L1 events (deposits, GER updates) and creates UPDATE_GER and CLAIM notes on Miden. Trusted to provide correct proofs and data. | Creates notes targeting bridge/faucet | | **Bridge Operator** (offchain) | Deploys bridge and faucet accounts. Creates CONFIG_AGG_BRIDGE notes to register faucets. Must hold the `FAUCET_MNGR` role. | Creates config notes | | **Bridge Admin (`BRIDGE_ADMIN`)** (offchain) | Holds the bridge account's built-in `ADMIN` role. Manages bridge roles, restores a paused bridge, and controls allowlists and fee-policy selection. | Creates RBAC_CONFIG, PAUSE_CONFIG, and NETWORK_ACCOUNT_CONFIG notes | @@ -480,19 +480,28 @@ holders are seeded into the access-control components at account creation time. ### 3.2 Faucet Account Component -The faucet account has the `agglayer_faucet` component (`components/faucet.masm`), -which is a thin wrapper, on top of `Ownable2Step` + the RBAC access-control stack -(`RoleBasedAccessControl` + `Authority::RbacControlled`), that re-exports the -standard Miden fungible-faucet procedures: +The faucet account carries no AggLayer-specific component. It is the standard `FungibleFaucet` +component (`miden-standards/asm/components/faucets/fungible_faucet/fungible_faucet.masm`), +installed on top of `Ownable2Step` + the RBAC access-control stack (`RoleBasedAccessControl` + +`Authority::RbacControlled`), so the AggLayer faucet is an ordinary network fungible faucet whose +owner happens to be the bridge. -- `mint_and_send` (from `miden::standards::faucets::fungible::mint_and_send`) -- `receive_and_burn` (from `miden::standards::faucets::fungible::receive_and_burn`) -- `has_procedure` (from the `CodeInspection` component, so the unified MINT/BURN note scripts - can reflectively detect the faucet kind) +The procedures the bridge protocol relies on are: -The underlying library code lives in `asm/agglayer/faucet/mod.masm`. +- `mint_and_send` - bridge-in, gated by the active mint policy (`owner_only`) +- `receive_and_burn` - bridge-out, gated by the active burn policy +- `has_procedure` - so the unified MINT/BURN note scripts can reflectively detect the faucet kind -#### `agglayer_faucet::mint_and_send` +The component additionally exposes the standard token-config and metadata accessors - +`get_name`, `get_token_symbol`, `get_decimals`, `get_token_supply`, `get_max_supply`, +`get_token_config`, `is_max_supply_mutable`, `get_mutability_config`, the `is_*_mutable` views, +and the `set_max_supply` / `set_description` / `set_logo_uri` / `set_external_link` setters. The +getters are what allow the bridge to recompute `keccak256(abi.encode(name, symbol, decimals))` +from faucet storage via FPI. The setters are authority-gated (so they fall to `FAUCET_ADMIN`) and, +with the faucet's note allowlist limited to MINT and BURN, unreachable until the admin explicitly +allowlists a note that calls them. + +#### `fungible::mint_and_send` | | | |-|-| @@ -502,7 +511,7 @@ The underlying library code lives in `asm/agglayer/faucet/mod.masm`. | **Context** | Consuming a `MINT` note on the faucet account | | **Panics** | Faucet owner verification fails; minting exceeds supply; the asset stored in the MINT note does not belong to the consuming faucet | -Re-export of `miden::standards::faucets::fungible::mint_and_send`. Mints the asset +Mints the asset identified by `ASSET_ID` / `ASSET_VALUE` and creates an output note with the given recipient. Requires the faucet's owner (the bridge account) to be the creator of this note (the bridge is stored in `Ownable2Step` storage slot as the owner; the faucet's @@ -511,9 +520,9 @@ recipient. Requires the faucet's owner (the bridge account) to be the creator of for the active faucet and panics if the stored `ASSET_ID` does not belong to that faucet, which binds the MINT note to its resolved faucet (see §4.10). -#### `agglayer_faucet::receive_and_burn` +#### `fungible::receive_and_burn` -This is a re-export of `miden::standards::faucets::fungible::receive_and_burn`. It burns the fungible asset from the active note, decreasing the faucet's token supply. +Burns the fungible asset from the active note, decreasing the faucet's token supply. | | | |-|-| @@ -525,9 +534,20 @@ This is a re-export of `miden::standards::faucets::fungible::receive_and_burn`. #### Faucet Account Storage +The faucet contributes the full standard `FungibleFaucet` slot set (25 value slots): + | Slot name | Slot type | Value encoding | Purpose | |-----------|-----------|----------------|---------| -| Faucet metadata (standard) | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard `NetworkFungibleFaucet` metadata | +| `token_config` | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard fungible faucet token configuration | +| `token_name_0` .. `token_name_1` | Value | Fixed-width string chunks | Token name, up to 32 UTF-8 bytes. Part of the AggLayer metadata hash preimage | +| `mutability_config` | Value | Boolean flags | Whether description / logo URI / external link / max supply may be updated. All `false` for AggLayer faucets | +| `token_description_0` .. `_6` | Value | Fixed-width string chunks | Optional description; empty for AggLayer faucets | +| `logo_uri_0` .. `_6` | Value | Fixed-width string chunks | Optional logo URI; empty for AggLayer faucets | +| `external_link_0` .. `_6` | Value | Fixed-width string chunks | Optional external link; empty for AggLayer faucets | + +Conversion metadata (origin token address, origin network, scale, metadata hash) is **not** stored +here. It lives on the bridge in `faucet_metadata_map`, written at registration time (see +[Section 7](#7-faucet-registry)). **Companion component storage slots:** @@ -1420,9 +1440,10 @@ be referenced by a `B2AGG` (bridge-out) or `CLAIM` (bridge-in) note. Registratio same flow for both kinds; the `is_native` flag in the `CONFIG_AGG_BRIDGE` note storage tells the bridge which dispatch path to take for each future bridge operation against that faucet. -The `AggLayerFaucet` Rust struct (`src/faucet.rs`) holds only -token metadata — symbol, decimals, max supply, and token supply -(TODO Missing token name ([#2585](https://github.com/0xMiden/protocol/issues/2585))). +The faucet holds only token metadata — name, symbol, decimals, max supply, and token supply. +Because the name is stored alongside the symbol and decimals, the full metadata hash preimage +`abi.encode(name, symbol, decimals)` is recoverable from faucet storage, which is what makes +registration-time verification possible. Conversion metadata (origin address, origin network, scale, and metadata hash) is *not* stored on the faucet; it is carried by the `CONFIG_AGG_BRIDGE` note at registration time and written directly into the bridge's `faucet_metadata_map`. The metadata hash is diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm deleted file mode 100644 index 1c35106856..0000000000 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ /dev/null @@ -1,6 +0,0 @@ -# PUBLIC INTERFACE -# ================================================================================================= - -pub use {receive_and_burn} from miden::standards::faucets::fungible -pub use {mint_and_send} from miden::standards::faucets::fungible -pub use {has_procedure} from miden::standards::inspection::code_inspection diff --git a/crates/miden-agglayer/asm/agglayer/mod.masm b/crates/miden-agglayer/asm/agglayer/mod.masm index f0bafa9be4..837495b869 100644 --- a/crates/miden-agglayer/asm/agglayer/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/mod.masm @@ -1,3 +1,2 @@ pub mod bridge -pub mod faucet pub mod notes diff --git a/crates/miden-agglayer/asm/components/faucet/faucet.masm b/crates/miden-agglayer/asm/components/faucet/faucet.masm deleted file mode 100644 index aa53edd522..0000000000 --- a/crates/miden-agglayer/asm/components/faucet/faucet.masm +++ /dev/null @@ -1,14 +0,0 @@ -# The MASM code of the AggLayer Faucet Account Component. -# -# This is a thin wrapper that re-exports faucet-related procedures from the agglayer library. -# -# The faucet exposes: -# - `mint_and_send` from the fungible faucet (for MINT note consumption, with owner verification -# gated by the active mint policy) -# - `receive_and_burn` for bridge-out -# - `has_procedure` from the CodeInspection component, so the unified MINT/BURN note scripts can -# reflectively detect the faucet kind. - -pub use {mint_and_send} from agglayer::faucet -pub use {receive_and_burn} from agglayer::faucet -pub use {has_procedure} from agglayer::faucet diff --git a/crates/miden-agglayer/asm/components/faucet/miden-project.toml b/crates/miden-agglayer/asm/components/faucet/miden-project.toml deleted file mode 100644 index c4c2bec004..0000000000 --- a/crates/miden-agglayer/asm/components/faucet/miden-project.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "miden-agglayer-faucet" -version.workspace = true - -[lib] -kind = "account-component" -namespace = "agglayer::components::faucet" -path = "faucet.masm" - -[dependencies] -miden-agglayer.workspace = true -miden-core.workspace = true -miden-protocol.workspace = true -miden-standards.workspace = true diff --git a/crates/miden-agglayer/asm/components/miden-project.toml b/crates/miden-agglayer/asm/components/miden-project.toml index 393e2f8a34..68226a3ab4 100644 --- a/crates/miden-agglayer/asm/components/miden-project.toml +++ b/crates/miden-agglayer/asm/components/miden-project.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bridge", "faucet"] +members = ["bridge"] [workspace.package] version = "0.16.0" diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index 95913e00bb..cb4bc3e4db 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -32,12 +32,6 @@ use miden_standards::account::fees::{ ConstantFeeManager, FeePolicyManager, }; -use miden_standards::account::policies::{ - BurnPolicy, - MintPolicy, - TokenPolicyManager, - TransferPolicy, -}; // CONSTANTS // ================================================================================================ @@ -152,7 +146,11 @@ fn compile_agglayer_package( /// /// This file contains: /// - AggLayer Bridge code commitment. -/// - AggLayer Faucet code commitment. +/// +/// The AggLayer faucet is the standard [`FungibleFaucet`] and so carries no bespoke commitment; +/// callers identify it through its interface instead (see `FungibleFaucet::try_from`). +/// +/// [`FungibleFaucet`]: miden_standards::account::faucets::FungibleFaucet fn generate_agglayer_constants( target_file: impl AsRef, component_packages: Vec>, @@ -177,9 +175,9 @@ fn generate_agglayer_constants( // code commitment, so it doesn't matter what does this metadata holds. let dummy_metadata = AccountComponentMetadata::new("dummy"); - // iterate over the AggLayer Bridge and AggLayer Faucet packages + // iterate over the AggLayer component packages (currently only the bridge) for package in component_packages { - // Derive the short component name (e.g. "bridge" / "faucet") from the package name + // Derive the short component name (e.g. "bridge") from the package name // (e.g. "miden-agglayer-bridge"). let component_name = package .name @@ -217,24 +215,6 @@ fn generate_agglayer_constants( components.push(AccountComponent::from(PausableManager)); components .push(AccountComponent::from(ConstantFeeManager::for_basic_constant_fee_policy())); - } else if component_name == "faucet" { - components.push(AccountComponent::from( - miden_standards::account::access::Ownable2Step::new(dummy_account_id), - )); - components.extend(AccessControl::Rbac { - admin: dummy_account_id, - procedure_roles: std::collections::BTreeMap::new(), - }); - let token_policy_manager = TokenPolicyManager::builder() - .active_mint_policy(MintPolicy::owner_only()) - .active_burn_policy(BurnPolicy::owner_only()) - .active_send_policy(TransferPolicy::allow_all()) - .active_receive_policy(TransferPolicy::allow_all()) - .build(); - - components.extend(token_policy_manager); - components - .push(AccountComponent::from(ConstantFeeManager::for_basic_constant_fee_policy())); } // use `AccountCode` to merge codes of agglayer and authentication components diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index 617c6ea5b0..f28536bdd9 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -1,9 +1,11 @@ +use alloc::string::ToString; use alloc::vec::Vec; use alloy_sol_types::{SolValue, sol}; use miden_core::utils::bytes_to_packed_u32_elements; use miden_crypto::hash::keccak::Keccak256; use miden_protocol::Felt; +use miden_standards::account::faucets::FungibleFaucet; // ================================================================================================ // METADATA HASH @@ -42,6 +44,18 @@ impl MetadataHash { Self::from_abi_encoded(&encoded) } + /// Computes the metadata hash from a faucet's own token metadata. + /// + /// Preferred over [`Self::from_token_info`] when the faucet exists, since it derives the hash + /// registered on the bridge from the very values held in faucet storage: the two cannot drift. + pub fn from_fungible_faucet(faucet: &FungibleFaucet) -> Self { + Self::from_token_info( + faucet.token_name().as_str(), + &faucet.symbol().to_string(), + faucet.decimals(), + ) + } + /// Returns the raw 32-byte array. pub const fn as_bytes(&self) -> &[u8; 32] { &self.0 @@ -124,6 +138,25 @@ mod tests { assert_eq!(hash, hash_from_info, "from_abi_encoded and from_token_info must agree"); } + /// A faucet's own metadata yields the same hash as passing its name, symbol and decimals + /// individually. This is the invariant that lets the bridge recompute the registered hash + /// from faucet storage (see issue #2586). + #[test] + fn test_metadata_hash_from_fungible_faucet_matches_token_info() { + let faucet = crate::agglayer_faucet_metadata( + "Test Token", + "TEST", + 12, + Felt::from(1000u32), + Felt::ZERO, + ); + + assert_eq!( + MetadataHash::from_fungible_faucet(&faucet), + MetadataHash::from_token_info("Test Token", "TEST", 12), + ); + } + fn hex_to_vec(hex: &str) -> std::vec::Vec { (0..hex.len()) .step_by(2) diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs deleted file mode 100644 index d86b65fa58..0000000000 --- a/crates/miden-agglayer/src/faucet.rs +++ /dev/null @@ -1,353 +0,0 @@ -extern crate alloc; - -use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::string::ToString; -use alloc::vec; -use alloc::vec::Vec; - -use miden_core::{Felt, Word}; -use miden_protocol::account::component::AccountComponentMetadata; -use miden_protocol::account::{ - Account, - AccountComponent, - AccountId, - AccountProcedureRoot, - RoleSymbol, - StorageSlot, - StorageSlotName, -}; -use miden_protocol::asset::{AssetAmount, TokenSymbol}; -use miden_protocol::errors::AccountIdError; -use miden_protocol::note::NoteScriptRoot; -use miden_standards::account::access::{Authority, Ownable2Step}; -use miden_standards::account::auth::AuthNetworkAccount; -use miden_standards::account::faucets::{FungibleFaucet, FungibleFaucetError, TokenName}; -use miden_standards::account::fees::ConstantFeeManager; -use miden_standards::account::policies::TokenPolicyManager; -pub use miden_standards::interop::eth::{ - EthAddress, - EthAmount, - EthAmountError, - EthEmbeddedAccountId, -}; -use miden_standards::note::{BurnNote, ConstantFeePolicyConfigNote, MintNote, RbacConfigNote}; -use miden_utils_sync::LazyLock; -use thiserror::Error; - -use super::agglayer_faucet_component_package; -pub use crate::{ - AggLayerBridge, - B2AggNote, - ClaimNoteStorage, - ConfigAggBridgeNote, - ExitRoot, - GlobalIndex, - GlobalIndexError, - LeafData, - MetadataHash, - ProofData, - SmtNode, - UpdateGerNote, -}; - -// CONSTANTS -// ================================================================================================ -// Include the generated agglayer constants -include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs")); - -// FAUCET RBAC ROLES -// ================================================================================================ - -static FEE_MANAGER_ROLE: LazyLock = - LazyLock::new(|| RoleSymbol::new("FEE_MNGR").expect("FEE_MNGR role symbol should be valid")); - -// AGGLAYER FAUCET STRUCT -// ================================================================================================ - -/// An [`AccountComponent`] implementing the AggLayer Faucet. -/// -/// It re-exports `mint_and_send` and `receive_and_burn` from the agglayer faucet package. -/// Conversion metadata (origin address, origin network, scale, metadata hash) is held by the -/// bridge, not the faucet — see -/// [`AggLayerBridge`] and the `faucet_metadata_map` populated on registration. -/// -/// ## Storage Layout -/// -/// - All [`FungibleFaucet`] storage slots (token config + name + mutability + description + logo -/// URI + external link). Conversion metadata is no longer stored on the faucet; the bridge holds -/// it in `faucet_metadata_map`. -/// -/// ## Required Companion Components -/// -/// This component re-exports `fungible::mint_and_send`, which requires: -/// - [`Ownable2Step`]: Provides ownership data (bridge account ID as owner). -/// - [`miden_standards::account::policies::TokenPolicyManager`]: Provides mint and burn policy -/// management. -/// -/// These must be added as separate components when building the faucet account. -#[derive(Debug, Clone)] -pub struct AggLayerFaucet { - faucet: FungibleFaucet, -} - -impl AggLayerFaucet { - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new AggLayer faucet component from the given configuration. - /// - /// The faucet's display name is derived from the symbol (an AggLayer faucet is identified by - /// its symbol; the human-readable name is not used in the bridge protocol). - /// - /// # Errors - /// Returns an error if: - /// - The decimals parameter exceeds maximum value of [`FungibleFaucet::MAX_DECIMALS`]. - /// - The max supply exceeds maximum possible amount for a fungible asset. - /// - The token supply exceeds the max supply. - pub fn new( - symbol: TokenSymbol, - decimals: u8, - max_supply: Felt, - token_supply: Felt, - ) -> Result { - // Use the symbol as the display name; AggLayer faucets do not use a separate token name. - let name = TokenName::new(symbol.to_string().as_str()) - .expect("symbol fits within token name capacity"); - let max_supply_amount = AssetAmount::try_from(max_supply).map_err(|_| { - FungibleFaucetError::MaxSupplyTooLarge { - actual: max_supply.as_canonical_u64(), - max: AssetAmount::MAX.as_u64(), - } - })?; - let token_supply_amount = AssetAmount::try_from(token_supply).map_err(|_| { - FungibleFaucetError::MaxSupplyTooLarge { - actual: token_supply.as_canonical_u64(), - max: AssetAmount::MAX.as_u64(), - } - })?; - let faucet = FungibleFaucet::builder() - .name(name) - .symbol(symbol) - .decimals(decimals) - .max_supply(max_supply_amount) - .token_supply(token_supply_amount) - .build()?; - Ok(Self { faucet }) - } - - /// Sets the token supply for an existing faucet (e.g. for testing scenarios). - /// - /// # Errors - /// Returns an error if the token supply exceeds the max supply. - pub fn with_token_supply(mut self, token_supply: Felt) -> Result { - let token_supply_amount = AssetAmount::try_from(token_supply).map_err(|_| { - FungibleFaucetError::MaxSupplyTooLarge { - actual: token_supply.as_canonical_u64(), - max: AssetAmount::MAX.as_u64(), - } - })?; - self.faucet = self.faucet.with_token_supply(token_supply_amount)?; - Ok(self) - } - - // RBAC ROLES - // -------------------------------------------------------------------------------------------- - - /// Returns the `FEE_MNGR` role symbol. Holders may update the faucet's note fee schedule. - pub fn fee_manager_role() -> RoleSymbol { - FEE_MANAGER_ROLE.clone() - } - - /// Returns the fixed procedure-to-role map used to configure the faucet's `Authority` - /// (`RbacControlled`) component. - pub fn procedure_roles() -> BTreeMap { - BTreeMap::from([(ConstantFeeManager::set_note_fee_root(), Self::fee_manager_role())]) - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Storage slot name for the token config word - /// `[token_supply, max_supply, decimals, token_symbol]`. - pub fn token_config_slot() -> &'static StorageSlotName { - FungibleFaucet::token_config_slot() - } - - /// Storage slot name for the owner account ID (bridge), provided by the - /// [`Ownable2Step`] companion component. - pub fn owner_config_slot() -> &'static StorageSlotName { - Ownable2Step::slot_name() - } - - // ALLOWED NOTES - // -------------------------------------------------------------------------------------------- - - /// Returns the input-note script roots allowlisted on a newly deployed AggLayer faucet. - /// - /// A live account's allowlist is available through - /// [`NetworkAccount::allowed_notes`](miden_standards::account::auth::NetworkAccount::allowed_notes). - pub fn allowed_notes() -> BTreeSet { - let mut notes = BTreeSet::from([ - MintNote::script_root(), - BurnNote::script_root(), - ConstantFeePolicyConfigNote::script_root(), - RbacConfigNote::script_root(), - ]); - notes.extend(AuthNetworkAccount::default_allowed_note_scripts()); - notes - } - - /// Extracts the underlying [`FungibleFaucet`] component (which holds the token metadata) - /// from the storage slots of the provided account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn try_faucet_from_account( - faucet_account: &Account, - ) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - FungibleFaucet::try_from(faucet_account.storage()) - .map_err(AgglayerFaucetError::FungibleFaucetError) - } - - /// Extracts the bridge account ID from the [`Ownable2Step`] owner config storage slot - /// of the provided account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn owner_account_id(faucet_account: &Account) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - let ownership = Ownable2Step::try_from_storage(faucet_account.storage()) - .map_err(AgglayerFaucetError::Ownable2StepError)?; - ownership.owner().ok_or(AgglayerFaucetError::OwnershipRenounced) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Checks that the provided account is an [`AggLayerFaucet`] account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account does not have all AggLayer Faucet specific storage slots. - /// - the provided account does not have all AggLayer Faucet specific procedures. - fn assert_faucet_account(account: &Account) -> Result<(), AgglayerFaucetError> { - // check that the storage slots are as expected - Self::assert_storage_slots(account)?; - - // check that the procedure roots are as expected - Self::assert_code_commitment(account)?; - - Ok(()) - } - - /// Checks that the provided account has all storage slots required for the [`AggLayerFaucet`]. - /// - /// # Errors - /// - /// Returns an error if: - /// - provided account does not have all AggLayer Faucet specific storage slots). - fn assert_storage_slots(account: &Account) -> Result<(), AgglayerFaucetError> { - // get the storage slot names of the provided account - let account_storage_slot_names: Vec<&StorageSlotName> = account - .storage() - .slots() - .iter() - .map(|storage_slot| storage_slot.name()) - .collect::>(); - - // check that all bridge specific storage slots are presented in the provided account - let are_slots_present = Self::slot_names() - .iter() - .all(|slot_name| account_storage_slot_names.contains(slot_name)); - if !are_slots_present { - return Err(AgglayerFaucetError::StorageSlotsMismatch); - } - - Ok(()) - } - - /// Checks that the code commitment of the provided account matches the code commitment of the - /// [`AggLayerFaucet`]. - /// - /// # Errors - /// - /// Returns an error if: - /// - the code commitment of the provided account does not match the code commitment of the - /// [`AggLayerFaucet`]. - fn assert_code_commitment(account: &Account) -> Result<(), AgglayerFaucetError> { - if FAUCET_CODE_COMMITMENT != account.code().commitment() { - return Err(AgglayerFaucetError::CodeCommitmentMismatch); - } - - Ok(()) - } - - /// Returns a vector of all [`AggLayerFaucet`] storage slot names. - fn slot_names() -> Vec<&'static StorageSlotName> { - vec![ - FungibleFaucet::token_config_slot(), - Ownable2Step::slot_name(), - Authority::authority_slot(), - TokenPolicyManager::active_mint_policy_slot(), - TokenPolicyManager::active_burn_policy_slot(), - TokenPolicyManager::allowed_mint_policies_slot(), - TokenPolicyManager::allowed_burn_policies_slot(), - TokenPolicyManager::allowed_send_policies_slot(), - TokenPolicyManager::allowed_receive_policies_slot(), - ] - } -} - -impl From for AccountComponent { - fn from(agglayer_faucet: AggLayerFaucet) -> Self { - // Bring in all of the FungibleFaucet's storage slots (token config + name + - // mutability + description + logo URI + external link). - agglayer_faucet_component(agglayer_faucet.faucet.into_storage_slots()) - } -} - -// AGGLAYER FAUCET ERROR -// ================================================================================================ - -/// AggLayer Faucet related errors. -#[derive(Debug, Error)] -pub enum AgglayerFaucetError { - #[error( - "provided account does not have storage slots required for the AggLayer Faucet account" - )] - StorageSlotsMismatch, - #[error("provided account does not have procedures required for the AggLayer Faucet account")] - CodeCommitmentMismatch, - #[error("fungible faucet error")] - FungibleFaucetError(#[source] FungibleFaucetError), - #[error("account ID error")] - AccountIdError(#[source] AccountIdError), - #[error("ownable2step error")] - Ownable2StepError(#[source] miden_standards::account::access::Ownable2StepError), - #[error("faucet ownership has been renounced")] - OwnershipRenounced, -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Creates an Agglayer Faucet component with the specified storage slots. -fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { - let package = agglayer_faucet_component_package(); - let metadata = AccountComponentMetadata::new("agglayer::faucet") - .with_description("AggLayer faucet component"); - - AccountComponent::new(package, storage_slots, metadata).expect( - "agglayer_faucet component should satisfy the requirements of a valid account component", - ) -} diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index dcc60f2702..8f12efc72b 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -2,11 +2,19 @@ extern crate alloc; +use alloc::collections::{BTreeMap, BTreeSet}; + use miden_core::{Felt, Word}; -use miden_protocol::account::{AccountBuilder, AccountComponent, AccountId, AssetCallbackFlag}; +use miden_protocol::account::{ + AccountBuilder, + AccountId, + AccountProcedureRoot, + AssetCallbackFlag, + RoleSymbol, +}; use miden_protocol::assembly::Path; -use miden_protocol::asset::TokenSymbol; -use miden_protocol::note::NoteScript; +use miden_protocol::asset::{AssetAmount, TokenSymbol}; +use miden_protocol::note::{NoteScript, NoteScriptRoot}; use miden_protocol::vm::Package; use miden_standards::account::access::{ Authority, @@ -16,7 +24,8 @@ use miden_standards::account::access::{ RoleBasedAccessControl, RoleConfig, }; -use miden_standards::account::auth::NetworkAccount; +use miden_standards::account::auth::{AuthNetworkAccount, NetworkAccount}; +use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::fees::{ BasicConstantFeePolicy, ConstantFeeManager, @@ -28,6 +37,7 @@ use miden_standards::account::policies::{ TokenPolicyManager, TransferPolicy, }; +use miden_standards::note::{BurnNote, ConstantFeePolicyConfigNote, MintNote, RbacConfigNote}; use miden_utils_sync::LazyLock; pub mod agglayer_note; @@ -39,7 +49,6 @@ pub mod costs; pub mod deregister_note; pub mod errors; pub mod eth_types; -pub mod faucet; mod ger_note; pub mod remove_ger_note; #[cfg(any(feature = "testing", test))] @@ -65,7 +74,6 @@ pub use deregister_note::DeregisterAggFaucetNote; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{GlobalIndex, GlobalIndexError, MetadataHash}; -pub use faucet::{AggLayerFaucet, AgglayerFaucetError}; pub use remove_ger_note::RemoveGerNote; pub use update_ger_note::UpdateGerNote; pub use utils::Keccak256Output; @@ -85,13 +93,6 @@ static BRIDGE_COMPONENT_PACKAGE: LazyLock = LazyLock::new(|| { .expect("shipped bridge component package is well-formed") }); -static FAUCET_COMPONENT_PACKAGE: LazyLock = LazyLock::new(|| { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/miden-agglayer-faucet.masp")); - Package::read_from_bytes_trusted(bytes) - .expect("shipped faucet component package is well-formed") -}); - /// Returns the AggLayer package containing all agglayer modules, including the note scripts. /// /// The note scripts this crate builds are external references into this package rather than @@ -120,40 +121,81 @@ fn agglayer_bridge_component_package() -> Package { BRIDGE_COMPONENT_PACKAGE.clone() } -/// Returns the Faucet component package. -fn agglayer_faucet_component_package() -> Package { - FAUCET_COMPONENT_PACKAGE.clone() +// AGGLAYER FAUCET RBAC ROLES +// ================================================================================================ + +static FAUCET_FEE_MANAGER_ROLE: LazyLock = + LazyLock::new(|| RoleSymbol::new("FEE_MNGR").expect("FEE_MNGR role symbol should be valid")); + +/// Returns the AggLayer faucet's `FEE_MNGR` role symbol. Holders may update the faucet's note fee +/// schedule. +pub fn agglayer_faucet_fee_manager_role() -> RoleSymbol { + FAUCET_FEE_MANAGER_ROLE.clone() +} + +/// Returns the fixed procedure-to-role map used to configure the AggLayer faucet's [`Authority`] +/// (`RbacControlled`) component. +pub fn agglayer_faucet_procedure_roles() -> BTreeMap { + BTreeMap::from([(ConstantFeeManager::set_note_fee_root(), agglayer_faucet_fee_manager_role())]) } // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ -/// Creates an agglayer faucet account component with the specified configuration. +/// Returns the input-note script roots allowlisted on a newly deployed AggLayer faucet. +/// +/// A live account's allowlist is available through +/// [`NetworkAccount::allowed_notes`](miden_standards::account::auth::NetworkAccount::allowed_notes). +pub fn agglayer_faucet_allowed_notes() -> BTreeSet { + let mut notes = BTreeSet::from([ + MintNote::script_root(), + BurnNote::script_root(), + ConstantFeePolicyConfigNote::script_root(), + RbacConfigNote::script_root(), + ]); + notes.extend(AuthNetworkAccount::default_allowed_note_scripts()); + notes +} + +/// Builds the [`FungibleFaucet`] component of an agglayer faucet account. /// /// The faucet holds only token metadata; conversion metadata (origin address, origin network, /// scale, metadata hash) lives on the bridge and is populated at registration time. /// +/// The token name is stored alongside the symbol and decimals so that the bridge can recompute +/// `keccak256(abi.encode(name, symbol, decimals))` from the faucet's own storage. +/// /// # Parameters +/// - `token_name`: The display name for the fungible token (e.g., "AggLayer Token") /// - `token_symbol`: The symbol for the fungible token (e.g., "AGG") /// - `decimals`: Number of decimal places for the token /// - `max_supply`: Maximum supply of the token /// - `initial_supply`: Initial outstanding token supply (0 for new faucets) /// -/// # Returns -/// Returns an [`AccountComponent`] configured for agglayer faucet operations. -/// /// # Panics -/// Panics if the token symbol is invalid or metadata validation fails. -fn create_agglayer_faucet_component( +/// Panics if the token name or symbol is invalid, or if the supplies exceed the maximum amount +/// representable by a fungible asset. +fn agglayer_faucet_metadata( + token_name: &str, token_symbol: &str, decimals: u8, max_supply: Felt, initial_supply: Felt, -) -> AccountComponent { +) -> FungibleFaucet { + let name = TokenName::new(token_name).expect("token name should be valid"); let symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); - AggLayerFaucet::new(symbol, decimals, max_supply, initial_supply) + let max_supply = AssetAmount::try_from(max_supply).expect("max supply should be valid"); + let initial_supply = + AssetAmount::try_from(initial_supply).expect("initial supply should be valid"); + + FungibleFaucet::builder() + .name(name) + .symbol(symbol) + .decimals(decimals) + .max_supply(max_supply) + .token_supply(initial_supply) + .build() .expect("agglayer faucet metadata should be valid") - .into() } impl AggLayerBridge { @@ -196,64 +238,68 @@ impl AggLayerBridge { } } -impl AggLayerFaucet { - /// Returns an [`AccountBuilder`] for a faucet account with the specified deployment - /// configuration. - /// - /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; - /// `fee_manager` is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its - /// [`Ownable2Step`] owner. `fee_policy` must contain entries for - /// [`AggLayerFaucet::allowed_notes`], denominated in the asset issued by `fee_faucet_id`. - /// - /// # Panics - /// - /// Panics if the token metadata is invalid. - #[allow(clippy::too_many_arguments)] - pub fn account_builder( - seed: Word, - token_symbol: &str, - decimals: u8, - max_supply: Felt, - initial_supply: Felt, - faucet_admin: AccountId, - fee_manager: AccountId, - bridge_account_id: AccountId, - fee_faucet_id: AccountId, - fee_policy: BasicConstantFeePolicy, - ) -> AccountBuilder { - let fee_policy_manager = FeePolicyManager::builder() - .fee_faucet_id(fee_faucet_id) - .active_fee_policy(fee_policy.into()) - .build(); - let agglayer_component = - create_agglayer_faucet_component(token_symbol, decimals, max_supply, initial_supply); +/// Returns an [`AccountBuilder`] for an agglayer faucet account with the specified deployment +/// configuration. +/// +/// The account is a standard [`FungibleFaucet`] carrying no AggLayer-specific component: +/// `mint_and_send` and `receive_and_burn` drive bridge-in and bridge-out, and the standard +/// metadata getters expose the token name, symbol and decimals so the bridge can recompute the +/// AggLayer metadata hash from faucet storage. +/// +/// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` is the +/// initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] owner, which +/// is what the `owner_only` mint and burn policies gate on. `fee_policy` must contain entries for +/// [`agglayer_faucet_allowed_notes`], denominated in the asset issued by `fee_faucet_id`. +/// +/// # Panics +/// +/// Panics if the token metadata is invalid. +#[allow(clippy::too_many_arguments)] +pub fn agglayer_faucet_account_builder( + seed: Word, + token_name: &str, + token_symbol: &str, + decimals: u8, + max_supply: Felt, + initial_supply: Felt, + faucet_admin: AccountId, + fee_manager: AccountId, + bridge_account_id: AccountId, + fee_faucet_id: AccountId, + fee_policy: BasicConstantFeePolicy, +) -> AccountBuilder { + let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy.into()) + .build(); + let faucet = + agglayer_faucet_metadata(token_name, token_symbol, decimals, max_supply, initial_supply); - let token_policy_manager = TokenPolicyManager::builder() - .active_mint_policy(MintPolicy::owner_only()) - .active_burn_policy(BurnPolicy::owner_only()) - .active_send_policy(TransferPolicy::allow_all()) - .active_receive_policy(TransferPolicy::allow_all()) - .build(); + let token_policy_manager = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::owner_only()) + .active_burn_policy(BurnPolicy::owner_only()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); - let asset_callbacks = AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()); - let rbac = RoleBasedAccessControl::builder() - .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(faucet_admin)) - .role(RoleConfig::new(AggLayerFaucet::fee_manager_role()).with_member(fee_manager)) - .build() - .expect("the faucet seeds non-empty roles administered by ADMIN"); + let asset_callbacks = AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()); + let rbac = RoleBasedAccessControl::builder() + .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(faucet_admin)) + .role(RoleConfig::new(agglayer_faucet_fee_manager_role()).with_member(fee_manager)) + .build() + .expect("the faucet seeds non-empty roles administered by ADMIN"); - NetworkAccount::builder(seed.into(), AggLayerFaucet::allowed_notes(), fee_policy_manager) - .expect("faucet note allowlist is non-empty") - .with_asset_callbacks(asset_callbacks) - .with_component(agglayer_component) - .with_component(Ownable2Step::new(bridge_account_id)) - .with_component(rbac) - .with_component(Authority::RbacControlled { - procedure_roles: AggLayerFaucet::procedure_roles(), - }) - .with_components(token_policy_manager) - .with_component(ConstantFeeManager::for_basic_constant_fee_policy()) - } + NetworkAccount::builder(seed.into(), agglayer_faucet_allowed_notes(), fee_policy_manager) + .expect("faucet note allowlist is non-empty") + .with_asset_callbacks(asset_callbacks) + .with_component(faucet) + .with_component(Ownable2Step::new(bridge_account_id)) + .with_component(rbac) + .with_component(Authority::RbacControlled { + procedure_roles: agglayer_faucet_procedure_roles(), + }) + .with_components(token_policy_manager) + .with_component(ConstantFeeManager::for_basic_constant_fee_policy()) } // TESTS @@ -280,6 +326,7 @@ mod tests { create_existing_bridge_account_with_roles(Word::default(), id, id, id, id, id, id, 77); let faucet = create_existing_agglayer_faucet( Word::default(), + "AggLayer Token", "AGG", 6, Felt::from(1000u32), diff --git a/crates/miden-agglayer/src/testing/mod.rs b/crates/miden-agglayer/src/testing/mod.rs index dbd38d7c1a..e4be903354 100644 --- a/crates/miden-agglayer/src/testing/mod.rs +++ b/crates/miden-agglayer/src/testing/mod.rs @@ -23,13 +23,14 @@ use serde::Deserialize; use crate::claim_note::{ProofData, SmtNode}; use crate::{ AggLayerBridge, - AggLayerFaucet, BridgeRoles, CgiChainHash, ExitRoot, GlobalIndex, LeafData, MetadataHash, + agglayer_faucet_account_builder, + agglayer_faucet_allowed_notes, }; // ACCOUNT HELPERS @@ -97,6 +98,7 @@ pub fn create_existing_bridge_account_with_roles( /// Creates an existing AggLayer faucet account with a zero-fee policy. pub fn create_existing_agglayer_faucet( seed: Word, + token_name: &str, token_symbol: &str, decimals: u8, max_supply: Felt, @@ -105,8 +107,9 @@ pub fn create_existing_agglayer_faucet( bridge_account_id: AccountId, ) -> Account { let faucet_admin = bridge_admin_account_id(); - AggLayerFaucet::account_builder( + agglayer_faucet_account_builder( seed, + token_name, token_symbol, decimals, max_supply, @@ -115,7 +118,7 @@ pub fn create_existing_agglayer_faucet( fee_manager, bridge_account_id, fee_faucet_id(), - zero_fee_policy(AggLayerFaucet::allowed_notes()), + zero_fee_policy(agglayer_faucet_allowed_notes()), ) .build_existing() .expect("agglayer faucet account should be valid") diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 8282331960..ea0e630ae5 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -183,6 +183,7 @@ async fn test_bridge_in_claim_to_p2id( // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) // Use the origin token address and network from the claim data. // -------------------------------------------------------------------------------------------- + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -194,6 +195,7 @@ async fn test_bridge_in_claim_to_p2id( let agglayer_faucet = priced_faucet_builder( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -518,7 +520,9 @@ async fn test_mint_cannot_be_consumed_by_unrelated_faucet() -> anyhow::Result<() let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let token_name_a = "AggLayer Token A"; let token_symbol_a = "AGGA"; + let token_name_b = "AggLayer Token B"; let token_symbol_b = "AGGB"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -528,6 +532,7 @@ async fn test_mint_cannot_be_consumed_by_unrelated_faucet() -> anyhow::Result<() let faucet_a_seed = builder.rng_mut().draw_word(); let faucet_a = create_existing_agglayer_faucet( faucet_a_seed, + token_name_a, token_symbol_a, decimals, max_supply, @@ -546,6 +551,7 @@ async fn test_mint_cannot_be_consumed_by_unrelated_faucet() -> anyhow::Result<() let other_token_address = EthAddress::new(other_bytes); let faucet_b = create_existing_agglayer_faucet( faucet_b_seed, + token_name_b, token_symbol_b, decimals, max_supply, @@ -736,6 +742,7 @@ async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) // Use the origin token address and network from the claim data. // -------------------------------------------------------------------------------------------- + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -747,6 +754,7 @@ async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { let metadata_hash = leaf_data.metadata_hash; let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -882,6 +890,7 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -893,6 +902,7 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -1052,6 +1062,7 @@ async fn test_claim_rejects_removed_ger() -> anyhow::Result<()> { let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -1063,6 +1074,7 @@ async fn test_claim_rejects_removed_ger() -> anyhow::Result<()> { let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -1755,6 +1767,7 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -1774,6 +1787,7 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, @@ -1903,6 +1917,7 @@ async fn test_reregister_clears_prior_token_key() -> anyhow::Result<()> { let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -1918,6 +1933,7 @@ async fn test_reregister_clears_prior_token_key() -> anyhow::Result<()> { let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, + token_name, token_symbol, decimals, max_supply, diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 442274c1d9..66f7d0dd22 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -141,14 +141,10 @@ async fn bridge_out_consecutive( .expect("valid shared origin token address"); let origin_network = 64u32; let scale = 0u8; - let metadata_hash = MetadataHash::from_token_info( - &vectors.token_name, - &vectors.token_symbol, - vectors.token_decimals, - ); let faucet_seed = builder.rng_mut().draw_word(); let faucet = priced_faucet_builder( faucet_seed, + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, FungibleAsset::MAX_AMOUNT.into(), @@ -157,6 +153,8 @@ async fn bridge_out_consecutive( verification_base_fee, )? .build_existing()?; + // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. + let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); builder.add_account(faucet.clone())?; // CONFIG_AGG_BRIDGE note to register the faucet in the bridge (sent by faucet manager) @@ -465,13 +463,9 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho .expect("valid shared origin token address"); let origin_network = 64u32; let scale = 0u8; - let metadata_hash = MetadataHash::from_token_info( - &vectors.token_name, - &vectors.token_symbol, - vectors.token_decimals, - ); let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, Felt::from(FungibleAsset::MAX_AMOUNT), @@ -479,6 +473,8 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho bridge_admin_account_id(), bridge_account.id(), ); + // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. + let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); builder.add_account(faucet.clone())?; let config_note = ConfigAggBridgeNote::create( @@ -602,6 +598,7 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> let vectors = &*SOLIDITY_MTF_VECTORS; let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, FungibleAsset::MAX_AMOUNT.into(), @@ -708,13 +705,9 @@ async fn test_bridge_out_rejects_invalid_b2agg_note( let origin_token_address = EthAddress::from_hex(&vectors.origin_token_address).expect("valid origin token address"); let origin_network = 64u32; - let metadata_hash = MetadataHash::from_token_info( - &vectors.token_name, - &vectors.token_symbol, - vectors.token_decimals, - ); let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + &vectors.token_name, &vectors.token_symbol, vectors.token_decimals, FungibleAsset::MAX_AMOUNT.into(), @@ -722,6 +715,8 @@ async fn test_bridge_out_rejects_invalid_b2agg_note( bridge_admin_account_id(), bridge_account.id(), ); + // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. + let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index 0951c1ba1b..cfd10d3db9 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -1,11 +1,13 @@ extern crate alloc; -use miden_agglayer::AggLayerFaucet; +use miden_agglayer::MetadataHash; use miden_agglayer::testing::create_existing_agglayer_faucet; use miden_protocol::Felt; use miden_protocol::account::auth::AuthScheme; -use miden_protocol::asset::FungibleAsset; +use miden_protocol::asset::{AssetAmount, FungibleAsset}; use miden_protocol::crypto::rand::FeltRng; +use miden_standards::account::access::Ownable2Step; +use miden_standards::account::faucets::FungibleFaucet; use miden_testing::{Auth, MockChain}; use super::test_utils::{ @@ -14,8 +16,13 @@ use super::test_utils::{ create_existing_bridge_account_with_roles, }; +/// An agglayer faucet is a standard [`FungibleFaucet`] owned by the bridge. +/// +/// This pins the two properties the bridge depends on: the faucet exposes the standard fungible +/// faucet interface with its token metadata intact - including the real token *name*, which the +/// AggLayer metadata hash is computed over - and its `Ownable2Step` owner is the bridge account. #[test] -fn test_faucet_helper_methods() -> anyhow::Result<()> { +fn agglayer_faucet_is_a_bridge_owned_fungible_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); let faucet_manager = builder.add_existing_wallet(Auth::BasicAuth { @@ -40,6 +47,7 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { ); builder.add_account(bridge_account.clone())?; + let token_name = "AggLayer Token"; let token_symbol = "AGG"; let decimals = 8u8; let max_supply: Felt = FungibleAsset::MAX_AMOUNT.into(); @@ -47,6 +55,7 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + token_name, token_symbol, decimals, max_supply, @@ -55,7 +64,28 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { bridge_account.id(), ); - assert_eq!(AggLayerFaucet::owner_account_id(&faucet)?, bridge_account.id()); + // The account carries the standard fungible faucet interface, so `try_from` (which checks the + // procedure roots before decoding storage) succeeds. + let metadata = FungibleFaucet::try_from(&faucet)?; + + // Every field round-trips, most importantly the token name: it used to be derived from the + // symbol, which made the on-chain metadata hash unverifiable (issue #2585). + assert_eq!(metadata.token_name().as_str(), token_name); + assert_eq!(metadata.symbol().to_string(), token_symbol); + assert_eq!(metadata.decimals(), decimals); + assert_eq!(metadata.max_supply(), AssetAmount::try_from(max_supply)?); + assert_eq!(metadata.token_supply(), AssetAmount::try_from(token_supply)?); + + // The metadata hash the bridge registers is reproducible from faucet storage alone. This is + // the invariant issue #2586 moves on-chain. + assert_eq!( + MetadataHash::from_fungible_faucet(&metadata), + MetadataHash::from_token_info(token_name, token_symbol, decimals), + ); + + // Mint and burn authorization is bound to the bridge through `Ownable2Step`. + let ownership = Ownable2Step::try_from_storage(faucet.storage())?; + assert_eq!(ownership.owner(), Some(bridge_account.id())); Ok(()) } diff --git a/crates/miden-testing/tests/agglayer/fee_policy.rs b/crates/miden-testing/tests/agglayer/fee_policy.rs index bdcfedfb63..3f88d70406 100644 --- a/crates/miden-testing/tests/agglayer/fee_policy.rs +++ b/crates/miden-testing/tests/agglayer/fee_policy.rs @@ -1,7 +1,14 @@ use alloc::collections::BTreeSet; use miden_agglayer::testing::bridge_admin_account_id; -use miden_agglayer::{AggLayerBridge, AggLayerFaucet, BridgeRoles}; +use miden_agglayer::{ + AggLayerBridge, + BridgeRoles, + agglayer_faucet_account_builder, + agglayer_faucet_allowed_notes, + agglayer_faucet_fee_manager_role, + agglayer_faucet_procedure_roles, +}; use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, StorageMapKey}; use miden_protocol::asset::{AssetId, FungibleAsset}; use miden_protocol::note::{Note, NoteScriptRoot}; @@ -73,7 +80,7 @@ fn agglayer_accounts_install_priced_basic_constant_fee_policies() -> anyhow::Res )?; assert_priced_account( &build_managed_account(ManagedAccount::Faucet)?, - AggLayerFaucet::allowed_notes(), + agglayer_faucet_allowed_notes(), ) } @@ -87,13 +94,13 @@ fn faucet_allowed_notes_pin() { NetworkAccountConfigNote::script_root(), FeeSponsorshipNote::script_root(), ]); - assert_eq!(AggLayerFaucet::allowed_notes(), expected); + assert_eq!(agglayer_faucet_allowed_notes(), expected); } #[test] fn fee_management_procedure_role_mappings() { let bridge_roles = AggLayerBridge::procedure_roles(); - let faucet_roles = AggLayerFaucet::procedure_roles(); + let faucet_roles = agglayer_faucet_procedure_roles(); assert_eq!( bridge_roles.get(&ConstantFeeManager::set_note_fee_root()), @@ -101,7 +108,7 @@ fn fee_management_procedure_role_mappings() { ); assert_eq!( faucet_roles.get(&ConstantFeeManager::set_note_fee_root()), - Some(&AggLayerFaucet::fee_manager_role()), + Some(&agglayer_faucet_fee_manager_role()), ); assert_eq!(faucet_roles.len(), 1, "only note repricing uses the faucet FEE_MNGR role"); @@ -156,8 +163,9 @@ fn build_managed_account(managed: ManagedAccount) -> anyhow::Result { Ok(match managed { ManagedAccount::Bridge => bridge, - ManagedAccount::Faucet => AggLayerFaucet::account_builder( + ManagedAccount::Faucet => agglayer_faucet_account_builder( Word::from([1u32, 0, 0, 0]), + "AggLayer Token", "AGG", 6, 1_000u32.into(), @@ -166,7 +174,7 @@ fn build_managed_account(managed: ManagedAccount) -> anyhow::Result { fee_manager_id(), bridge.id(), pricer.fee_parameters().fee_faucet_id(), - pricer.basic_constant_fee_policy(AggLayerFaucet::allowed_notes())?, + pricer.basic_constant_fee_policy(agglayer_faucet_allowed_notes())?, ) .build_existing()?, }) diff --git a/crates/miden-testing/tests/agglayer/network_account_regression.rs b/crates/miden-testing/tests/agglayer/network_account_regression.rs index c230f617e6..694f71bc67 100644 --- a/crates/miden-testing/tests/agglayer/network_account_regression.rs +++ b/crates/miden-testing/tests/agglayer/network_account_regression.rs @@ -5,7 +5,7 @@ //! 1. The account rejects any transaction that executes a tx script. //! 2. The account rejects any input note whose script root is not in its //! [`allowed_notes`](miden_agglayer::AggLayerBridge::allowed_notes) / -//! [`allowed_notes`](miden_agglayer::AggLayerFaucet::allowed_notes) set. +//! [`agglayer_faucet_allowed_notes`](miden_agglayer::agglayer_faucet_allowed_notes) set. //! //! [`AuthNetworkAccount`]: miden_standards::account::auth::AuthNetworkAccount @@ -165,6 +165,7 @@ async fn faucet_rejects_tx_script() -> anyhow::Result<()> { let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "Test Token", "TEST", 8, Felt::new(1_000_000).unwrap(), @@ -207,6 +208,7 @@ async fn faucet_rejects_non_allowlisted_input_note() -> anyhow::Result<()> { let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "Test Token", "TEST", 8, Felt::new(1_000_000).unwrap(), diff --git a/crates/miden-testing/tests/agglayer/rbac_rotation.rs b/crates/miden-testing/tests/agglayer/rbac_rotation.rs index 75a3da4707..c39eb4b370 100644 --- a/crates/miden-testing/tests/agglayer/rbac_rotation.rs +++ b/crates/miden-testing/tests/agglayer/rbac_rotation.rs @@ -93,6 +93,7 @@ async fn faucet_accepts_rbac_config_note() -> anyhow::Result<()> { })?; let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), + "AggLayer Token", "AGG", 6, Felt::from(1_000u32), diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index db56d0abcb..3736f1c817 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -11,7 +11,13 @@ pub use miden_agglayer::testing::{ bridge_admin_account_id, create_existing_bridge_account_with_roles, }; -use miden_agglayer::{AggLayerBridge, AggLayerFaucet, BridgeRoles, agglayer_package}; +use miden_agglayer::{ + AggLayerBridge, + BridgeRoles, + agglayer_faucet_account_builder, + agglayer_faucet_allowed_notes, + agglayer_package, +}; use miden_core_lib::CoreLibrary; use miden_crypto::hash::keccak::Keccak256; use miden_processor::advice::AdviceInputs; @@ -172,6 +178,7 @@ pub fn create_existing_priced_bridge( pub fn priced_faucet_builder( seed: Word, + token_name: &str, token_symbol: &str, decimals: u8, max_supply: Felt, @@ -180,10 +187,11 @@ pub fn priced_faucet_builder( verification_base_fee: u32, ) -> anyhow::Result { let pricer = network_note_pricer(verification_base_fee); - let fee_policy = pricer.basic_constant_fee_policy(AggLayerFaucet::allowed_notes())?; + let fee_policy = pricer.basic_constant_fee_policy(agglayer_faucet_allowed_notes())?; let faucet_admin = bridge_admin_account_id(); - Ok(AggLayerFaucet::account_builder( + Ok(agglayer_faucet_account_builder( seed, + token_name, token_symbol, decimals, max_supply, From bab8326a9f8163eee26ef4ee7d50f5b4738e29e2 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Wed, 26 Aug 2026 16:43:33 +0300 Subject: [PATCH 02/11] fix: address review comments - Keep `AggLayerFaucet` as a stateless namespace instead of moving its associated functions out to free functions in `lib.rs`. The type no longer wraps a `FungibleFaucet` or validates accounts, but `allowed_notes`, `procedure_roles`, `fee_manager_role` and `account_builder` stay grouped under it, mirroring `AggLayerBridge`. - Rename `agglayer_faucet_metadata` to `build_fungible_faucet`: it returns the `FungibleFaucet` component, not a metadata value. - Drop `test_metadata_hash_from_fungible_faucet_matches_token_info`, which compared `from_fungible_faucet` against the `from_token_info` it forwards to. The same redundant assertion is removed from `agglayer_faucet_is_a_bridge_owned_fungible_faucet`, where the field-by-field checks above it already cover the round-trip. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- .../src/eth_types/metadata_hash.rs | 19 -- crates/miden-agglayer/src/faucet.rs | 69 +++++++ crates/miden-agglayer/src/lib.rs | 179 +++++++----------- crates/miden-agglayer/src/testing/mod.rs | 7 +- .../tests/agglayer/faucet_helpers.rs | 11 +- .../tests/agglayer/fee_policy.rs | 21 +- .../agglayer/network_account_regression.rs | 2 +- .../tests/agglayer/test_utils.rs | 12 +- 9 files changed, 157 insertions(+), 165 deletions(-) create mode 100644 crates/miden-agglayer/src/faucet.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 787cbf8d61..98858e46d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ - [BREAKING] Narrowed the block header version field from 32 to 8 bits and set it to the only supported version instead of taking it as a `BlockHeader::new` parameter ([#3695](https://github.com/0xMiden/protocol/pull/3695)). - [BREAKING] Serialize the version in `Account`, `AccountHeader`, `PartialNoteMetadata` and `AssetId` ([#3697](https://github.com/0xMiden/protocol/pull/3697)). - [BREAKING] Moved the account delta and patch domain separators into the hasher capacity word. Added a version to their commitments ([#3698](https://github.com/0xMiden/protocol/pull/3698)). -- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`: the `AggLayerFaucet` / `AgglayerFaucetError` types and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` / `AggLayerFaucet::allowed_notes` became the free functions `agglayer_faucet_account_builder` / `agglayer_faucet_allowed_notes`, and both they and `create_existing_agglayer_faucet` now take a token name. The faucet stores the real token name instead of a copy of its symbol, so the AggLayer metadata hash preimage `abi.encode(name, symbol, decimals)` is recoverable from faucet storage; the faucet code commitment changes ([#2585](https://github.com/0xMiden/protocol/issues/2585)). +- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`, so the faucet stores the real token name instead of a copy of its symbol and the metadata hash preimage `abi.encode(name, symbol, decimals)` is recoverable from its storage. `AggLayerFaucet` is now a stateless namespace, `AgglayerFaucetError` and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` and `create_existing_agglayer_faucet` take a token name, and the faucet code commitment changes ([#2585](https://github.com/0xMiden/protocol/issues/2585)). ### Fixes diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index f28536bdd9..528aa7d5ca 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -138,25 +138,6 @@ mod tests { assert_eq!(hash, hash_from_info, "from_abi_encoded and from_token_info must agree"); } - /// A faucet's own metadata yields the same hash as passing its name, symbol and decimals - /// individually. This is the invariant that lets the bridge recompute the registered hash - /// from faucet storage (see issue #2586). - #[test] - fn test_metadata_hash_from_fungible_faucet_matches_token_info() { - let faucet = crate::agglayer_faucet_metadata( - "Test Token", - "TEST", - 12, - Felt::from(1000u32), - Felt::ZERO, - ); - - assert_eq!( - MetadataHash::from_fungible_faucet(&faucet), - MetadataHash::from_token_info("Test Token", "TEST", 12), - ); - } - fn hex_to_vec(hex: &str) -> std::vec::Vec { (0..hex.len()) .step_by(2) diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs new file mode 100644 index 0000000000..1350c5a281 --- /dev/null +++ b/crates/miden-agglayer/src/faucet.rs @@ -0,0 +1,69 @@ +extern crate alloc; + +use alloc::collections::{BTreeMap, BTreeSet}; + +use miden_protocol::account::{AccountProcedureRoot, RoleSymbol}; +use miden_protocol::note::NoteScriptRoot; +use miden_standards::account::auth::AuthNetworkAccount; +use miden_standards::account::fees::ConstantFeeManager; +use miden_standards::note::{BurnNote, ConstantFeePolicyConfigNote, MintNote, RbacConfigNote}; +use miden_utils_sync::LazyLock; + +// FAUCET RBAC ROLES +// ================================================================================================ + +static FEE_MANAGER_ROLE: LazyLock = + LazyLock::new(|| RoleSymbol::new("FEE_MNGR").expect("FEE_MNGR role symbol should be valid")); + +// AGGLAYER FAUCET +// ================================================================================================ + +/// The deployment configuration of an AggLayer faucet. +/// +/// An AggLayer faucet is a standard +/// [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) owned by the bridge; it +/// carries no AggLayer-specific account component, so this type holds no state and exists only to +/// namespace the deployment parameters the bridge and the faucet operator have to agree on: the +/// note allowlist, the RBAC roles, and the account builder +/// ([`AggLayerFaucet::account_builder`](crate::AggLayerFaucet::account_builder), defined alongside +/// the bridge's). +/// +/// To read the token metadata of a deployed faucet, decode the account with +/// [`FungibleFaucet::try_from`](miden_standards::account::faucets::FungibleFaucet); to read its +/// owner, use [`Ownable2Step`](miden_standards::account::access::Ownable2Step). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AggLayerFaucet; + +impl AggLayerFaucet { + // RBAC ROLES + // -------------------------------------------------------------------------------------------- + + /// Returns the `FEE_MNGR` role symbol. Holders may update the faucet's note fee schedule. + pub fn fee_manager_role() -> RoleSymbol { + FEE_MANAGER_ROLE.clone() + } + + /// Returns the fixed procedure-to-role map used to configure the faucet's `Authority` + /// (`RbacControlled`) component. + pub fn procedure_roles() -> BTreeMap { + BTreeMap::from([(ConstantFeeManager::set_note_fee_root(), Self::fee_manager_role())]) + } + + // ALLOWED NOTES + // -------------------------------------------------------------------------------------------- + + /// Returns the input-note script roots allowlisted on a newly deployed AggLayer faucet. + /// + /// A live account's allowlist is available through + /// [`NetworkAccount::allowed_notes`](miden_standards::account::auth::NetworkAccount::allowed_notes). + pub fn allowed_notes() -> BTreeSet { + let mut notes = BTreeSet::from([ + MintNote::script_root(), + BurnNote::script_root(), + ConstantFeePolicyConfigNote::script_root(), + RbacConfigNote::script_root(), + ]); + notes.extend(AuthNetworkAccount::default_allowed_note_scripts()); + notes + } +} diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 8f12efc72b..8933e43cad 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -2,19 +2,11 @@ extern crate alloc; -use alloc::collections::{BTreeMap, BTreeSet}; - use miden_core::{Felt, Word}; -use miden_protocol::account::{ - AccountBuilder, - AccountId, - AccountProcedureRoot, - AssetCallbackFlag, - RoleSymbol, -}; +use miden_protocol::account::{AccountBuilder, AccountId, AssetCallbackFlag}; use miden_protocol::assembly::Path; use miden_protocol::asset::{AssetAmount, TokenSymbol}; -use miden_protocol::note::{NoteScript, NoteScriptRoot}; +use miden_protocol::note::NoteScript; use miden_protocol::vm::Package; use miden_standards::account::access::{ Authority, @@ -24,7 +16,7 @@ use miden_standards::account::access::{ RoleBasedAccessControl, RoleConfig, }; -use miden_standards::account::auth::{AuthNetworkAccount, NetworkAccount}; +use miden_standards::account::auth::NetworkAccount; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::fees::{ BasicConstantFeePolicy, @@ -37,7 +29,6 @@ use miden_standards::account::policies::{ TokenPolicyManager, TransferPolicy, }; -use miden_standards::note::{BurnNote, ConstantFeePolicyConfigNote, MintNote, RbacConfigNote}; use miden_utils_sync::LazyLock; pub mod agglayer_note; @@ -49,6 +40,7 @@ pub mod costs; pub mod deregister_note; pub mod errors; pub mod eth_types; +pub mod faucet; mod ger_note; pub mod remove_ger_note; #[cfg(any(feature = "testing", test))] @@ -74,6 +66,7 @@ pub use deregister_note::DeregisterAggFaucetNote; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{GlobalIndex, GlobalIndexError, MetadataHash}; +pub use faucet::AggLayerFaucet; pub use remove_ger_note::RemoveGerNote; pub use update_ger_note::UpdateGerNote; pub use utils::Keccak256Output; @@ -121,49 +114,16 @@ fn agglayer_bridge_component_package() -> Package { BRIDGE_COMPONENT_PACKAGE.clone() } -// AGGLAYER FAUCET RBAC ROLES -// ================================================================================================ - -static FAUCET_FEE_MANAGER_ROLE: LazyLock = - LazyLock::new(|| RoleSymbol::new("FEE_MNGR").expect("FEE_MNGR role symbol should be valid")); - -/// Returns the AggLayer faucet's `FEE_MNGR` role symbol. Holders may update the faucet's note fee -/// schedule. -pub fn agglayer_faucet_fee_manager_role() -> RoleSymbol { - FAUCET_FEE_MANAGER_ROLE.clone() -} - -/// Returns the fixed procedure-to-role map used to configure the AggLayer faucet's [`Authority`] -/// (`RbacControlled`) component. -pub fn agglayer_faucet_procedure_roles() -> BTreeMap { - BTreeMap::from([(ConstantFeeManager::set_note_fee_root(), agglayer_faucet_fee_manager_role())]) -} - // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ -/// Returns the input-note script roots allowlisted on a newly deployed AggLayer faucet. -/// -/// A live account's allowlist is available through -/// [`NetworkAccount::allowed_notes`](miden_standards::account::auth::NetworkAccount::allowed_notes). -pub fn agglayer_faucet_allowed_notes() -> BTreeSet { - let mut notes = BTreeSet::from([ - MintNote::script_root(), - BurnNote::script_root(), - ConstantFeePolicyConfigNote::script_root(), - RbacConfigNote::script_root(), - ]); - notes.extend(AuthNetworkAccount::default_allowed_note_scripts()); - notes -} - -/// Builds the [`FungibleFaucet`] component of an agglayer faucet account. +/// Builds the [`FungibleFaucet`] component of an agglayer faucet account from its token metadata. /// /// The faucet holds only token metadata; conversion metadata (origin address, origin network, /// scale, metadata hash) lives on the bridge and is populated at registration time. /// -/// The token name is stored alongside the symbol and decimals so that the bridge can recompute -/// `keccak256(abi.encode(name, symbol, decimals))` from the faucet's own storage. +/// The token name is stored alongside the symbol and decimals, which makes the metadata hash +/// preimage `abi.encode(name, symbol, decimals)` recoverable from the faucet's own storage. /// /// # Parameters /// - `token_name`: The display name for the fungible token (e.g., "AggLayer Token") @@ -175,7 +135,7 @@ pub fn agglayer_faucet_allowed_notes() -> BTreeSet { /// # Panics /// Panics if the token name or symbol is invalid, or if the supplies exceed the maximum amount /// representable by a fungible asset. -fn agglayer_faucet_metadata( +fn build_fungible_faucet( token_name: &str, token_symbol: &str, decimals: u8, @@ -238,68 +198,71 @@ impl AggLayerBridge { } } -/// Returns an [`AccountBuilder`] for an agglayer faucet account with the specified deployment -/// configuration. -/// -/// The account is a standard [`FungibleFaucet`] carrying no AggLayer-specific component: -/// `mint_and_send` and `receive_and_burn` drive bridge-in and bridge-out, and the standard -/// metadata getters expose the token name, symbol and decimals so the bridge can recompute the -/// AggLayer metadata hash from faucet storage. -/// -/// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` is the -/// initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] owner, which -/// is what the `owner_only` mint and burn policies gate on. `fee_policy` must contain entries for -/// [`agglayer_faucet_allowed_notes`], denominated in the asset issued by `fee_faucet_id`. -/// -/// # Panics -/// -/// Panics if the token metadata is invalid. -#[allow(clippy::too_many_arguments)] -pub fn agglayer_faucet_account_builder( - seed: Word, - token_name: &str, - token_symbol: &str, - decimals: u8, - max_supply: Felt, - initial_supply: Felt, - faucet_admin: AccountId, - fee_manager: AccountId, - bridge_account_id: AccountId, - fee_faucet_id: AccountId, - fee_policy: BasicConstantFeePolicy, -) -> AccountBuilder { - let fee_policy_manager = FeePolicyManager::builder() - .fee_faucet_id(fee_faucet_id) - .active_fee_policy(fee_policy.into()) - .build(); - let faucet = - agglayer_faucet_metadata(token_name, token_symbol, decimals, max_supply, initial_supply); +impl AggLayerFaucet { + /// Returns an [`AccountBuilder`] for a faucet account with the specified deployment + /// configuration. + /// + /// The account is a standard [`FungibleFaucet`] carrying no AggLayer-specific component: + /// `mint_and_send` and `receive_and_burn` drive bridge-in and bridge-out, and the standard + /// metadata getters expose the token name, symbol and decimals that make up the AggLayer + /// metadata hash preimage. + /// + /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` + /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] + /// owner, which is what the `owner_only` mint and burn policies gate on. `fee_policy` must + /// contain entries for [`AggLayerFaucet::allowed_notes`], denominated in the asset issued by + /// `fee_faucet_id`. + /// + /// # Panics + /// + /// Panics if the token metadata is invalid. + #[allow(clippy::too_many_arguments)] + pub fn account_builder( + seed: Word, + token_name: &str, + token_symbol: &str, + decimals: u8, + max_supply: Felt, + initial_supply: Felt, + faucet_admin: AccountId, + fee_manager: AccountId, + bridge_account_id: AccountId, + fee_faucet_id: AccountId, + fee_policy: BasicConstantFeePolicy, + ) -> AccountBuilder { + let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy.into()) + .build(); + let faucet = + build_fungible_faucet(token_name, token_symbol, decimals, max_supply, initial_supply); - let token_policy_manager = TokenPolicyManager::builder() - .active_mint_policy(MintPolicy::owner_only()) - .active_burn_policy(BurnPolicy::owner_only()) - .active_send_policy(TransferPolicy::allow_all()) - .active_receive_policy(TransferPolicy::allow_all()) - .build(); + let token_policy_manager = TokenPolicyManager::builder() + .active_mint_policy(MintPolicy::owner_only()) + .active_burn_policy(BurnPolicy::owner_only()) + .active_send_policy(TransferPolicy::allow_all()) + .active_receive_policy(TransferPolicy::allow_all()) + .build(); - let asset_callbacks = AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()); - let rbac = RoleBasedAccessControl::builder() - .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(faucet_admin)) - .role(RoleConfig::new(agglayer_faucet_fee_manager_role()).with_member(fee_manager)) - .build() - .expect("the faucet seeds non-empty roles administered by ADMIN"); + let asset_callbacks = AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()); + let rbac = RoleBasedAccessControl::builder() + .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(faucet_admin)) + .role(RoleConfig::new(AggLayerFaucet::fee_manager_role()).with_member(fee_manager)) + .build() + .expect("the faucet seeds non-empty roles administered by ADMIN"); - NetworkAccount::builder(seed.into(), agglayer_faucet_allowed_notes(), fee_policy_manager) - .expect("faucet note allowlist is non-empty") - .with_asset_callbacks(asset_callbacks) - .with_component(faucet) - .with_component(Ownable2Step::new(bridge_account_id)) - .with_component(rbac) - .with_component(Authority::RbacControlled { - procedure_roles: agglayer_faucet_procedure_roles(), - }) - .with_components(token_policy_manager) - .with_component(ConstantFeeManager::for_basic_constant_fee_policy()) + NetworkAccount::builder(seed.into(), AggLayerFaucet::allowed_notes(), fee_policy_manager) + .expect("faucet note allowlist is non-empty") + .with_asset_callbacks(asset_callbacks) + .with_component(faucet) + .with_component(Ownable2Step::new(bridge_account_id)) + .with_component(rbac) + .with_component(Authority::RbacControlled { + procedure_roles: AggLayerFaucet::procedure_roles(), + }) + .with_components(token_policy_manager) + .with_component(ConstantFeeManager::for_basic_constant_fee_policy()) + } } // TESTS diff --git a/crates/miden-agglayer/src/testing/mod.rs b/crates/miden-agglayer/src/testing/mod.rs index e4be903354..c138da7e55 100644 --- a/crates/miden-agglayer/src/testing/mod.rs +++ b/crates/miden-agglayer/src/testing/mod.rs @@ -23,14 +23,13 @@ use serde::Deserialize; use crate::claim_note::{ProofData, SmtNode}; use crate::{ AggLayerBridge, + AggLayerFaucet, BridgeRoles, CgiChainHash, ExitRoot, GlobalIndex, LeafData, MetadataHash, - agglayer_faucet_account_builder, - agglayer_faucet_allowed_notes, }; // ACCOUNT HELPERS @@ -107,7 +106,7 @@ pub fn create_existing_agglayer_faucet( bridge_account_id: AccountId, ) -> Account { let faucet_admin = bridge_admin_account_id(); - agglayer_faucet_account_builder( + AggLayerFaucet::account_builder( seed, token_name, token_symbol, @@ -118,7 +117,7 @@ pub fn create_existing_agglayer_faucet( fee_manager, bridge_account_id, fee_faucet_id(), - zero_fee_policy(agglayer_faucet_allowed_notes()), + zero_fee_policy(AggLayerFaucet::allowed_notes()), ) .build_existing() .expect("agglayer faucet account should be valid") diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index cfd10d3db9..f44519550e 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -1,6 +1,5 @@ extern crate alloc; -use miden_agglayer::MetadataHash; use miden_agglayer::testing::create_existing_agglayer_faucet; use miden_protocol::Felt; use miden_protocol::account::auth::AuthScheme; @@ -69,20 +68,14 @@ fn agglayer_faucet_is_a_bridge_owned_fungible_faucet() -> anyhow::Result<()> { let metadata = FungibleFaucet::try_from(&faucet)?; // Every field round-trips, most importantly the token name: it used to be derived from the - // symbol, which made the on-chain metadata hash unverifiable (issue #2585). + // symbol, which left the metadata hash preimage `abi.encode(name, symbol, decimals)` + // unrecoverable from faucet storage, and so unverifiable on-chain (issues #2585, #2586). assert_eq!(metadata.token_name().as_str(), token_name); assert_eq!(metadata.symbol().to_string(), token_symbol); assert_eq!(metadata.decimals(), decimals); assert_eq!(metadata.max_supply(), AssetAmount::try_from(max_supply)?); assert_eq!(metadata.token_supply(), AssetAmount::try_from(token_supply)?); - // The metadata hash the bridge registers is reproducible from faucet storage alone. This is - // the invariant issue #2586 moves on-chain. - assert_eq!( - MetadataHash::from_fungible_faucet(&metadata), - MetadataHash::from_token_info(token_name, token_symbol, decimals), - ); - // Mint and burn authorization is bound to the bridge through `Ownable2Step`. let ownership = Ownable2Step::try_from_storage(faucet.storage())?; assert_eq!(ownership.owner(), Some(bridge_account.id())); diff --git a/crates/miden-testing/tests/agglayer/fee_policy.rs b/crates/miden-testing/tests/agglayer/fee_policy.rs index 3f88d70406..50b5993444 100644 --- a/crates/miden-testing/tests/agglayer/fee_policy.rs +++ b/crates/miden-testing/tests/agglayer/fee_policy.rs @@ -1,14 +1,7 @@ use alloc::collections::BTreeSet; use miden_agglayer::testing::bridge_admin_account_id; -use miden_agglayer::{ - AggLayerBridge, - BridgeRoles, - agglayer_faucet_account_builder, - agglayer_faucet_allowed_notes, - agglayer_faucet_fee_manager_role, - agglayer_faucet_procedure_roles, -}; +use miden_agglayer::{AggLayerBridge, AggLayerFaucet, BridgeRoles}; use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, StorageMapKey}; use miden_protocol::asset::{AssetId, FungibleAsset}; use miden_protocol::note::{Note, NoteScriptRoot}; @@ -80,7 +73,7 @@ fn agglayer_accounts_install_priced_basic_constant_fee_policies() -> anyhow::Res )?; assert_priced_account( &build_managed_account(ManagedAccount::Faucet)?, - agglayer_faucet_allowed_notes(), + AggLayerFaucet::allowed_notes(), ) } @@ -94,13 +87,13 @@ fn faucet_allowed_notes_pin() { NetworkAccountConfigNote::script_root(), FeeSponsorshipNote::script_root(), ]); - assert_eq!(agglayer_faucet_allowed_notes(), expected); + assert_eq!(AggLayerFaucet::allowed_notes(), expected); } #[test] fn fee_management_procedure_role_mappings() { let bridge_roles = AggLayerBridge::procedure_roles(); - let faucet_roles = agglayer_faucet_procedure_roles(); + let faucet_roles = AggLayerFaucet::procedure_roles(); assert_eq!( bridge_roles.get(&ConstantFeeManager::set_note_fee_root()), @@ -108,7 +101,7 @@ fn fee_management_procedure_role_mappings() { ); assert_eq!( faucet_roles.get(&ConstantFeeManager::set_note_fee_root()), - Some(&agglayer_faucet_fee_manager_role()), + Some(&AggLayerFaucet::fee_manager_role()), ); assert_eq!(faucet_roles.len(), 1, "only note repricing uses the faucet FEE_MNGR role"); @@ -163,7 +156,7 @@ fn build_managed_account(managed: ManagedAccount) -> anyhow::Result { Ok(match managed { ManagedAccount::Bridge => bridge, - ManagedAccount::Faucet => agglayer_faucet_account_builder( + ManagedAccount::Faucet => AggLayerFaucet::account_builder( Word::from([1u32, 0, 0, 0]), "AggLayer Token", "AGG", @@ -174,7 +167,7 @@ fn build_managed_account(managed: ManagedAccount) -> anyhow::Result { fee_manager_id(), bridge.id(), pricer.fee_parameters().fee_faucet_id(), - pricer.basic_constant_fee_policy(agglayer_faucet_allowed_notes())?, + pricer.basic_constant_fee_policy(AggLayerFaucet::allowed_notes())?, ) .build_existing()?, }) diff --git a/crates/miden-testing/tests/agglayer/network_account_regression.rs b/crates/miden-testing/tests/agglayer/network_account_regression.rs index 694f71bc67..347b88ce0f 100644 --- a/crates/miden-testing/tests/agglayer/network_account_regression.rs +++ b/crates/miden-testing/tests/agglayer/network_account_regression.rs @@ -5,7 +5,7 @@ //! 1. The account rejects any transaction that executes a tx script. //! 2. The account rejects any input note whose script root is not in its //! [`allowed_notes`](miden_agglayer::AggLayerBridge::allowed_notes) / -//! [`agglayer_faucet_allowed_notes`](miden_agglayer::agglayer_faucet_allowed_notes) set. +//! [`allowed_notes`](miden_agglayer::AggLayerFaucet::allowed_notes) set. //! //! [`AuthNetworkAccount`]: miden_standards::account::auth::AuthNetworkAccount diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 3736f1c817..90374923c2 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -11,13 +11,7 @@ pub use miden_agglayer::testing::{ bridge_admin_account_id, create_existing_bridge_account_with_roles, }; -use miden_agglayer::{ - AggLayerBridge, - BridgeRoles, - agglayer_faucet_account_builder, - agglayer_faucet_allowed_notes, - agglayer_package, -}; +use miden_agglayer::{AggLayerBridge, AggLayerFaucet, BridgeRoles, agglayer_package}; use miden_core_lib::CoreLibrary; use miden_crypto::hash::keccak::Keccak256; use miden_processor::advice::AdviceInputs; @@ -187,9 +181,9 @@ pub fn priced_faucet_builder( verification_base_fee: u32, ) -> anyhow::Result { let pricer = network_note_pricer(verification_base_fee); - let fee_policy = pricer.basic_constant_fee_policy(agglayer_faucet_allowed_notes())?; + let fee_policy = pricer.basic_constant_fee_policy(AggLayerFaucet::allowed_notes())?; let faucet_admin = bridge_admin_account_id(); - Ok(agglayer_faucet_account_builder( + Ok(AggLayerFaucet::account_builder( seed, token_name, token_symbol, From b012462b68e634e523f90a3989148d5c1483e5fd Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Wed, 26 Aug 2026 18:26:47 +0300 Subject: [PATCH 03/11] docs: address review comments - Reword the `AggLayerFaucet` doc to say what it is: a stateless helper holding deployment configuration, explicitly not an `AccountComponent`. - Trim `MetadataHash::from_fungible_faucet`'s doc to the suggested line. - Drop the `generate_agglayer_constants` note explaining why the faucet has no code commitment. - Cut SPEC 3.2 down to what is distinctive about an AggLayer faucet - bridge ownership, `owner_only` mint/burn, the note allowlist and the token name - instead of enumerating standard `FungibleFaucet` procedures and accessors. The old text also claimed the allowlist was limited to MINT and BURN, which has not been true since #3632. - Shorten the CHANGELOG entry to the suggested wording and point it at this PR rather than the issue. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- crates/miden-agglayer/SPEC.md | 40 +++++++++---------- crates/miden-agglayer/build.rs | 5 --- .../src/eth_types/metadata_hash.rs | 3 +- crates/miden-agglayer/src/faucet.rs | 10 ++--- 5 files changed, 25 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98858e46d5..f30154d925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ - [BREAKING] Narrowed the block header version field from 32 to 8 bits and set it to the only supported version instead of taking it as a `BlockHeader::new` parameter ([#3695](https://github.com/0xMiden/protocol/pull/3695)). - [BREAKING] Serialize the version in `Account`, `AccountHeader`, `PartialNoteMetadata` and `AssetId` ([#3697](https://github.com/0xMiden/protocol/pull/3697)). - [BREAKING] Moved the account delta and patch domain separators into the hasher capacity word. Added a version to their commitments ([#3698](https://github.com/0xMiden/protocol/pull/3698)). -- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`, so the faucet stores the real token name instead of a copy of its symbol and the metadata hash preimage `abi.encode(name, symbol, decimals)` is recoverable from its storage. `AggLayerFaucet` is now a stateless namespace, `AgglayerFaucetError` and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` and `create_existing_agglayer_faucet` take a token name, and the faucet code commitment changes ([#2585](https://github.com/0xMiden/protocol/issues/2585)). +- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`. `AggLayerFaucet` is now a stateless namespace, `AgglayerFaucetError` and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` and `create_existing_agglayer_faucet` take a token name ([#3525](https://github.com/0xMiden/protocol/pull/3525)). ### Fixes diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 8fb945a544..9cf24ebc4f 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -478,28 +478,24 @@ Initial state: all map slots empty, all value slots `[0, 0, 0, 0]`. The initial member and the initial `FAUCET_MNGR` / `GER_INJECTOR` / `GER_REMOVER` / `FEE_MNGR` / `PAUSER` role holders are seeded into the access-control components at account creation time. -### 3.2 Faucet Account Component - -The faucet account carries no AggLayer-specific component. It is the standard `FungibleFaucet` -component (`miden-standards/asm/components/faucets/fungible_faucet/fungible_faucet.masm`), -installed on top of `Ownable2Step` + the RBAC access-control stack (`RoleBasedAccessControl` + -`Authority::RbacControlled`), so the AggLayer faucet is an ordinary network fungible faucet whose -owner happens to be the bridge. - -The procedures the bridge protocol relies on are: - -- `mint_and_send` - bridge-in, gated by the active mint policy (`owner_only`) -- `receive_and_burn` - bridge-out, gated by the active burn policy -- `has_procedure` - so the unified MINT/BURN note scripts can reflectively detect the faucet kind - -The component additionally exposes the standard token-config and metadata accessors - -`get_name`, `get_token_symbol`, `get_decimals`, `get_token_supply`, `get_max_supply`, -`get_token_config`, `is_max_supply_mutable`, `get_mutability_config`, the `is_*_mutable` views, -and the `set_max_supply` / `set_description` / `set_logo_uri` / `set_external_link` setters. The -getters are what allow the bridge to recompute `keccak256(abi.encode(name, symbol, decimals))` -from faucet storage via FPI. The setters are authority-gated (so they fall to `FAUCET_ADMIN`) and, -with the faucet's note allowlist limited to MINT and BURN, unreachable until the admin explicitly -allowlists a note that calls them. +### 3.2 Faucet Account + +The faucet account carries no AggLayer-specific component: it is the standard `FungibleFaucet` +installed on top of `Ownable2Step` and the RBAC access-control stack (`RoleBasedAccessControl` + +`Authority::RbacControlled`). Bridge-in and bridge-out run through the standard `mint_and_send` +and `receive_and_burn`, and the unified MINT/BURN note scripts use `has_procedure` to detect the +faucet kind reflectively. + +What makes it an AggLayer faucet is its deployment configuration: + +- The bridge is its `Ownable2Step` owner, and its active mint and burn policies are `owner_only`, + so only the bridge can mint on a bridge-in claim or burn on a bridge-out. +- Its note allowlist is limited to MINT, BURN, RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG plus the + network-account defaults, which keeps the standard `set_*` metadata setters unreachable until + `FAUCET_ADMIN` explicitly allowlists a note that calls them. +- Its token name holds the foreign token's real name, so the metadata hash preimage + `abi.encode(name, symbol, decimals)` is recoverable from its storage (see + [Section 7.1](#71-registering-faucets-on-miden)). #### `fungible::mint_and_send` diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index cb4bc3e4db..07ee1bfd69 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -146,11 +146,6 @@ fn compile_agglayer_package( /// /// This file contains: /// - AggLayer Bridge code commitment. -/// -/// The AggLayer faucet is the standard [`FungibleFaucet`] and so carries no bespoke commitment; -/// callers identify it through its interface instead (see `FungibleFaucet::try_from`). -/// -/// [`FungibleFaucet`]: miden_standards::account::faucets::FungibleFaucet fn generate_agglayer_constants( target_file: impl AsRef, component_packages: Vec>, diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index 528aa7d5ca..c128c690f8 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -46,8 +46,7 @@ impl MetadataHash { /// Computes the metadata hash from a faucet's own token metadata. /// - /// Preferred over [`Self::from_token_info`] when the faucet exists, since it derives the hash - /// registered on the bridge from the very values held in faucet storage: the two cannot drift. + /// Preferred over [`Self::from_token_info`] when the faucet exists. pub fn from_fungible_faucet(faucet: &FungibleFaucet) -> Self { Self::from_token_info( faucet.token_name().as_str(), diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index 1350c5a281..ba04baace7 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -18,12 +18,12 @@ static FEE_MANAGER_ROLE: LazyLock = // AGGLAYER FAUCET // ================================================================================================ -/// The deployment configuration of an AggLayer faucet. +/// Helper type holding the deployment configuration of an AggLayer faucet. /// -/// An AggLayer faucet is a standard -/// [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) owned by the bridge; it -/// carries no AggLayer-specific account component, so this type holds no state and exists only to -/// namespace the deployment parameters the bridge and the faucet operator have to agree on: the +/// This is not an [`AccountComponent`](miden_protocol::account::AccountComponent): an AggLayer +/// faucet is an ordinary [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) +/// owned by the bridge, with no AggLayer-specific component of its own. The type carries no state +/// and only groups what the bridge and the faucet operator have to agree on at deployment: the /// note allowlist, the RBAC roles, and the account builder /// ([`AggLayerFaucet::account_builder`](crate::AggLayerFaucet::account_builder), defined alongside /// the bridge's). From 6afdea1d7116234f77c1687f3cf829452635f37f Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 27 Aug 2026 17:00:33 +0300 Subject: [PATCH 04/11] refactor: address review comments - `AggLayerFaucet::account_builder` takes `TokenName`, `TokenSymbol` and `AssetAmount` instead of `&str` and `Felt`, so validation happens at the caller. This drops the `build_fungible_faucet` wrapper and its four `.expect`s; the remaining one covers the checks `FungibleFaucet` itself performs, on the decimals and on the two supplies. - `create_existing_agglayer_faucet` keeps its loose signature and converts internally, so test call sites are unaffected. - Rewrite the `AggLayerFaucet`, `account_builder` and SPEC 3.2 docs to describe the faucet as it is rather than as the absence of the component that used to wrap it. - Drop the four "so the two cannot drift" comments next to `MetadataHash::from_fungible_faucet` and the "(currently only the bridge)" aside in `build.rs`. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- .../src/context_setups/mod.rs | 1 - crates/miden-agglayer/SPEC.md | 9 +-- crates/miden-agglayer/build.rs | 2 +- crates/miden-agglayer/src/faucet.rs | 22 +++--- crates/miden-agglayer/src/lib.rs | 74 ++++++------------- crates/miden-agglayer/src/testing/mod.rs | 11 +-- .../tests/agglayer/bridge_out.rs | 3 - .../tests/agglayer/faucet_helpers.rs | 6 +- .../tests/agglayer/fee_policy.rs | 11 +-- .../tests/agglayer/test_utils.rs | 11 +-- 11 files changed, 59 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 528e7a108e..408005164a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ - [BREAKING] Narrowed the block header version field from 32 to 8 bits and set it to the only supported version instead of taking it as a `BlockHeader::new` parameter ([#3695](https://github.com/0xMiden/protocol/pull/3695)). - [BREAKING] Serialize the version in `Account`, `AccountHeader`, `PartialNoteMetadata` and `AssetId` ([#3697](https://github.com/0xMiden/protocol/pull/3697)). - [BREAKING] Moved the account delta and patch domain separators into the hasher capacity word. Added a version to their commitments ([#3698](https://github.com/0xMiden/protocol/pull/3698)). -- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`. `AggLayerFaucet` is now a stateless namespace, `AgglayerFaucetError` and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` and `create_existing_agglayer_faucet` take a token name ([#3525](https://github.com/0xMiden/protocol/pull/3525)). +- [BREAKING] Replaced the dedicated AggLayer faucet account component with the standard `FungibleFaucet`. `AggLayerFaucet` is now a stateless namespace, `AgglayerFaucetError` and the `miden-agglayer-faucet` MASM package were removed, `AggLayerFaucet::account_builder` and `create_existing_agglayer_faucet` take a token name, and `account_builder` now takes `TokenName` / `TokenSymbol` / `AssetAmount` rather than `&str` / `Felt` ([#3525](https://github.com/0xMiden/protocol/pull/3525)). - [BREAKING] Introduced `ProtocolConfig` that commits to all kernel's procedures, the fee asset ID and the security policy for recursive verification ([#3725](https://github.com/0xMiden/protocol/pull/3725)). - [BREAKING] Renamed `ValidatorKeys` to `ValidatorConfig` and added a quorum to it ([#3725](https://github.com/0xMiden/protocol/pull/3725)). - [BREAKING] Replaced `tx::get_fee_faucet_id` with `tx::get_fee_asset_id` ([#3741](https://github.com/0xMiden/protocol/pull/3741)). diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index 916e5defe0..29064fbd91 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -720,7 +720,6 @@ pub async fn tx_consume_b2agg_note( builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 9cf24ebc4f..717eb7fad5 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -480,11 +480,10 @@ holders are seeded into the access-control components at account creation time. ### 3.2 Faucet Account -The faucet account carries no AggLayer-specific component: it is the standard `FungibleFaucet` -installed on top of `Ownable2Step` and the RBAC access-control stack (`RoleBasedAccessControl` + -`Authority::RbacControlled`). Bridge-in and bridge-out run through the standard `mint_and_send` -and `receive_and_burn`, and the unified MINT/BURN note scripts use `has_procedure` to detect the -faucet kind reflectively. +The faucet account is the standard `FungibleFaucet` installed on top of `Ownable2Step` and the +RBAC access-control stack (`RoleBasedAccessControl` + `Authority::RbacControlled`). Bridge-in and +bridge-out run through the standard `mint_and_send` and `receive_and_burn`, and the unified +MINT/BURN note scripts use `has_procedure` to detect the faucet kind reflectively. What makes it an AggLayer faucet is its deployment configuration: diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index 07ee1bfd69..ea7f040253 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -170,7 +170,7 @@ fn generate_agglayer_constants( // code commitment, so it doesn't matter what does this metadata holds. let dummy_metadata = AccountComponentMetadata::new("dummy"); - // iterate over the AggLayer component packages (currently only the bridge) + // iterate over the AggLayer component packages for package in component_packages { // Derive the short component name (e.g. "bridge") from the package name // (e.g. "miden-agglayer-bridge"). diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index ba04baace7..6f0b371727 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -18,19 +18,19 @@ static FEE_MANAGER_ROLE: LazyLock = // AGGLAYER FAUCET // ================================================================================================ -/// Helper type holding the deployment configuration of an AggLayer faucet. +/// The deployment configuration of an AggLayer faucet. /// -/// This is not an [`AccountComponent`](miden_protocol::account::AccountComponent): an AggLayer -/// faucet is an ordinary [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) -/// owned by the bridge, with no AggLayer-specific component of its own. The type carries no state -/// and only groups what the bridge and the faucet operator have to agree on at deployment: the -/// note allowlist, the RBAC roles, and the account builder -/// ([`AggLayerFaucet::account_builder`](crate::AggLayerFaucet::account_builder), defined alongside -/// the bridge's). +/// An AggLayer faucet is a [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) +/// owned by the bridge. This type is a stateless namespace for the settings the bridge and the +/// faucet operator agree on when one is deployed: the note allowlist +/// ([`AggLayerFaucet::allowed_notes`]), the RBAC roles +/// ([`AggLayerFaucet::procedure_roles`], [`AggLayerFaucet::fee_manager_role`]) and the account +/// builder ([`AggLayerFaucet::account_builder`](crate::AggLayerFaucet::account_builder), defined +/// alongside the bridge's). /// -/// To read the token metadata of a deployed faucet, decode the account with -/// [`FungibleFaucet::try_from`](miden_standards::account::faucets::FungibleFaucet); to read its -/// owner, use [`Ownable2Step`](miden_standards::account::access::Ownable2Step). +/// To inspect a deployed faucet, decode its account with +/// [`FungibleFaucet::try_from`](miden_standards::account::faucets::FungibleFaucet) for the token +/// metadata and [`Ownable2Step`](miden_standards::account::access::Ownable2Step) for the owner. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AggLayerFaucet; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 8933e43cad..9a877b32f4 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -2,7 +2,7 @@ extern crate alloc; -use miden_core::{Felt, Word}; +use miden_core::Word; use miden_protocol::account::{AccountBuilder, AccountId, AssetCallbackFlag}; use miden_protocol::assembly::Path; use miden_protocol::asset::{AssetAmount, TokenSymbol}; @@ -117,47 +117,6 @@ fn agglayer_bridge_component_package() -> Package { // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ -/// Builds the [`FungibleFaucet`] component of an agglayer faucet account from its token metadata. -/// -/// The faucet holds only token metadata; conversion metadata (origin address, origin network, -/// scale, metadata hash) lives on the bridge and is populated at registration time. -/// -/// The token name is stored alongside the symbol and decimals, which makes the metadata hash -/// preimage `abi.encode(name, symbol, decimals)` recoverable from the faucet's own storage. -/// -/// # Parameters -/// - `token_name`: The display name for the fungible token (e.g., "AggLayer Token") -/// - `token_symbol`: The symbol for the fungible token (e.g., "AGG") -/// - `decimals`: Number of decimal places for the token -/// - `max_supply`: Maximum supply of the token -/// - `initial_supply`: Initial outstanding token supply (0 for new faucets) -/// -/// # Panics -/// Panics if the token name or symbol is invalid, or if the supplies exceed the maximum amount -/// representable by a fungible asset. -fn build_fungible_faucet( - token_name: &str, - token_symbol: &str, - decimals: u8, - max_supply: Felt, - initial_supply: Felt, -) -> FungibleFaucet { - let name = TokenName::new(token_name).expect("token name should be valid"); - let symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); - let max_supply = AssetAmount::try_from(max_supply).expect("max supply should be valid"); - let initial_supply = - AssetAmount::try_from(initial_supply).expect("initial supply should be valid"); - - FungibleFaucet::builder() - .name(name) - .symbol(symbol) - .decimals(decimals) - .max_supply(max_supply) - .token_supply(initial_supply) - .build() - .expect("agglayer faucet metadata should be valid") -} - impl AggLayerBridge { /// Returns an [`AccountBuilder`] for a bridge account with the standard configuration. /// @@ -202,10 +161,11 @@ impl AggLayerFaucet { /// Returns an [`AccountBuilder`] for a faucet account with the specified deployment /// configuration. /// - /// The account is a standard [`FungibleFaucet`] carrying no AggLayer-specific component: - /// `mint_and_send` and `receive_and_burn` drive bridge-in and bridge-out, and the standard - /// metadata getters expose the token name, symbol and decimals that make up the AggLayer - /// metadata hash preimage. + /// The account is a standard [`FungibleFaucet`]: `mint_and_send` and `receive_and_burn` drive + /// bridge-in and bridge-out, and the standard metadata getters expose the token name, symbol + /// and decimals that make up the AggLayer metadata hash preimage. Conversion metadata (origin + /// address, origin network, scale, metadata hash) lives on the bridge and is written there at + /// registration time. /// /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] @@ -215,15 +175,16 @@ impl AggLayerFaucet { /// /// # Panics /// - /// Panics if the token metadata is invalid. + /// Panics if `decimals` exceeds [`FungibleFaucet::MAX_DECIMALS`], or if `initial_supply` + /// exceeds `max_supply`. #[allow(clippy::too_many_arguments)] pub fn account_builder( seed: Word, - token_name: &str, - token_symbol: &str, + token_name: TokenName, + token_symbol: TokenSymbol, decimals: u8, - max_supply: Felt, - initial_supply: Felt, + max_supply: AssetAmount, + initial_supply: AssetAmount, faucet_admin: AccountId, fee_manager: AccountId, bridge_account_id: AccountId, @@ -234,8 +195,14 @@ impl AggLayerFaucet { .fee_faucet_id(fee_faucet_id) .active_fee_policy(fee_policy.into()) .build(); - let faucet = - build_fungible_faucet(token_name, token_symbol, decimals, max_supply, initial_supply); + let faucet = FungibleFaucet::builder() + .name(token_name) + .symbol(token_symbol) + .decimals(decimals) + .max_supply(max_supply) + .token_supply(initial_supply) + .build() + .expect("agglayer faucet decimals and supplies should be within their valid ranges"); let token_policy_manager = TokenPolicyManager::builder() .active_mint_policy(MintPolicy::owner_only()) @@ -270,6 +237,7 @@ impl AggLayerFaucet { #[cfg(test)] mod tests { + use miden_core::Felt; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use miden_standards::tx_script::ExpirationTransactionScript; diff --git a/crates/miden-agglayer/src/testing/mod.rs b/crates/miden-agglayer/src/testing/mod.rs index c138da7e55..46a7cb46e1 100644 --- a/crates/miden-agglayer/src/testing/mod.rs +++ b/crates/miden-agglayer/src/testing/mod.rs @@ -7,7 +7,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; use miden_protocol::account::{Account, AccountId}; -use miden_protocol::asset::AssetAmount; +use miden_protocol::asset::{AssetAmount, TokenSymbol}; use miden_protocol::note::NoteScriptRoot; use miden_protocol::testing::account_id::{ ACCOUNT_ID_FEE_FAUCET, @@ -16,6 +16,7 @@ use miden_protocol::testing::account_id::{ use miden_protocol::utils::hex_to_bytes; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::TokenName; use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager}; use miden_standards::interop::eth::{EthAddress, EthAmount}; use serde::Deserialize; @@ -108,11 +109,11 @@ pub fn create_existing_agglayer_faucet( let faucet_admin = bridge_admin_account_id(); AggLayerFaucet::account_builder( seed, - token_name, - token_symbol, + TokenName::new(token_name).expect("token name should be valid"), + TokenSymbol::new(token_symbol).expect("token symbol should be valid"), decimals, - max_supply, - initial_supply, + AssetAmount::try_from(max_supply).expect("max supply should be valid"), + AssetAmount::try_from(initial_supply).expect("initial supply should be valid"), faucet_admin, fee_manager, bridge_account_id, diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 66f7d0dd22..36b689f312 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -153,7 +153,6 @@ async fn bridge_out_consecutive( verification_base_fee, )? .build_existing()?; - // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); builder.add_account(faucet.clone())?; @@ -473,7 +472,6 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho bridge_admin_account_id(), bridge_account.id(), ); - // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); builder.add_account(faucet.clone())?; @@ -715,7 +713,6 @@ async fn test_bridge_out_rejects_invalid_b2agg_note( bridge_admin_account_id(), bridge_account.id(), ); - // Derive the registered hash from the faucet's own stored metadata so the two cannot drift. let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); builder.add_account(faucet.clone())?; diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index f44519550e..b2b86f6b7a 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -67,9 +67,9 @@ fn agglayer_faucet_is_a_bridge_owned_fungible_faucet() -> anyhow::Result<()> { // procedure roots before decoding storage) succeeds. let metadata = FungibleFaucet::try_from(&faucet)?; - // Every field round-trips, most importantly the token name: it used to be derived from the - // symbol, which left the metadata hash preimage `abi.encode(name, symbol, decimals)` - // unrecoverable from faucet storage, and so unverifiable on-chain (issues #2585, #2586). + // Every field round-trips, most importantly the token name: it makes the metadata hash + // preimage `abi.encode(name, symbol, decimals)` recoverable from faucet storage, which is what + // will let the bridge verify the registered hash on-chain (issue #2586). assert_eq!(metadata.token_name().as_str(), token_name); assert_eq!(metadata.symbol().to_string(), token_symbol); assert_eq!(metadata.decimals(), decimals); diff --git a/crates/miden-testing/tests/agglayer/fee_policy.rs b/crates/miden-testing/tests/agglayer/fee_policy.rs index 64b68c09de..1034763fb6 100644 --- a/crates/miden-testing/tests/agglayer/fee_policy.rs +++ b/crates/miden-testing/tests/agglayer/fee_policy.rs @@ -3,11 +3,12 @@ use alloc::collections::BTreeSet; use miden_agglayer::testing::bridge_admin_account_id; use miden_agglayer::{AggLayerBridge, AggLayerFaucet, BridgeRoles}; use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType, StorageMapKey}; -use miden_protocol::asset::{AssetId, FungibleAsset}; +use miden_protocol::asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}; use miden_protocol::note::{Note, NoteScriptRoot}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; use miden_standards::account::auth::{AuthNetworkAccount, NetworkAccount}; +use miden_standards::account::faucets::TokenName; use miden_standards::account::fees::{ BasicConstantFeePolicy, ConstantFeeManager, @@ -158,11 +159,11 @@ fn build_managed_account(managed: ManagedAccount) -> anyhow::Result { ManagedAccount::Bridge => bridge, ManagedAccount::Faucet => AggLayerFaucet::account_builder( Word::from([1u32, 0, 0, 0]), - "AggLayer Token", - "AGG", + TokenName::new("AggLayer Token")?, + TokenSymbol::new("AGG")?, 6, - 1_000u32.into(), - Felt::ZERO, + AssetAmount::new(1_000)?, + AssetAmount::ZERO, account_admin, fee_manager_id(), bridge.id(), diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index e53ffc8d72..88b6b9fcd8 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -26,7 +26,7 @@ use miden_processor::{ }; use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::{Account, AccountBuilder, AccountId}; -use miden_protocol::asset::{AssetId, FungibleAsset}; +use miden_protocol::asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol}; use miden_protocol::block::FeeParameters; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::{Note, NoteScriptRoot}; @@ -36,6 +36,7 @@ use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, ProtocolLib, Word}; use miden_standards::StandardsLib; use miden_standards::account::access::PausableStorage; +use miden_standards::account::faucets::TokenName; use miden_standards::note::{FeeSponsorshipNote, StandardNote}; use miden_testing::{Auth, MockChain, MockChainBuilder}; use miden_tx::NetworkNotePricer; @@ -186,11 +187,11 @@ pub fn priced_faucet_builder( let faucet_admin = bridge_admin_account_id(); Ok(AggLayerFaucet::account_builder( seed, - token_name, - token_symbol, + TokenName::new(token_name)?, + TokenSymbol::new(token_symbol)?, decimals, - max_supply, - initial_supply, + AssetAmount::try_from(max_supply)?, + AssetAmount::try_from(initial_supply)?, faucet_admin, faucet_admin, bridge_account_id, From c4f44ee1d7b8f66f70a022684d01dc26742fa5d2 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 27 Aug 2026 17:33:12 +0300 Subject: [PATCH 05/11] fix: repair the release digest check and qualify the metadata hash claim `scripts/check-masm-export-digests.rs` reads `faucet::FAUCET_CODE_COMMITMENT`, which this PR removes. The script lives outside the cargo workspace (it is a `-Zscript` file assembled by check-masm-root-stability.sh at release time), so no workspace build compiles it. Drop the constant from both the `current` and the `previous` collector: `previous` resolves against the release baseline, which stops carrying the constant as soon as a tag cut after this PR becomes the baseline. `scripts/README.md` no longer claims a faucet commitment is collected, and says the faucet's component composition is not pinned. Also qualify the claim that the metadata hash preimage is recoverable from faucet storage. That holds only when the faucet's metadata is identical to the origin token's, which needs a zero `scale` (a scaled faucet stores `origin_decimals - scale`), an origin name within `TokenName`'s 32 bytes, and an origin symbol within `TokenSymbol`'s 1-12 uppercase ASCII characters. The caveat lands on `MetadataHash::from_fungible_faucet`, on the deployer-facing `AggLayerFaucet::account_builder`, on `ConversionMetadata::metadata_hash` and in SPEC 3.2 / 7.1. Every `from_fungible_faucet` call site uses `scale = 0`, so nothing is wrong today, but #2586 plans to assert the unqualified claim on-chain. Co-Authored-By: Claude Opus 5 --- crates/miden-agglayer/SPEC.md | 23 ++++++++++++++----- crates/miden-agglayer/src/config_note.rs | 4 +++- .../src/eth_types/metadata_hash.rs | 8 ++++++- crates/miden-agglayer/src/lib.rs | 10 +++++--- scripts/README.md | 2 +- scripts/check-masm-export-digests.rs | 12 ++-------- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 717eb7fad5..f7e9c5af9c 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -492,9 +492,9 @@ What makes it an AggLayer faucet is its deployment configuration: - Its note allowlist is limited to MINT, BURN, RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG plus the network-account defaults, which keeps the standard `set_*` metadata setters unreachable until `FAUCET_ADMIN` explicitly allowlists a note that calls them. -- Its token name holds the foreign token's real name, so the metadata hash preimage - `abi.encode(name, symbol, decimals)` is recoverable from its storage (see - [Section 7.1](#71-registering-faucets-on-miden)). +- Its token name holds the foreign token's real name, which is what can make the metadata hash + preimage `abi.encode(name, symbol, decimals)` recoverable from its storage - subject to the + conditions in [Section 7.1](#71-registering-faucets-on-miden). #### `fungible::mint_and_send` @@ -1436,9 +1436,20 @@ same flow for both kinds; the `is_native` flag in the `CONFIG_AGG_BRIDGE` note s the bridge which dispatch path to take for each future bridge operation against that faucet. The faucet holds only token metadata — name, symbol, decimals, max supply, and token supply. -Because the name is stored alongside the symbol and decimals, the full metadata hash preimage -`abi.encode(name, symbol, decimals)` is recoverable from faucet storage, which is what makes -registration-time verification possible. +Storing the name alongside the symbol and decimals is what can make the full metadata hash +preimage `abi.encode(name, symbol, decimals)` recoverable from faucet storage, and so makes +registration-time verification possible — but only when the faucet's metadata is identical to the +origin token's, which requires all three of: + +- `scale` is zero, since a scaled faucet stores `origin_decimals - scale` rather than the origin + decimals that go into the preimage; +- the origin name fits `TokenName`'s 32-byte cap; +- the origin symbol fits `TokenSymbol`'s 1-12 uppercase-ASCII form, which excludes symbols such as + `USDC.e`, `wstETH` or `1INCH`. + +Outside those conditions the registered hash is the one carried in the origin chain's leaf and +cannot be reconstructed from the faucet, which +[#2586](https://github.com/0xMiden/protocol/issues/2586) has to account for. Conversion metadata (origin address, origin network, scale, and metadata hash) is *not* stored on the faucet; it is carried by the `CONFIG_AGG_BRIDGE` note at registration time and written directly into the bridge's `faucet_metadata_map`. The metadata hash is diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index 8d642b3440..6507e7f54a 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -66,7 +66,9 @@ pub struct ConversionMetadata { /// locks into it); `false` for bridge-owned faucets (bridge-in mints via the faucet, /// bridge-out burns via the faucet). pub is_native: bool, - /// keccak256 hash of the ABI-encoded token metadata (`name`, `symbol`, `decimals`). + /// keccak256 hash of the ABI-encoded token metadata (`name`, `symbol`, `decimals`) as it + /// exists on the origin chain, which is not necessarily what the faucet stores. See SPEC + /// section 7.1. pub metadata_hash: MetadataHash, } diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index c128c690f8..a1097f2aad 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -46,7 +46,13 @@ impl MetadataHash { /// Computes the metadata hash from a faucet's own token metadata. /// - /// Preferred over [`Self::from_token_info`] when the faucet exists. + /// This reproduces the hash registered on the bridge only for a faucet whose stored metadata + /// is identical to the origin token's, which requires all three of: the faucet's `scale` is + /// zero (otherwise it stores `origin_decimals - scale`), the origin name fits + /// [`TokenName`](miden_standards::account::faucets::TokenName)'s 32 bytes, and the origin + /// symbol fits [`TokenSymbol`](miden_protocol::asset::TokenSymbol)'s 1-12 uppercase ASCII + /// characters. Otherwise the registered hash is the origin chain's, and its preimage has to + /// be passed to [`Self::from_token_info`] directly. pub fn from_fungible_faucet(faucet: &FungibleFaucet) -> Self { Self::from_token_info( faucet.token_name().as_str(), diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index bf688f73d8..2f700d8a3d 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -163,9 +163,13 @@ impl AggLayerFaucet { /// /// The account is a standard [`FungibleFaucet`]: `mint_and_send` and `receive_and_burn` drive /// bridge-in and bridge-out, and the standard metadata getters expose the token name, symbol - /// and decimals that make up the AggLayer metadata hash preimage. Conversion metadata (origin - /// address, origin network, scale, metadata hash) lives on the bridge and is written there at - /// registration time. + /// and decimals. Conversion metadata (origin address, origin network, scale, metadata hash) + /// lives on the bridge and is written there at registration time. + /// + /// The metadata stored here reproduces the AggLayer metadata hash preimage only under the + /// conditions listed in SPEC section 7.1; in particular a faucet registered with a non-zero + /// `scale` stores `origin_decimals - scale`, not the origin decimals the preimage is built + /// from. See [`MetadataHash::from_fungible_faucet`]. /// /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] diff --git a/scripts/README.md b/scripts/README.md index d6255f9f63..bfac598f96 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,7 +12,7 @@ The baseline is the latest release tag and it is compared against the version ab - Every exported procedure digest of the `ProtocolLib`, transaction kernel, `StandardsLib` and agglayer packages. - The kernel commitment and the two kernel programs (they cover the prologue, epilogue and procedure ordering, which the package exports do not). -- The agglayer bridge and faucet code commitments, which no package export covers. +- The agglayer bridge code commitment, which no package export covers. There is no faucet equivalent: the agglayer faucet is assembled from standard components, whose own roots and commitments are covered by the per-component entries below, but nothing pins the composition itself. - Per account component: each procedure root, plus a commitment over its whole procedure set. A changed or removed root fails. An added procedure is only a warning, but an addition to the kernel or to a component still fails because it changes the corresponding commitment. diff --git a/scripts/check-masm-export-digests.rs b/scripts/check-masm-export-digests.rs index 9be94b30fa..6e21de7bff 100755 --- a/scripts/check-masm-export-digests.rs +++ b/scripts/check-masm-export-digests.rs @@ -70,7 +70,7 @@ fn compare_roots(previous: Roots, current: Roots) -> Result<(), String> { } mod current { - use miden_agglayer_current::{AggLayerBridge, agglayer_package, bridge, faucet}; + use miden_agglayer_current::{AggLayerBridge, agglayer_package, bridge}; use miden_protocol_current::account::AccountComponentCode; use miden_protocol_current::account::component::AUTH_SCRIPT_ATTRIBUTE; use miden_protocol_current::transaction::TransactionKernel; @@ -188,10 +188,6 @@ mod current { "agglayer::BRIDGE_CODE_COMMITMENT".to_string(), bridge::BRIDGE_CODE_COMMITMENT.to_hex(), ); - roots.insert( - "agglayer::FAUCET_CODE_COMMITMENT".to_string(), - faucet::FAUCET_CODE_COMMITMENT.to_hex(), - ); for code in COMPONENT_CODE { collect_component(code(), &mut roots); @@ -233,7 +229,7 @@ mod current { } mod previous { - use miden_agglayer_previous::{AggLayerBridge, agglayer_package, bridge, faucet}; + use miden_agglayer_previous::{AggLayerBridge, agglayer_package, bridge}; use miden_protocol_previous::account::AccountComponentCode; use miden_protocol_previous::account::component::AUTH_SCRIPT_ATTRIBUTE; use miden_protocol_previous::transaction::TransactionKernel; @@ -351,10 +347,6 @@ mod previous { "agglayer::BRIDGE_CODE_COMMITMENT".to_string(), bridge::BRIDGE_CODE_COMMITMENT.to_hex(), ); - roots.insert( - "agglayer::FAUCET_CODE_COMMITMENT".to_string(), - faucet::FAUCET_CODE_COMMITMENT.to_hex(), - ); for code in COMPONENT_CODE { collect_component(code(), &mut roots); From fbf7344fa709dc1fe8b1b9b86f596d57498e35ad Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 27 Aug 2026 19:57:13 +0300 Subject: [PATCH 06/11] docs: correct where `scale` lives `scale` is a bridge-side registration parameter on `ConversionMetadata`, written into the bridge's `faucet_metadata_map`. It is not a `FungibleFaucet` or `TokenMetadata` field, so a faucet does not "store `origin_decimals - scale`" as the previous wording claimed. No bridge procedure reads the faucet's `decimals` at all; the relation between it and `scale` is an operator convention that nothing enforces. State it that way instead: the preimage takes the origin token's decimals, the faucet's own `decimals` is independent, and a non-zero `scale` is the signal that the operator meant the two to differ. Co-Authored-By: Claude Opus 5 --- crates/miden-agglayer/SPEC.md | 19 +++++++++++-------- .../src/eth_types/metadata_hash.rs | 14 +++++++------- crates/miden-agglayer/src/lib.rs | 6 +++--- scripts/README.md | 2 +- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index f7e9c5af9c..f3a733e8ca 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -1438,14 +1438,17 @@ the bridge which dispatch path to take for each future bridge operation against The faucet holds only token metadata — name, symbol, decimals, max supply, and token supply. Storing the name alongside the symbol and decimals is what can make the full metadata hash preimage `abi.encode(name, symbol, decimals)` recoverable from faucet storage, and so makes -registration-time verification possible — but only when the faucet's metadata is identical to the -origin token's, which requires all three of: - -- `scale` is zero, since a scaled faucet stores `origin_decimals - scale` rather than the origin - decimals that go into the preimage; -- the origin name fits `TokenName`'s 32-byte cap; -- the origin symbol fits `TokenSymbol`'s 1-12 uppercase-ASCII form, which excludes symbols such as - `USDC.e`, `wstETH` or `1INCH`. +registration-time verification possible — but only for a faucet the operator gave metadata +identical to the origin token's, which needs all three of: + +- the same decimals. The `decimals` in the preimage are the origin token's; a faucet's own + `decimals` is a separate value that no bridge procedure reads and that nothing relates to the + registered `scale`. Because `scale` converts between origin-chain and Miden-side units, a + non-zero `scale` is the operator's signal that the two decimal bases deliberately differ, and + `FungibleFaucet::MAX_DECIMALS` (12) forces that for an 18-decimal ERC-20; +- an origin name within `TokenName`'s 32-byte cap; +- an origin symbol within `TokenSymbol`'s 1-12 uppercase-ASCII form, which excludes symbols such + as `USDC.e`, `wstETH` or `1INCH`. Outside those conditions the registered hash is the one carried in the origin chain's leaf and cannot be reconstructed from the faucet, which diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index a1097f2aad..e2f32376d7 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -46,13 +46,13 @@ impl MetadataHash { /// Computes the metadata hash from a faucet's own token metadata. /// - /// This reproduces the hash registered on the bridge only for a faucet whose stored metadata - /// is identical to the origin token's, which requires all three of: the faucet's `scale` is - /// zero (otherwise it stores `origin_decimals - scale`), the origin name fits - /// [`TokenName`](miden_standards::account::faucets::TokenName)'s 32 bytes, and the origin - /// symbol fits [`TokenSymbol`](miden_protocol::asset::TokenSymbol)'s 1-12 uppercase ASCII - /// characters. Otherwise the registered hash is the origin chain's, and its preimage has to - /// be passed to [`Self::from_token_info`] directly. + /// The `decimals` in the AggLayer preimage are the origin token's. A faucet's own `decimals` + /// is an independent value that nothing ties to them, so this reproduces the hash registered + /// on the bridge only for a faucet the deployer gave metadata identical to the origin token's: + /// the same decimals, a name within + /// [`TokenName`](miden_standards::account::faucets::TokenName)'s 32 bytes, and a symbol within + /// [`TokenSymbol`](miden_protocol::asset::TokenSymbol)'s 1-12 uppercase ASCII characters. + /// Otherwise pass the origin token's values to [`Self::from_token_info`] instead. pub fn from_fungible_faucet(faucet: &FungibleFaucet) -> Self { Self::from_token_info( faucet.token_name().as_str(), diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 2f700d8a3d..a4b761a4f6 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -167,9 +167,9 @@ impl AggLayerFaucet { /// lives on the bridge and is written there at registration time. /// /// The metadata stored here reproduces the AggLayer metadata hash preimage only under the - /// conditions listed in SPEC section 7.1; in particular a faucet registered with a non-zero - /// `scale` stores `origin_decimals - scale`, not the origin decimals the preimage is built - /// from. See [`MetadataHash::from_fungible_faucet`]. + /// conditions listed in SPEC section 7.1; in particular the preimage is built from the origin + /// token's decimals, which nothing requires `decimals` to match. See + /// [`MetadataHash::from_fungible_faucet`]. /// /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] diff --git a/scripts/README.md b/scripts/README.md index bfac598f96..4a79e78f65 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,7 +12,7 @@ The baseline is the latest release tag and it is compared against the version ab - Every exported procedure digest of the `ProtocolLib`, transaction kernel, `StandardsLib` and agglayer packages. - The kernel commitment and the two kernel programs (they cover the prologue, epilogue and procedure ordering, which the package exports do not). -- The agglayer bridge code commitment, which no package export covers. There is no faucet equivalent: the agglayer faucet is assembled from standard components, whose own roots and commitments are covered by the per-component entries below, but nothing pins the composition itself. +- The agglayer bridge code commitment, which no package export covers. - Per account component: each procedure root, plus a commitment over its whole procedure set. A changed or removed root fails. An added procedure is only a warning, but an addition to the kernel or to a component still fails because it changes the corresponding commitment. From fcea3e79aaa846bb2727fccaad1ab508f3b797bd Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 27 Aug 2026 21:30:57 +0300 Subject: [PATCH 07/11] refactor: restrict the faucet-derived metadata hash to native faucets The AggLayer metadata hash preimage is the origin token's `(name, symbol, decimals)`. For a Miden-native faucet the faucet is that origin token, so deriving the hash from its storage cannot disagree with what the bridge registers. For a wrapped faucet the origin token lives on another chain and the faucet is a separate account that Miden's types often cannot mirror at all: `FungibleFaucet::MAX_DECIMALS` is 12, so an 18-decimal ERC-20 is unrepresentable. Rename `from_fungible_faucet` to `from_native_faucet` so the precondition is visible at the call site, and use it where it belongs: the native bridge-out test registered a hardcoded hash over `("Native Token", "NATIVE", 8)` while its faucet stores `("NATIVE", "NATIVE", 10)`, a mismatch that would have failed #2586's check. The three wrapped `bridge_out` sites and the bench go back to `from_token_info` with the origin vector's values. Co-Authored-By: Claude Opus 5 --- .../src/context_setups/mod.rs | 3 +- crates/miden-agglayer/SPEC.md | 39 +++++++++---------- .../src/eth_types/metadata_hash.rs | 21 ++++++---- crates/miden-agglayer/src/lib.rs | 8 ++-- .../tests/agglayer/bridge_out.rs | 22 +++++++++-- 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index 29064fbd91..b3b59b0c9f 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -28,7 +28,6 @@ use miden_protocol::testing::account_id::{ACCOUNT_ID_FEE_FAUCET, ACCOUNT_ID_SEND use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; use miden_standards::account::auth::SponsorshipPolicy; -use miden_standards::account::faucets::FungibleFaucet; use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager}; use miden_standards::code_builder::CodeBuilder; use miden_standards::interop::eth::EthAddress; @@ -720,7 +719,7 @@ pub async fn tx_consume_b2agg_note( builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); + let metadata_hash = MetadataHash::from_token_info("AGG", "AGG", 8); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { faucet_account_id: faucet.id(), diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index f3a733e8ca..9ebe73ef87 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -492,9 +492,9 @@ What makes it an AggLayer faucet is its deployment configuration: - Its note allowlist is limited to MINT, BURN, RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG plus the network-account defaults, which keeps the standard `set_*` metadata setters unreachable until `FAUCET_ADMIN` explicitly allowlists a note that calls them. -- Its token name holds the foreign token's real name, which is what can make the metadata hash - preimage `abi.encode(name, symbol, decimals)` recoverable from its storage - subject to the - conditions in [Section 7.1](#71-registering-faucets-on-miden). +- Its token name holds the foreign token's real name rather than a copy of its symbol, so the + faucet carries as much of the metadata hash preimage `abi.encode(name, symbol, decimals)` as + Miden's types allow (see [Section 7.1](#71-registering-faucets-on-miden)). #### `fungible::mint_and_send` @@ -1436,23 +1436,22 @@ same flow for both kinds; the `is_native` flag in the `CONFIG_AGG_BRIDGE` note s the bridge which dispatch path to take for each future bridge operation against that faucet. The faucet holds only token metadata — name, symbol, decimals, max supply, and token supply. -Storing the name alongside the symbol and decimals is what can make the full metadata hash -preimage `abi.encode(name, symbol, decimals)` recoverable from faucet storage, and so makes -registration-time verification possible — but only for a faucet the operator gave metadata -identical to the origin token's, which needs all three of: - -- the same decimals. The `decimals` in the preimage are the origin token's; a faucet's own - `decimals` is a separate value that no bridge procedure reads and that nothing relates to the - registered `scale`. Because `scale` converts between origin-chain and Miden-side units, a - non-zero `scale` is the operator's signal that the two decimal bases deliberately differ, and - `FungibleFaucet::MAX_DECIMALS` (12) forces that for an 18-decimal ERC-20; -- an origin name within `TokenName`'s 32-byte cap; -- an origin symbol within `TokenSymbol`'s 1-12 uppercase-ASCII form, which excludes symbols such - as `USDC.e`, `wstETH` or `1INCH`. - -Outside those conditions the registered hash is the one carried in the origin chain's leaf and -cannot be reconstructed from the faucet, which -[#2586](https://github.com/0xMiden/protocol/issues/2586) has to account for. +The metadata hash preimage is always the *origin* token's `(name, symbol, decimals)`, so how far +that preimage is recoverable from faucet storage depends on which kind of faucet is registered: + +- Miden-native (`is_native = true`): the faucet **is** the origin token, so its stored name, + symbol and decimals are the preimage. +- Wrapped (`is_native = false`): the origin token is a contract on another chain and the faucet is + a separate account. Nothing constrains the two to agree, and Miden's types often make agreement + impossible: `FungibleFaucet::MAX_DECIMALS` is 12, so an 18-decimal ERC-20 cannot be mirrored; + `TokenName` caps names at 32 bytes; and `TokenSymbol` admits only 1-12 uppercase ASCII + characters, which excludes symbols such as `USDC.e`, `wstETH` or `1INCH`. The registered hash is + then the one carried in the origin chain's leaf, and the faucet cannot reproduce it. + +The `scale` recorded at registration converts between origin-chain and Miden-side units; it is a +bridge-side value, is not stored on the faucet, and no bridge procedure reads the faucet's +`decimals` at all. Registration-time verification of the hash +([#2586](https://github.com/0xMiden/protocol/issues/2586)) has to account for the wrapped case. Conversion metadata (origin address, origin network, scale, and metadata hash) is *not* stored on the faucet; it is carried by the `CONFIG_AGG_BRIDGE` note at registration time and written directly into the bridge's `faucet_metadata_map`. The metadata hash is diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index e2f32376d7..64cee5fed3 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -44,16 +44,21 @@ impl MetadataHash { Self::from_abi_encoded(&encoded) } - /// Computes the metadata hash from a faucet's own token metadata. + /// Computes the metadata hash of a Miden-native faucet from its own token metadata. /// - /// The `decimals` in the AggLayer preimage are the origin token's. A faucet's own `decimals` - /// is an independent value that nothing ties to them, so this reproduces the hash registered - /// on the bridge only for a faucet the deployer gave metadata identical to the origin token's: - /// the same decimals, a name within - /// [`TokenName`](miden_standards::account::faucets::TokenName)'s 32 bytes, and a symbol within + /// The preimage is always the *origin* token's `(name, symbol, decimals)`. For a faucet + /// registered with `is_native = true` the faucet is that origin token, so its stored metadata + /// is the preimage and this cannot disagree with what the bridge registers. + /// + /// Do not use this for a wrapped faucet. There the origin token is a contract on another + /// chain, and the faucet is a separate account whose metadata nothing constrains to match it: + /// the origin decimals may exceed + /// [`FungibleFaucet::MAX_DECIMALS`](miden_standards::account::faucets::FungibleFaucet::MAX_DECIMALS), + /// its name may exceed [`TokenName`](miden_standards::account::faucets::TokenName)'s 32 bytes + /// and its symbol may fall outside /// [`TokenSymbol`](miden_protocol::asset::TokenSymbol)'s 1-12 uppercase ASCII characters. - /// Otherwise pass the origin token's values to [`Self::from_token_info`] instead. - pub fn from_fungible_faucet(faucet: &FungibleFaucet) -> Self { + /// Pass the origin chain's values to [`Self::from_token_info`] instead. + pub fn from_native_faucet(faucet: &FungibleFaucet) -> Self { Self::from_token_info( faucet.token_name().as_str(), &faucet.symbol().to_string(), diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index a4b761a4f6..33f8d8b574 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -166,10 +166,10 @@ impl AggLayerFaucet { /// and decimals. Conversion metadata (origin address, origin network, scale, metadata hash) /// lives on the bridge and is written there at registration time. /// - /// The metadata stored here reproduces the AggLayer metadata hash preimage only under the - /// conditions listed in SPEC section 7.1; in particular the preimage is built from the origin - /// token's decimals, which nothing requires `decimals` to match. See - /// [`MetadataHash::from_fungible_faucet`]. + /// The AggLayer metadata hash preimage is the origin token's `(name, symbol, decimals)`. + /// A faucet built here reproduces it only when it is itself the origin token, i.e. when it is + /// registered as Miden-native; see SPEC section 7.1 and + /// [`MetadataHash::from_native_faucet`]. /// /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 36b689f312..546080c612 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -153,7 +153,11 @@ async fn bridge_out_consecutive( verification_base_fee, )? .build_existing()?; - let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); builder.add_account(faucet.clone())?; // CONFIG_AGG_BRIDGE note to register the faucet in the bridge (sent by faucet manager) @@ -472,7 +476,11 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho bridge_admin_account_id(), bridge_account.id(), ); - let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); builder.add_account(faucet.clone())?; let config_note = ConfigAggBridgeNote::create( @@ -713,7 +721,11 @@ async fn test_bridge_out_rejects_invalid_b2agg_note( bridge_admin_account_id(), bridge_account.id(), ); - let metadata_hash = MetadataHash::from_fungible_faucet(&FungibleFaucet::try_from(&faucet)?); + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) @@ -1113,7 +1125,9 @@ async fn bridge_out_lock_native_token() -> anyhow::Result<()> { .expect("valid eth address"); let origin_network = 7u32; // any stable u32 — Miden's test network id let scale = 0u8; - let metadata_hash = MetadataHash::from_token_info("Native Token", "NATIVE", 8); + // The faucet is the origin token here, so its own metadata is the hash preimage. + let metadata_hash = + MetadataHash::from_native_faucet(&FungibleFaucet::try_from(&native_faucet)?); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { From 64a79ac15632859bd849e51b06002bc4cd642072 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 27 Aug 2026 21:36:33 +0300 Subject: [PATCH 08/11] docs: align SPEC 3.2 with the native/wrapped split in 7.1 The faucet storage table still described `token_name` as "Part of the AggLayer metadata hash preimage" without qualification, and the 3.2 bullet framed the gap as a type-width limit. Both read as the claim 7.1 retracts: for a wrapped faucet nothing binds the faucet's metadata to the origin token's, whatever its width. Also read the bench's registered hash from the Solidity vectors instead of repeating their values as literals. Co-Authored-By: Claude Opus 5 --- bin/bench-transaction/src/context_setups/mod.rs | 7 ++++++- crates/miden-agglayer/SPEC.md | 9 +++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index b3b59b0c9f..2ae3d6c8f4 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -719,7 +719,12 @@ pub async fn tx_consume_b2agg_note( builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - let metadata_hash = MetadataHash::from_token_info("AGG", "AGG", 8); + // The preimage is the origin token's metadata, from the vectors, not the wrapped faucet's. + let metadata_hash = MetadataHash::from_token_info( + &vectors.token_name, + &vectors.token_symbol, + vectors.token_decimals, + ); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { faucet_account_id: faucet.id(), diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 9ebe73ef87..487d3bc301 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -492,9 +492,10 @@ What makes it an AggLayer faucet is its deployment configuration: - Its note allowlist is limited to MINT, BURN, RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG plus the network-account defaults, which keeps the standard `set_*` metadata setters unreachable until `FAUCET_ADMIN` explicitly allowlists a note that calls them. -- Its token name holds the foreign token's real name rather than a copy of its symbol, so the - faucet carries as much of the metadata hash preimage `abi.encode(name, symbol, decimals)` as - Miden's types allow (see [Section 7.1](#71-registering-faucets-on-miden)). +- Its token name holds a real name rather than a copy of its symbol. For a Miden-native faucet + that makes the faucet's `(name, symbol, decimals)` the AggLayer metadata hash preimage; for a + wrapped faucet nothing binds them to the origin token's, so the registered hash comes from the + origin chain (see [Section 7.1](#71-registering-faucets-on-miden)). #### `fungible::mint_and_send` @@ -534,7 +535,7 @@ The faucet contributes the full standard `FungibleFaucet` slot set (25 value slo | Slot name | Slot type | Value encoding | Purpose | |-----------|-----------|----------------|---------| | `token_config` | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard fungible faucet token configuration | -| `token_name_0` .. `token_name_1` | Value | Fixed-width string chunks | Token name, up to 32 UTF-8 bytes. Part of the AggLayer metadata hash preimage | +| `token_name_0` .. `token_name_1` | Value | Fixed-width string chunks | Token name, up to 32 UTF-8 bytes. The AggLayer metadata hash preimage for a Miden-native faucet; see [Section 7.1](#71-registering-faucets-on-miden) | | `mutability_config` | Value | Boolean flags | Whether description / logo URI / external link / max supply may be updated. All `false` for AggLayer faucets | | `token_description_0` .. `_6` | Value | Fixed-width string chunks | Optional description; empty for AggLayer faucets | | `logo_uri_0` .. `_6` | Value | Fixed-width string chunks | Optional logo URI; empty for AggLayer faucets | From bfe970b7ff61ee3052609d9c2a78ac8b6620e3b0 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Thu, 27 Aug 2026 23:59:26 +0300 Subject: [PATCH 09/11] refactor: drop `MetadataHash::from_native_faucet` Deriving the hash from a faucet is only correct when that faucet is the origin token, which holds for a Miden-native registration and not for a wrapped one. Encoding that in a name and a doc comment left a public helper that still returns a plausible-looking hash when handed a wrapped faucet, and a wrong hash there produces an exit leaf the origin chain will not match. Removing it makes the misuse unreachable instead of discouraged. The single caller, the native bridge-out test, spells out `from_token_info` and reads the values back from the faucet it registers, so they cannot drift from the mock-chain builder's defaults. `metadata_hash.rs` is now unchanged by this PR. Co-Authored-By: Claude Opus 5 --- .../src/eth_types/metadata_hash.rs | 24 ------------------- crates/miden-agglayer/src/lib.rs | 3 +-- .../tests/agglayer/bridge_out.rs | 11 ++++++--- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index 64cee5fed3..617c6ea5b0 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -1,11 +1,9 @@ -use alloc::string::ToString; use alloc::vec::Vec; use alloy_sol_types::{SolValue, sol}; use miden_core::utils::bytes_to_packed_u32_elements; use miden_crypto::hash::keccak::Keccak256; use miden_protocol::Felt; -use miden_standards::account::faucets::FungibleFaucet; // ================================================================================================ // METADATA HASH @@ -44,28 +42,6 @@ impl MetadataHash { Self::from_abi_encoded(&encoded) } - /// Computes the metadata hash of a Miden-native faucet from its own token metadata. - /// - /// The preimage is always the *origin* token's `(name, symbol, decimals)`. For a faucet - /// registered with `is_native = true` the faucet is that origin token, so its stored metadata - /// is the preimage and this cannot disagree with what the bridge registers. - /// - /// Do not use this for a wrapped faucet. There the origin token is a contract on another - /// chain, and the faucet is a separate account whose metadata nothing constrains to match it: - /// the origin decimals may exceed - /// [`FungibleFaucet::MAX_DECIMALS`](miden_standards::account::faucets::FungibleFaucet::MAX_DECIMALS), - /// its name may exceed [`TokenName`](miden_standards::account::faucets::TokenName)'s 32 bytes - /// and its symbol may fall outside - /// [`TokenSymbol`](miden_protocol::asset::TokenSymbol)'s 1-12 uppercase ASCII characters. - /// Pass the origin chain's values to [`Self::from_token_info`] instead. - pub fn from_native_faucet(faucet: &FungibleFaucet) -> Self { - Self::from_token_info( - faucet.token_name().as_str(), - &faucet.symbol().to_string(), - faucet.decimals(), - ) - } - /// Returns the raw 32-byte array. pub const fn as_bytes(&self) -> &[u8; 32] { &self.0 diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 33f8d8b574..9f2753f148 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -168,8 +168,7 @@ impl AggLayerFaucet { /// /// The AggLayer metadata hash preimage is the origin token's `(name, symbol, decimals)`. /// A faucet built here reproduces it only when it is itself the origin token, i.e. when it is - /// registered as Miden-native; see SPEC section 7.1 and - /// [`MetadataHash::from_native_faucet`]. + /// registered as Miden-native; see SPEC section 7.1. /// /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 546080c612..166b536b21 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1125,9 +1125,14 @@ async fn bridge_out_lock_native_token() -> anyhow::Result<()> { .expect("valid eth address"); let origin_network = 7u32; // any stable u32 — Miden's test network id let scale = 0u8; - // The faucet is the origin token here, so its own metadata is the hash preimage. - let metadata_hash = - MetadataHash::from_native_faucet(&FungibleFaucet::try_from(&native_faucet)?); + // This faucet is registered as Miden-native, so it *is* the origin token and its own stored + // metadata is the hash preimage. Read it back rather than repeating the builder's defaults. + let native_metadata = FungibleFaucet::try_from(&native_faucet)?; + let metadata_hash = MetadataHash::from_token_info( + native_metadata.token_name().as_str(), + &native_metadata.symbol().to_string(), + native_metadata.decimals(), + ); let config_note = ConfigAggBridgeNote::create( ConversionMetadata { From df0f568c4d1db6bd0c559863dc7237b8d2a5bf55 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Fri, 28 Aug 2026 00:33:58 +0300 Subject: [PATCH 10/11] docs: address review comments - Apply the suggested `AggLayerFaucet` doc and drop the paragraph on inspecting a deployed faucet. - Restore `ConversionMetadata::metadata_hash`'s original wording. - Drop the metadata-hash paragraph from `account_builder` and turn its `# Panics` section into a bullet list. - Drop the bench comment and the `agglayer_faucet_is_a_bridge_owned_fungible_faucet` preamble. - Record the wrapped-faucet limitation in SPEC 7.1 as a known issue with a TODO rather than splitting the section by faucet kind, and revert the 3.2 bullet and storage table to their unqualified form now that 7.1 carries it. Co-Authored-By: Claude Opus 5 --- .../src/context_setups/mod.rs | 1 - crates/miden-agglayer/SPEC.md | 38 +++++++++---------- crates/miden-agglayer/src/config_note.rs | 4 +- crates/miden-agglayer/src/faucet.rs | 11 +----- crates/miden-agglayer/src/lib.rs | 9 ++--- .../tests/agglayer/faucet_helpers.rs | 4 -- 6 files changed, 23 insertions(+), 44 deletions(-) diff --git a/bin/bench-transaction/src/context_setups/mod.rs b/bin/bench-transaction/src/context_setups/mod.rs index 2ae3d6c8f4..ae2f26695c 100644 --- a/bin/bench-transaction/src/context_setups/mod.rs +++ b/bin/bench-transaction/src/context_setups/mod.rs @@ -719,7 +719,6 @@ pub async fn tx_consume_b2agg_note( builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - // The preimage is the origin token's metadata, from the vectors, not the wrapped faucet's. let metadata_hash = MetadataHash::from_token_info( &vectors.token_name, &vectors.token_symbol, diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 487d3bc301..c1a37e73b0 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -492,10 +492,9 @@ What makes it an AggLayer faucet is its deployment configuration: - Its note allowlist is limited to MINT, BURN, RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG plus the network-account defaults, which keeps the standard `set_*` metadata setters unreachable until `FAUCET_ADMIN` explicitly allowlists a note that calls them. -- Its token name holds a real name rather than a copy of its symbol. For a Miden-native faucet - that makes the faucet's `(name, symbol, decimals)` the AggLayer metadata hash preimage; for a - wrapped faucet nothing binds them to the origin token's, so the registered hash comes from the - origin chain (see [Section 7.1](#71-registering-faucets-on-miden)). +- Its token name holds the foreign token's real name rather than a copy of its symbol, so the + faucet carries the metadata hash preimage `abi.encode(name, symbol, decimals)` (see + [Section 7.1](#71-registering-faucets-on-miden)). #### `fungible::mint_and_send` @@ -535,7 +534,7 @@ The faucet contributes the full standard `FungibleFaucet` slot set (25 value slo | Slot name | Slot type | Value encoding | Purpose | |-----------|-----------|----------------|---------| | `token_config` | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard fungible faucet token configuration | -| `token_name_0` .. `token_name_1` | Value | Fixed-width string chunks | Token name, up to 32 UTF-8 bytes. The AggLayer metadata hash preimage for a Miden-native faucet; see [Section 7.1](#71-registering-faucets-on-miden) | +| `token_name_0` .. `token_name_1` | Value | Fixed-width string chunks | Token name, up to 32 UTF-8 bytes. Part of the AggLayer metadata hash preimage | | `mutability_config` | Value | Boolean flags | Whether description / logo URI / external link / max supply may be updated. All `false` for AggLayer faucets | | `token_description_0` .. `_6` | Value | Fixed-width string chunks | Optional description; empty for AggLayer faucets | | `logo_uri_0` .. `_6` | Value | Fixed-width string chunks | Optional logo URI; empty for AggLayer faucets | @@ -1437,22 +1436,19 @@ same flow for both kinds; the `is_native` flag in the `CONFIG_AGG_BRIDGE` note s the bridge which dispatch path to take for each future bridge operation against that faucet. The faucet holds only token metadata — name, symbol, decimals, max supply, and token supply. -The metadata hash preimage is always the *origin* token's `(name, symbol, decimals)`, so how far -that preimage is recoverable from faucet storage depends on which kind of faucet is registered: - -- Miden-native (`is_native = true`): the faucet **is** the origin token, so its stored name, - symbol and decimals are the preimage. -- Wrapped (`is_native = false`): the origin token is a contract on another chain and the faucet is - a separate account. Nothing constrains the two to agree, and Miden's types often make agreement - impossible: `FungibleFaucet::MAX_DECIMALS` is 12, so an 18-decimal ERC-20 cannot be mirrored; - `TokenName` caps names at 32 bytes; and `TokenSymbol` admits only 1-12 uppercase ASCII - characters, which excludes symbols such as `USDC.e`, `wstETH` or `1INCH`. The registered hash is - then the one carried in the origin chain's leaf, and the faucet cannot reproduce it. - -The `scale` recorded at registration converts between origin-chain and Miden-side units; it is a -bridge-side value, is not stored on the faucet, and no bridge procedure reads the faucet's -`decimals` at all. Registration-time verification of the hash -([#2586](https://github.com/0xMiden/protocol/issues/2586)) has to account for the wrapped case. +Because the name is stored alongside the symbol and decimals, the full metadata hash preimage +`abi.encode(name, symbol, decimals)` is recoverable from faucet storage, which is what makes +registration-time verification possible. + +The preimage is the *origin* token's `(name, symbol, decimals)`, and Miden's types cannot always +hold those values: `FungibleFaucet::MAX_DECIMALS` is 12, so an 18-decimal ERC-20 cannot be +mirrored; `TokenName` caps names at 32 bytes; and `TokenSymbol` admits only 1-12 uppercase ASCII +characters, which excludes symbols such as `USDC.e`, `wstETH` or `1INCH`. A wrapped faucet +therefore may not be able to reproduce its registered hash at all, in which case that hash is the +one carried in the origin chain's leaf +(TODO Support metadata hash verification for wrapped faucets +([#2586](https://github.com/0xMiden/protocol/issues/2586))). + Conversion metadata (origin address, origin network, scale, and metadata hash) is *not* stored on the faucet; it is carried by the `CONFIG_AGG_BRIDGE` note at registration time and written directly into the bridge's `faucet_metadata_map`. The metadata hash is diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index 6507e7f54a..8d642b3440 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -66,9 +66,7 @@ pub struct ConversionMetadata { /// locks into it); `false` for bridge-owned faucets (bridge-in mints via the faucet, /// bridge-out burns via the faucet). pub is_native: bool, - /// keccak256 hash of the ABI-encoded token metadata (`name`, `symbol`, `decimals`) as it - /// exists on the origin chain, which is not necessarily what the faucet stores. See SPEC - /// section 7.1. + /// keccak256 hash of the ABI-encoded token metadata (`name`, `symbol`, `decimals`). pub metadata_hash: MetadataHash, } diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index 6f0b371727..f8127d54b9 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -22,15 +22,8 @@ static FEE_MANAGER_ROLE: LazyLock = /// /// An AggLayer faucet is a [`FungibleFaucet`](miden_standards::account::faucets::FungibleFaucet) /// owned by the bridge. This type is a stateless namespace for the settings the bridge and the -/// faucet operator agree on when one is deployed: the note allowlist -/// ([`AggLayerFaucet::allowed_notes`]), the RBAC roles -/// ([`AggLayerFaucet::procedure_roles`], [`AggLayerFaucet::fee_manager_role`]) and the account -/// builder ([`AggLayerFaucet::account_builder`](crate::AggLayerFaucet::account_builder), defined -/// alongside the bridge's). -/// -/// To inspect a deployed faucet, decode its account with -/// [`FungibleFaucet::try_from`](miden_standards::account::faucets::FungibleFaucet) for the token -/// metadata and [`Ownable2Step`](miden_standards::account::access::Ownable2Step) for the owner. +/// faucet operator agree on when one is deployed: the note allowlist, the RBAC roles, and the +/// account builder. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AggLayerFaucet; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 9f2753f148..033e31d9ee 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -166,10 +166,6 @@ impl AggLayerFaucet { /// and decimals. Conversion metadata (origin address, origin network, scale, metadata hash) /// lives on the bridge and is written there at registration time. /// - /// The AggLayer metadata hash preimage is the origin token's `(name, symbol, decimals)`. - /// A faucet built here reproduces it only when it is itself the origin token, i.e. when it is - /// registered as Miden-native; see SPEC section 7.1. - /// /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager` /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`] /// owner, which is what the `owner_only` mint and burn policies gate on. `fee_policy` must @@ -178,8 +174,9 @@ impl AggLayerFaucet { /// /// # Panics /// - /// Panics if `decimals` exceeds [`FungibleFaucet::MAX_DECIMALS`], or if `initial_supply` - /// exceeds `max_supply`. + /// Panics if: + /// - `decimals` exceeds [`FungibleFaucet::MAX_DECIMALS`]; + /// - `initial_supply` exceeds `max_supply`. #[allow(clippy::too_many_arguments)] pub fn account_builder( seed: Word, diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index b2b86f6b7a..a3b01c35e1 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -16,10 +16,6 @@ use super::test_utils::{ }; /// An agglayer faucet is a standard [`FungibleFaucet`] owned by the bridge. -/// -/// This pins the two properties the bridge depends on: the faucet exposes the standard fungible -/// faucet interface with its token metadata intact - including the real token *name*, which the -/// AggLayer metadata hash is computed over - and its `Ownable2Step` owner is the bridge account. #[test] fn agglayer_faucet_is_a_bridge_owned_fungible_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); From 7e73708b39c5266049649c5627be78fa11a71287 Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 28 Aug 2026 12:33:44 +0200 Subject: [PATCH 11/11] Apply suggestion from @mmagician --- crates/miden-testing/tests/agglayer/bridge_out.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 166b536b21..f1cc1ea4a9 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1125,8 +1125,6 @@ async fn bridge_out_lock_native_token() -> anyhow::Result<()> { .expect("valid eth address"); let origin_network = 7u32; // any stable u32 — Miden's test network id let scale = 0u8; - // This faucet is registered as Miden-native, so it *is* the origin token and its own stored - // metadata is the hash preimage. Read it back rather than repeating the builder's defaults. let native_metadata = FungibleFaucet::try_from(&native_faucet)?; let metadata_hash = MetadataHash::from_token_info( native_metadata.token_name().as_str(),