diff --git a/CHANGELOG.md b/CHANGELOG.md index ad096d1117..375c135b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## v0.15.0 (TBD) ### Features + +- Added lock/unlock path for Miden-native tokens in the AggLayer bridge: `is_native` flag in `faucet_registry_map`, bridge-local `faucet_metadata_map` (replacing FPI to faucets for conversion data), and `lock_asset` / `unlock_and_send` procedures so the bridge holds native assets in its own vault instead of burn/mint via a faucet ([#2771](https://github.com/0xMiden/protocol/pull/2771)). ### Changes - Added validation of leaf type on CLAIM note processing to prevent message leaves from being processed as asset claims ([#2730](https://github.com/0xMiden/protocol/pull/2730)). diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs index 58a980c35a..d8f066dbbf 100644 --- a/bin/bench-transaction/src/context_setups.rs +++ b/bin/bench-transaction/src/context_setups.rs @@ -5,6 +5,7 @@ use miden_agglayer::{ B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, MetadataHash, UpdateGerNote, @@ -206,10 +207,6 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result Result Result) -> Result `[1, 0, 0, 0]`. -2. Hashes the `(origin_token_address, origin_network)` pair using Poseidon2 and writes - the mapping into the `token_registry_map`: - `hash(origin_token_addr, origin_network)` -> `[0, 0, faucet_id_suffix, faucet_id_prefix]`. +operator creates a `CONFIG_AGG_BRIDGE` note carrying the faucet's account ID, origin token +address, origin network, scale, metadata hash, and the `is_native` flag, then sends it to +the bridge. On consumption, the note script calls `bridge_config::register_faucet` (plus +`store_faucet_metadata_hash` for the metadata hash — split into two calls because the +16-element MASM stack cannot fit all 18 registration felts at once). These procedures +perform the following writes: + +1. `faucet_registry_map`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` → `[1, is_native, 0, 0]`. +2. `faucet_metadata_map`: origin-address + origin-network + scale under sub-keys + `[0, 0, fid_s, fid_p]` and `[1, 0, fid_s, fid_p]`; metadata hash (lo/hi) under + `[2, 0, fid_s, fid_p]` and `[3, 0, fid_s, fid_p]`. +3. `token_registry_map`: `Poseidon2(origin_token_addr, origin_network)` → `[0, 0, fid_s, fid_p]`. + Keying on the pair (not the address alone) matches Solidity's `tokenInfoHash` and + prevents same-address cross-network collisions. The token registry enables the bridge to resolve which Miden-side faucet corresponds to a given origin asset during CLAIM note processing. When the bridge processes a [`CLAIM`](#42-claim) note, it reads the origin token address and origin network from the leaf data and calls `bridge_config::lookup_faucet_by_token_address` to find the registered -faucet. This lookup hashes the `(origin_token_address, origin_network)` pair with Poseidon2 -and retrieves the faucet ID from the token registry map. If the pair is not registered, the -`CLAIM` note consumption will fail. - -This means that the bridge admin must register the faucet on the Miden side before the corresponding tokens can be bridged in. - -The bridge admin is a trusted role, and is the sole entity that can register faucets on the Miden side (due to the caller restriction on [`bridge_config::register_faucet`](#bridge_configregister_faucet)). - -### 7.2 Bridging-out: How Miden-native tokens are registered on other chains - -When an asset is bridged out from Miden, [`bridge_out::bridge_out`](#bridge_outbridge_out) constructs a leaf for -the Local Exit Tree. The leaf includes the metadata hash, which the bridge fetches from -the faucet via FPI (`agglayer_faucet::get_metadata_hash`), as well as the other leaf data fields, including origin network and origin token address. +faucet. If the `(origin_token_address, origin_network)` pair is not registered, the `CLAIM` +note consumption will fail. + +The bridge admin is a trusted role, and is the sole entity that can register faucets on +the Miden side (enforced by the caller restriction on +[`bridge_config::register_faucet`](#bridge_configregister_faucet)). + +#### Wrapped (`is_native = false`) vs Miden-native (`is_native = true`) faucets + +The difference between the two kinds is what they represent and how the bridge dispatches +operations against them — *not* how they are registered. + +- **Wrapped faucets** represent a foreign ERC20 token bridged into Miden. The bridge holds + mint/burn authority for the faucet, so a bridge-in CLAIM emits a `MINT` note that the + faucet consumes to mint the wrapped asset, and a bridge-out B2AGG emits a `BURN` note that + the faucet consumes to destroy it. For these faucets, `origin_token_address` is the + foreign EVM token address. +- **Miden-native faucets** represent a Miden-native fungible asset that is being made + bridgeable. The bridge does *not* own the faucet and cannot mint or burn through it, so it + uses lock/unlock semantics instead: bridge-out adds the asset to the bridge's own vault + (`lock_asset`), and bridge-in claims the same token by removing the asset from the vault + and emitting a `P2ID` note directly (`unlock_and_send`). For these faucets, + `origin_token_address` is the faucet's own `AccountId` in the [Embedded + Format](#62-embedded-format), and `origin_network` is Miden's own network ID. + +In both cases the bridge admin drives registration via the same `CONFIG_AGG_BRIDGE` note; +the bridge admin is responsible for setting `is_native` correctly for the faucet at hand. + +### 7.2 Bridging-out: How tokens are registered on other chains + +When an asset is bridged out from Miden, [`bridge_out::bridge_out`](#bridge_outbridge_out) +constructs a leaf for the Local Exit Tree. The metadata hash, origin token address, origin +network, and scale factor are all read from the bridge's local `faucet_metadata_map` +(`bridge_config::get_faucet_conversion_info` and `bridge_config::get_faucet_metadata_hash`). +No FPI into the faucet is required — the bridge is fully self-contained for conversion data. On the EVM destination chain, when a user claims the bridged asset via `PolygonZkEVMBridgeV2.claimAsset()`, the wrapped token is deployed lazily on first claim. @@ -1066,22 +1135,35 @@ as a parameter to `claimAsset()`. The EVM bridge verifies that no wrapped token exists yet, the bridge deploys a new `TokenWrapped` ERC20 using the decoded name, symbol, and decimals from the metadata bytes. -#### Miden-native faucets - -A Miden-native faucet uses the same storage -layout and registration flow as a wrapped faucet. The key difference is what values are -stored in the conversion metadata: +For Miden-native faucets, the registered metadata uses: - `origin_token_address`: the faucet's own `AccountId` as per the [Embedded Format](#62-embedded-format). - `origin_network`: Miden's network ID as assigned by AggLayer (currently unassigned). -- `metadata_hash`: `keccak256(abi.encode(name, symbol, decimals))` - same as for wrapped +- `metadata_hash`: `keccak256(abi.encode(name, symbol, decimals))` — same as for wrapped faucets. -On the EVM side, `claimAsset()` sees `originNetwork != networkID` (foreign asset), so it -follows the wrapped token path: computes +On the EVM side, `claimAsset()` sees `originNetwork != networkID` (foreign asset) for a +Miden-native token, so it follows the wrapped token path: computes `tokenInfoHash = keccak256(abi.encodePacked(originNetwork, originTokenAddress))`, and deploys a new `TokenWrapped` ERC20 via `CREATE2` on first claim, minting on subsequent -claims. The `CREATE2` salt is `tokenInfoHash`, so the wrapper address is deterministic -from the `(originNetwork, originTokenAddress)` pair. The metadata bytes provided by the -claimer (which must hash to the leaf's `metadataHash`) are used to initialize the wrapped -token's name, symbol, and decimals. +claims. + +### 7.3 Native vs non-native paths on the Miden side + +The `is_native` flag recorded in `faucet_registry_map` splits the bridge's own Miden-side +behavior on both directions: + +| Direction | `is_native = false` (wrapped / foreign) | `is_native = true` (Miden-native) | +| ------------ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Bridge-out | `bridge_out::create_burn_note` — emits a BURN note consumed by the faucet. | `bridge_out::lock_asset` — `native_account::add_asset` locks the asset in the bridge vault. No BURN note is emitted. | +| Bridge-in | `bridge_in_output::build_mint_output_note` — emits a MINT note consumed by the faucet. | `bridge_in_output::unlock_and_send` — `native_account::remove_asset` unlocks from the vault, then emits a P2ID note directly to the recipient. No MINT note is emitted. | + +The LET leaf is constructed identically in both bridge-out branches. The native branch +does not require the bridge to be the faucet's owner, and `ownable2step::assert_sender_is_owner` +is not invoked on the native path. The P2ID note emitted by `unlock_and_send` uses the +`PROOF_DATA_KEY` as its serial number, which makes the note commitment deterministic for +a given claim and prevents double-spend within the same claim. + +This mirrors `PolygonZkEVMBridgeV2.claimAsset()`'s handling of +`originNetwork == networkID`: the EVM bridge transfers native tokens from / to its own +balance instead of minting / burning them via the token contract. diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 96bfad4d9c..7dabbd470b 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -3,6 +3,7 @@ use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note use miden::protocol::native_account +use agglayer::common::utils # ERRORS # ================================================================================================= @@ -22,6 +23,7 @@ const GER_MANAGER_SLOT = word("agglayer::bridge::ger_manager_account_id") const GER_MAP_STORAGE_SLOT = word("agglayer::bridge::ger_map") const FAUCET_REGISTRY_MAP_SLOT = word("agglayer::bridge::faucet_registry_map") const TOKEN_REGISTRY_MAP_SLOT = word("agglayer::bridge::token_registry_map") +const FAUCET_METADATA_MAP_SLOT = word("agglayer::bridge::faucet_metadata_map") # Flags const GER_KNOWN_FLAG = 1 @@ -30,6 +32,21 @@ const IS_FAUCET_REGISTERED_FLAG = 1 # Offset in the local memory of the `hash_token_address` procedure const TOKEN_ADDR_HASH_PTR = 0 +# Local memory slot offsets used inside the `register_faucet` procedure +const REG_TOKEN_HASH_LOC = 0 +const REG_FAUCET_ID_SUFFIX_LOC = 4 +const REG_FAUCET_ID_PREFIX_LOC = 5 +const REG_SCALE_LOC = 6 +const REG_ORIGIN_NETWORK_LOC = 7 +const REG_IS_NATIVE_LOC = 8 + +# faucet_metadata_map sub-keys (used as the first element of the 4-felt KEY). +# Each sub-key indexes a different word of metadata for a given faucet ID. +const FAUCET_METADATA_SUBKEY_ADDR_LO = 0 # [addr0, addr1, addr2, addr3] +const FAUCET_METADATA_SUBKEY_ADDR_HI = 1 # [addr4, origin_network, scale, 0] +const FAUCET_METADATA_SUBKEY_HASH_LO = 2 # METADATA_HASH_LO[4] +const FAUCET_METADATA_SUBKEY_HASH_HI = 3 # METADATA_HASH_HI[4] + # PUBLIC INTERFACE # ================================================================================================= @@ -102,71 +119,161 @@ proc assert_valid_ger # => [] end -#! Registers a faucet in the bridge's faucet registry and token registry. +#! Registers a faucet in the bridge's faucet registry, token registry, and metadata map. +#! +#! Stores conversion metadata for the faucet using a sub-key scheme in faucet_metadata_map: +#! 1. KEY [0, 0, faucet_id_suffix, faucet_id_prefix] -> [addr0, addr1, addr2, addr3] (origin address part 1) +#! 2. KEY [1, 0, faucet_id_suffix, faucet_id_prefix] -> [addr4, origin_network, scale, 0] (origin address part 2) #! -#! 1. Writes `KEY -> [1, 0, 0, 0]` into the `faucet_registry` map, where -#! `KEY = [0, 0, faucet_id_suffix, faucet_id_prefix]`. -#! 2. Writes `hash(tokenAddress[5] || origin_network) -> [faucet_id_suffix, faucet_id_prefix, 0, 0]` -#! into the `token_registry` map. The (origin_network, origin_token_address) pair is the -#! canonical asset identity; keying on the address alone would collide across networks. +#! Also registers: +#! 3. faucet_registry_map: [0, 0, faucet_id_suffix, faucet_id_prefix] -> [1, is_native, 0, 0] +#! 4. token_registry_map: hash(tokenAddress[5] || origin_network) -> [0, 0, faucet_id_suffix, faucet_id_prefix]. +#! The (origin_network, origin_token_address) pair is the canonical asset identity; +#! keying on the address alone would collide across networks. #! -#! Inputs: [origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix, pad(8)] +#! Inputs: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] #! Outputs: [pad(16)] #! #! Panics if: #! - the note sender is not the bridge admin. #! #! Invocation: call +@locals(14) pub proc register_faucet - # assert the note sender is the bridge admin. exec.assert_sender_is_bridge_admin - # => [origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix, pad(8)] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] + + # Save non-address data to locals. + movup.5 loc_store.REG_FAUCET_ID_SUFFIX_LOC + movup.5 loc_store.REG_FAUCET_ID_PREFIX_LOC + movup.5 loc_store.REG_SCALE_LOC + movup.5 loc_store.REG_ORIGIN_NETWORK_LOC + movup.5 loc_store.REG_IS_NATIVE_LOC + # => [addr0, addr1, addr2, addr3, addr4, pad(11)] + + # Duplicate the 5-felt address for hashing before it gets consumed. + repeat.5 + dup.4 + end + # => [addr0, addr1, addr2, addr3, addr4, addr0, addr1, addr2, addr3, addr4, pad(11)] + + # Push origin_network to position 5 so hash_token_address sees [addr(5), origin_network]. + # Per the agglayer #2860 fix, the token registry is keyed on the (address, network) pair so + # that the same EVM address on two different chains resolves to two distinct faucets. + # + # Byte-swap origin_network before hashing so the felt matches the leaf-side representation + # used by `lookup_faucet_by_token_address` (`claim_note.rs` LE-packs the leaf's + # origin_network felt). `faucet_metadata_map` keeps origin_network in raw form, so this swap + # is a hash-input-only conversion. + loc_load.REG_ORIGIN_NETWORK_LOC exec.utils::swap_u32_bytes movdn.5 + # => [addr0, addr1, addr2, addr3, addr4, origin_network_swapped, addr0, addr1, addr2, addr3, addr4, pad(11)] - # Save faucet ID for later use in token_registry - dup.7 dup.7 - # => [faucet_id_suffix, faucet_id_prefix, origin_token_addr(5), origin_network, - # faucet_id_suffix, faucet_id_prefix, pad(8)] + exec.hash_token_address + # => [TOKEN_ADDR_HASH, addr0, addr1, addr2, addr3, addr4, pad(11)] - # --- 1. Register faucet in faucet_registry --- + loc_storew_le.REG_TOKEN_HASH_LOC dropw + # => [addr0, addr1, addr2, addr3, addr4, pad(11)] - # set_map_item expects [slot_id(2), KEY, VALUE] and returns [OLD_VALUE]. - # Build KEY = [0, 0, suffix, prefix] and VALUE = [IS_FAUCET_REGISTERED_FLAG, 0, 0, 0] - push.0.0.0.IS_FAUCET_REGISTERED_FLAG - # => [IS_FAUCET_REGISTERED_FLAG, 0, 0, 0, - # faucet_id_suffix, faucet_id_prefix, origin_token_addr(5), origin_network, - # faucet_id_suffix, faucet_id_prefix, pad(8)] + # --- Step 1: Store origin address part 1 in faucet_metadata_map --- + # KEY = [0, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [addr0, addr1, addr2, addr3] - movup.5 movup.5 push.0.0 - # => [ - # [0, 0, faucet_id_suffix, faucet_id_prefix], - # [IS_FAUCET_REGISTERED_FLAG, 0, 0, 0], - # origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix, pad(8) - # ] + loc_load.REG_FAUCET_ID_PREFIX_LOC loc_load.REG_FAUCET_ID_SUFFIX_LOC + push.0.FAUCET_METADATA_SUBKEY_ADDR_LO + # => [SUBKEY_ADDR_LO, 0, faucet_id_suffix, faucet_id_prefix, addr0, addr1, addr2, addr3, addr4, pad(11)] - push.FAUCET_REGISTRY_MAP_SLOT[0..2] + push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item - # => [OLD_VALUE, origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix, pad(8)] + dropw + # => [addr4, pad(15)] + + # --- Step 2: Store origin address part 2 + origin_network + scale --- + # KEY = [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [addr4, origin_network, scale, 0] + + push.0 + loc_load.REG_SCALE_LOC + loc_load.REG_ORIGIN_NETWORK_LOC + movup.3 + # => [addr4, origin_network, scale, 0, pad(15)] + loc_load.REG_FAUCET_ID_PREFIX_LOC loc_load.REG_FAUCET_ID_SUFFIX_LOC + # => [faucet_id_suffix, faucet_id_prefix, addr4, origin_network, scale, 0, pad(15)] + + push.0.FAUCET_METADATA_SUBKEY_ADDR_HI + # => [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix, addr4, origin_network, scale, 0, pad(15)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item dropw - # => [origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix, pad(8)] + # => [pad(16)] - # --- 2. Register (token address, origin network) → faucet ID in token_registry --- + # --- Step 3: Store [1, is_native, 0, 0] in faucet_registry_map --- + # KEY = [0, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [1, is_native, 0, 0] + # The trailing [0, 0] of VALUE is supplied by the stack's bottom pads. - # Hash the (token address, origin network) pair - exec.hash_token_address - # => [TOKEN_ADDR_HASH, faucet_id_suffix, faucet_id_prefix, pad(10)] + loc_load.REG_IS_NATIVE_LOC push.IS_FAUCET_REGISTERED_FLAG + # => [1, is_native, pad(16)] - # Build VALUE = [0, 0, faucet_id_suffix, faucet_id_prefix] - movup.5 movup.5 push.0.0 - # => [0, 0, faucet_id_suffix, faucet_id_prefix, TOKEN_ADDR_HASH, pad(8)] + loc_load.REG_FAUCET_ID_PREFIX_LOC loc_load.REG_FAUCET_ID_SUFFIX_LOC push.0.0 + # => [0, 0, faucet_id_suffix, faucet_id_prefix, 1, is_native, pad(16)] - swapw - # => [TOKEN_ADDR_HASH, 0, 0, faucet_id_suffix, faucet_id_prefix, pad(8)] + push.FAUCET_REGISTRY_MAP_SLOT[0..2] + exec.native_account::set_map_item + dropw + # => [pad(16)] + + # --- Step 4: Store TOKEN_ADDR_HASH -> [0, 0, faucet_id_suffix, faucet_id_prefix] in token_registry --- + + loc_load.REG_FAUCET_ID_PREFIX_LOC loc_load.REG_FAUCET_ID_SUFFIX_LOC + # => [faucet_id_suffix, faucet_id_prefix, pad(16)] + + push.0.0 + # => [0, 0, faucet_id_suffix, faucet_id_prefix, pad(16)] + + padw loc_loadw_le.REG_TOKEN_HASH_LOC + # => [TOKEN_ADDR_HASH, 0, 0, faucet_id_suffix, faucet_id_prefix, pad(16)] push.TOKEN_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item - # => [OLD_VALUE, pad(12)] + dropw + # => [pad(16)] +end + +#! Stores the metadata hash for a registered faucet in the bridge's faucet metadata map. +#! +#! This is the second call in the faucet registration flow (called after register_faucet). +#! Stores the metadata hash using sub-keys 2 and 3 in faucet_metadata_map: +#! - KEY [2, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_LO +#! - KEY [3, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_HI +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix, METADATA_HASH_LO, METADATA_HASH_HI, pad(6)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the bridge admin. +#! +#! Invocation: call +pub proc store_faucet_metadata_hash + exec.assert_sender_is_bridge_admin + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] + # --- Store METADATA_HASH_LO at key [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] --- + dup.1 dup.1 swapw movup.5 movup.5 + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, faucet_id_suffix, faucet_id_prefix, MH_HI, pad(6)] + + push.0.FAUCET_METADATA_SUBKEY_HASH_LO + # => [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix, MH_LO, faucet_id_suffix, faucet_id_prefix, MH_HI, pad(6)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item + dropw + # => [faucet_id_suffix, faucet_id_prefix, MH_HI, pad(6)] + + # --- Store METADATA_HASH_HI at key [SUBKEY_HASH_HI, 0, faucet_id_suffix, faucet_id_prefix] --- + push.0.FAUCET_METADATA_SUBKEY_HASH_HI + # => [SUBKEY_HASH_HI, 0, faucet_id_suffix, faucet_id_prefix, MH_HI, pad(6)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item dropw # => [pad(16)] end @@ -174,6 +281,7 @@ end #! Asserts that a faucet is registered in the bridge's faucet registry. #! #! Looks up the faucet ID in the faucet registry map and asserts the registration flag is set. +#! The stored value is [is_registered, is_native, 0, 0]. #! #! Inputs: [faucet_id_suffix, faucet_id_prefix] #! Outputs: [] @@ -191,11 +299,130 @@ proc assert_faucet_registered exec.active_account::get_map_item # => [VALUE] - # the stored word must be [1, 0, 0, 0] for registered faucets + # the stored word is [1, is_native, 0, 0] for registered faucets + # assert element 0 (registration flag) equals "1" assert.err=ERR_FAUCET_NOT_REGISTERED drop drop drop # => [] end +#! Returns the scale factor for a registered faucet from the bridge's faucet metadata map. +#! +#! Reads the metadata from the faucet_metadata_map at key [1, 0, faucet_id_suffix, faucet_id_prefix]. +#! The stored value is [addr4, origin_network, scale, 0]. +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Outputs: [scale] +#! +#! Invocation: exec +proc get_faucet_scale + # Build KEY = [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix] + push.0.FAUCET_METADATA_SUBKEY_ADDR_HI + # => [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [addr4, origin_network, scale, 0] + + drop drop swap drop + # => [scale] +end + +#! Returns the origin token address (5 felts), origin network, and scale factor for a registered +#! faucet from the bridge's faucet metadata map. +#! +#! Reads sub-keys 0 and 1 from faucet_metadata_map: +#! - Key [0, 0, faucet_id_suffix, faucet_id_prefix] -> [addr0, addr1, addr2, addr3] +#! - Key [1, 0, faucet_id_suffix, faucet_id_prefix] -> [addr4, origin_network, scale, 0] +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Outputs: [origin_addr(5), origin_network, scale] +#! +#! Invocation: exec +proc get_faucet_conversion_info + # Prepare the SUBKEY_ADDR_LO KEY underneath. + push.0.FAUCET_METADATA_SUBKEY_ADDR_LO + # => [SUBKEY_ADDR_LO, 0, faucet_id_suffix, faucet_id_prefix] + + # Prepare the SUBKEY_ADDR_HI KEY on top. + dup.3 dup.3 push.0.FAUCET_METADATA_SUBKEY_ADDR_HI + # => [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix, SUBKEY_ADDR_LO, 0, faucet_id_suffix, faucet_id_prefix] + + # Read sub-key 1: [addr4, origin_network, scale, 0] + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [addr4, origin_network, scale, 0, 0, 0, faucet_id_suffix, faucet_id_prefix] + + # Surface the pre-built sub-key 0 KEY for the second read. + swapw + # => [0, 0, faucet_id_suffix, faucet_id_prefix, addr4, origin_network, scale, 0] + + # Read sub-key 0: [addr0, addr1, addr2, addr3] + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [addr0, addr1, addr2, addr3, addr4, origin_network, scale, 0] + + # Drop the trailing 0 left over from sub-key 1. + movup.7 drop + # => [addr0, addr1, addr2, addr3, addr4, origin_network, scale] +end + +#! Returns the metadata hash (8 u32 felts) for a registered faucet from the bridge's faucet +#! metadata map. +#! +#! Reads sub-keys 2 and 3 from faucet_metadata_map: +#! - Key [2, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_LO +#! - Key [3, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_HI +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Outputs: [METADATA_HASH_LO, METADATA_HASH_HI] +#! +#! Invocation: exec +proc get_faucet_metadata_hash + # Prepare the SUBKEY_HASH_LO KEY underneath. + push.0.FAUCET_METADATA_SUBKEY_HASH_LO + # => [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] + + # Prepare the SUBKEY_HASH_HI KEY on top. + dup.3 dup.3 push.0.FAUCET_METADATA_SUBKEY_HASH_HI + # => [SUBKEY_HASH_HI, 0, faucet_id_suffix, faucet_id_prefix, SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] + + # Read sub-key HASH_HI: METADATA_HASH_HI + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [MH_HI, SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] + + # Surface the pre-built sub-key HASH_LO KEY for the second read. + swapw + # => [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix, MH_HI] + + # Read sub-key HASH_LO: METADATA_HASH_LO + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [MH_LO, MH_HI] +end + +#! Returns whether a faucet is native (not owned by the bridge). +#! +#! Reads the faucet_registry_map value [1, is_native, 0, 0] and returns the is_native flag. +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Outputs: [is_native] +#! +#! Invocation: exec +proc is_faucet_native + # Build KEY = [0, 0, faucet_id_suffix, faucet_id_prefix] + push.0.0 + # => [0, 0, faucet_id_suffix, faucet_id_prefix] + + push.FAUCET_REGISTRY_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [1, is_native, 0, 0] + + # Drop element 0 (registration flag), move is_native past the two trailing zeros, drop them. + drop movdn.2 drop drop + # => [is_native] +end + #! Looks up the faucet account ID for a given (origin_token_address, origin_network) pair. #! #! Hashes the (origin_token_address, origin_network) pair and looks up the result in the diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index f514b2a4d9..c0e1c1ab42 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -1,24 +1,16 @@ use agglayer::bridge::bridge_config +use agglayer::bridge::bridge_in_output use agglayer::bridge::leaf_utils use agglayer::common::constants::MIDEN_NETWORK_ID use agglayer::common::utils use agglayer::common::asset_conversion use agglayer::common::eth_address -use agglayer::faucet -> agglayer_faucet use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::poseidon2 use miden::core::mem use miden::core::word -use miden::protocol::note -use miden::protocol::output_note -use miden::protocol::output_note::ATTACHMENT_KIND_NONE use miden::protocol::active_account use miden::protocol::native_account -use miden::protocol::tx -use miden::standards::note_tag -use miden::standards::note_tag::DEFAULT_TAG -use miden::standards::attachments::network_account_target -use miden::standards::note::execution_hint::ALWAYS use miden::protocol::types::DoubleWord use miden::protocol::types::MemoryAddress @@ -61,24 +53,6 @@ const CGI_CHAIN_HASH_HI_SLOT_NAME = word("agglayer::bridge::cgi_chain_hash_hi") const CLAIM_PROOF_DATA_WORD_LEN = 134 const CLAIM_LEAF_DATA_WORD_LEN = 8 -# MINT note storage layout (public mode, 18 felts total): -# - tag [0] : 1 felt -# - amount [1] : 1 felt -# - attachment_kind [2] : 1 felt -# - attachment_scheme [3] : 1 felt -# - ATTACHMENT [4..7] : 4 felts -# - P2ID_SCRIPT_ROOT [8..11] : 4 felts -# - SERIAL_NUM [12..15] : 4 felts -# - account_id_suffix [16] : 1 felt -# - account_id_prefix [17] : 1 felt -const MINT_NOTE_NUM_STORAGE_ITEMS = 18 - -# P2ID output note constants -const OUTPUT_NOTE_TYPE_PUBLIC = 1 - -# P2ID attachment constants (the P2ID note created by the faucet has no attachment) -const P2ID_ATTACHMENT_SCHEME_NONE = 0 - # Global memory pointers # ------------------------------------------------------------------------------------------------- @@ -105,10 +79,10 @@ const LEAF_DATA_START_PTR = 0 # Memory pointers for piped advice map data (used by claim procedure) const CLAIM_PROOF_DATA_START_PTR = 0 const CLAIM_LEAF_DATA_START_PTR = 536 -const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 +pub const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 # Memory addresses for stored keys (used by claim procedure) -const CLAIM_PROOF_DATA_KEY_MEM_ADDR = 700 +pub const CLAIM_PROOF_DATA_KEY_MEM_ADDR = 700 const CLAIM_LEAF_DATA_KEY_MEM_ADDR = 704 # Memory addresses used to temporarily store leaf_index and source_bridge_network @@ -143,18 +117,6 @@ const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = CLAIM_LEAF_DATA_START_PTR + 18 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = CLAIM_LEAF_DATA_START_PTR + 19 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = CLAIM_LEAF_DATA_START_PTR + 20 -# Memory addresses for MINT note output construction -const MINT_NOTE_STORAGE_MEM_ADDR_0 = 800 -const MINT_NOTE_STORAGE_DEST_TAG = 800 -const MINT_NOTE_STORAGE_NATIVE_AMOUNT = 801 -const MINT_NOTE_STORAGE_ATTACHMENT_KIND = 802 -const MINT_NOTE_STORAGE_ATTACHMENT_SCHEME = 803 -const MINT_NOTE_STORAGE_ATTACHMENT = 804 -const MINT_NOTE_STORAGE_OUTPUT_SCRIPT_ROOT = 808 -const MINT_NOTE_STORAGE_OUTPUT_SERIAL_NUM = 812 -const MINT_NOTE_STORAGE_OUTPUT_NOTE_SUFFIX = 816 -const MINT_NOTE_STORAGE_OUTPUT_NOTE_PREFIX = 817 - # Local memory offsets # ------------------------------------------------------------------------------------------------- @@ -260,13 +222,25 @@ pub proc claim # Verify faucet_mint_amount matches the leaf data amount exec.verify_claim_amount # => [faucet_id_suffix, faucet_id_prefix, pad(16)] - - # Build MINT output note targeting the AggLayer faucet - loc_load.CLAIM_DEST_ID_PREFIX_LOCAL loc_load.CLAIM_DEST_ID_SUFFIX_LOCAL - # => [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix, pad(16)] - exec.build_mint_output_note - # => [pad(16)] + # Branch on is_native: native faucets unlock from the bridge vault and emit a P2ID note + # directly to the recipient; non-native faucets go through the standard MINT path. + dup.1 dup.1 exec.bridge_config::is_faucet_native + # => [is_native, faucet_id_suffix, faucet_id_prefix, pad(16)] + + if.true + loc_load.CLAIM_DEST_ID_PREFIX_LOCAL loc_load.CLAIM_DEST_ID_SUFFIX_LOCAL + # => [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix, pad(16)] + + exec.bridge_in_output::unlock_and_send + # => [pad(16)] + else + loc_load.CLAIM_DEST_ID_PREFIX_LOCAL loc_load.CLAIM_DEST_ID_SUFFIX_LOCAL + # => [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix, pad(16)] + + exec.bridge_in_output::build_mint_output_note + # => [pad(16)] + end end # HELPER PROCEDURES @@ -472,7 +446,7 @@ end #! by the faucet's scale factor. #! #! This procedure: -#! 1. Performs an FPI call to the faucet's `get_scale` procedure to retrieve the scale factor. +#! 1. Reads the scale factor from the bridge's faucet_metadata_map. #! 2. Loads the raw U256 amount from the leaf data in memory. #! 3. Calls `verify_u256_to_native_amount_conversion` to assert that #! `faucet_mint_amount == floor(raw_amount / 10^scale)`. @@ -481,44 +455,26 @@ end #! Outputs: [] #! #! Panics if: -#! - the FPI call to the faucet's get_scale fails. #! - the faucet_mint_amount does not match the expected scaled-down value. #! #! Invocation: exec proc verify_claim_amount - # Step 1: Pad the stack explicitly for FPI call (get_scale takes no inputs) - padw padw - movup.9 movup.9 - padw padw - movup.9 movup.9 - # => [faucet_id_suffix, faucet_id_prefix, pad(16)] - - # Step 2: FPI call to faucet's get_scale procedure - procref.agglayer_faucet::get_scale - # => [PROC_MAST_ROOT(4), faucet_id_suffix, faucet_id_prefix, pad(16)] - - movup.5 movup.5 - # => [faucet_id_suffix, faucet_id_prefix, PROC_MAST_ROOT(4), pad(16)] - - exec.tx::execute_foreign_procedure - # => [scale, pad(15)] - - # Clean up FPI output padding, keeping only scale - movdn.15 dropw dropw dropw drop drop drop + # Step 1: Read scale from bridge's faucet_metadata_map + exec.bridge_config::get_faucet_scale # => [scale] - # Step 3: Load the raw U256 amount from leaf data memory + # Step 2: Load the raw U256 amount from leaf data memory exec.load_raw_claim_amount # => [x7, x6, x5, x4, x3, x2, x1, x0, scale] - # Step 4: Load faucet_mint_amount (y) and position it for verification + # Step 3: Load faucet_mint_amount (y) and position it for verification mem_load.CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT # => [y, x7, x6, x5, x4, x3, x2, x1, x0, scale] movdn.9 # => [x7, x6, x5, x4, x3, x2, x1, x0, scale, y] - # Step 5: Verify that y = floor(x / 10^scale) + # Step 4: Verify that y = floor(x / 10^scale) exec.asset_conversion::verify_u256_to_native_amount_conversion # => [] end @@ -835,182 +791,6 @@ proc load_origin_token_address_and_network # => [origin_token_addr(5), origin_network] end -#! Builds a PUBLIC MINT output note targeting the AggLayer Faucet. -#! -#! The MINT note uses public mode (18 storage items) so the AggLayer Faucet creates a PUBLIC P2ID -#! note on consumption. This procedure orchestrates three steps: -#! 1. Write all 18 MINT note storage items to global memory. -#! 2. Build the MINT note recipient digest from the storage. -#! 3. Create the output note, and set the attachment. -#! -#! Inputs: [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix] -#! Outputs: [] -#! -#! Invocation: exec -proc build_mint_output_note - # Step 1: Write all 18 MINT note storage items to global memory - exec.write_mint_note_storage - # => [faucet_id_suffix, faucet_id_prefix] - - # Step 2: Build the MINT note recipient digest - exec.build_mint_recipient - # => [MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] - - # Step 3: Create the output note and set the faucet attachment - exec.create_mint_note_with_attachment - # => [] -end - -#! Writes all 18 MINT note storage items to global memory. -#! -#! Storage layout: -#! - [0]: tag (note tag for the P2ID output note, targeting the destination account) -#! - [1]: amount (the scaled-down Miden amount to mint) -#! - [2]: attachment_kind (0 = no attachment) -#! - [3]: attachment_scheme (0 = no attachment) -#! - [4-7]: ATTACHMENT ([0, 0, 0, 0]) -#! - [8-11]: P2ID_SCRIPT_ROOT (script root of the P2ID note) -#! - [12-15]: SERIAL_NUM (serial number for the P2ID note, derived from PROOF_DATA_KEY) -#! - [16]: account_id_suffix (destination account suffix) -#! - [17]: account_id_prefix (destination account prefix) -#! -#! Inputs: [destination_id_suffix, destination_id_prefix] -#! Outputs: [] -#! -#! Invocation: exec -proc write_mint_note_storage - # Write P2ID storage items first (before prefix is consumed): [16..17] - # Write destination_id_suffix [16] - dup mem_store.MINT_NOTE_STORAGE_OUTPUT_NOTE_SUFFIX - # => [destination_id_suffix, destination_id_prefix] - - # Write destination_id_prefix [17] - dup.1 mem_store.MINT_NOTE_STORAGE_OUTPUT_NOTE_PREFIX - # => [destination_id_suffix, destination_id_prefix] - - drop - # => [destination_id_prefix] - - # Get the native amount from the pre-computed miden_claim_amount - mem_load.CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT - # => [native_amount, destination_id_prefix] - - # Compute the note tag for the destination account (consumes prefix) - swap - # => [destination_id_prefix, native_amount] - - exec.note_tag::create_account_target - # => [dest_tag, native_amount] - - # Write tag to MINT note storage [0] - mem_store.MINT_NOTE_STORAGE_DEST_TAG - # => [native_amount] - - # Write amount to MINT note storage [1] - mem_store.MINT_NOTE_STORAGE_NATIVE_AMOUNT - # => [] - - # Write P2ID attachment fields (the P2ID note has no attachment) - # attachment_kind = NONE [2] - push.ATTACHMENT_KIND_NONE mem_store.MINT_NOTE_STORAGE_ATTACHMENT_KIND - # => [] - - # attachment_scheme = NONE [3] - push.P2ID_ATTACHMENT_SCHEME_NONE mem_store.MINT_NOTE_STORAGE_ATTACHMENT_SCHEME - # => [] - - # ATTACHMENT = empty word [4..7] - padw mem_storew_le.MINT_NOTE_STORAGE_ATTACHMENT dropw - # => [] - - # Write P2ID_SCRIPT_ROOT to MINT note storage [8..11] - procref.::miden::standards::notes::p2id::main - # => [P2ID_SCRIPT_ROOT] - - mem_storew_le.MINT_NOTE_STORAGE_OUTPUT_SCRIPT_ROOT dropw - # => [] - - # Write SERIAL_NUM (PROOF_DATA_KEY) to MINT note storage [12..15] - mem_loadw_be.CLAIM_PROOF_DATA_KEY_MEM_ADDR - # => [SERIAL_NUM] - - mem_storew_le.MINT_NOTE_STORAGE_OUTPUT_SERIAL_NUM dropw - # => [] -end - -#! Builds the MINT note recipient digest from the storage items already written to global memory. -#! -#! Uses the MINT note script root and PROOF_DATA_KEY as serial number, then calls -#! `note::build_recipient` with the storage pointer and item count. -#! -#! Inputs: [] -#! Outputs: [MINT_RECIPIENT] -#! -#! Invocation: exec -proc build_mint_recipient - # Get the MINT note script root - procref.::miden::standards::notes::mint::main - # => [MINT_SCRIPT_ROOT] - - # Generate a serial number for the MINT note (use PROOF_DATA_KEY) - padw mem_loadw_be.CLAIM_PROOF_DATA_KEY_MEM_ADDR - # => [MINT_SERIAL_NUM, MINT_SCRIPT_ROOT] - - # Build the MINT note recipient - push.MINT_NOTE_NUM_STORAGE_ITEMS - # => [num_storage_items, MINT_SERIAL_NUM, MINT_SCRIPT_ROOT] - - push.MINT_NOTE_STORAGE_MEM_ADDR_0 - # => [storage_ptr, num_storage_items, MINT_SERIAL_NUM, MINT_SCRIPT_ROOT] - - exec.note::build_recipient - # => [MINT_RECIPIENT] -end - -#! Creates the MINT output note and sets the NetworkAccountTarget attachment on it. -#! -#! Creates a public output note with no assets, and sets the attachment so only the target faucet -#! can consume the note. -#! -#! Inputs: [MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] -#! Outputs: [] -#! -#! Invocation: exec -proc create_mint_note_with_attachment - # Create the MINT output note targeting the faucet - push.OUTPUT_NOTE_TYPE_PUBLIC - # => [note_type, MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] - - # Set tag to DEFAULT - push.DEFAULT_TAG - # => [tag, note_type, MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] - - # Create the output note (no assets - MINT notes carry no assets) - exec.output_note::create - # => [note_idx, faucet_id_suffix, faucet_id_prefix] - - movdn.2 - # => [faucet_id_suffix, faucet_id_prefix, note_idx] - - # Set the attachment on the MINT note to target the faucet account - # NetworkAccountTarget attachment: targets the faucet so only it can consume the note - # network_account_target::new expects [suffix, prefix, exec_hint] - # and returns [attachment_scheme, attachment_kind, ATTACHMENT] - push.ALWAYS # exec_hint = ALWAYS - movdn.2 - # => [faucet_id_suffix, faucet_id_prefix, exec_hint, note_idx] - - exec.network_account_target::new - # => [attachment_scheme, attachment_kind, ATTACHMENT, note_idx] - - # Rearrange for set_attachment: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] - movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT(4)] - - exec.output_note::set_attachment - # => [] -end - #! Computes the root of the SMT based on the provided Merkle path, leaf value and leaf index. #! #! Inputs: [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx] diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm new file mode 100644 index 0000000000..4985c866c6 --- /dev/null +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm @@ -0,0 +1,315 @@ +use agglayer::bridge::bridge_in::CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT +use agglayer::bridge::bridge_in::CLAIM_PROOF_DATA_KEY_MEM_ADDR +use miden::protocol::asset +use miden::protocol::native_account +use miden::protocol::note +use miden::protocol::output_note +use miden::protocol::output_note::ATTACHMENT_KIND_NONE +use miden::standards::note_tag +use miden::standards::note_tag::DEFAULT_TAG +use miden::standards::notes::p2id +use miden::standards::attachments::network_account_target +use miden::standards::note::execution_hint::ALWAYS + +# CONSTANTS +# ================================================================================================= + +# MINT note storage layout (public mode, 18 felts total): +# - tag [0] : 1 felt +# - amount [1] : 1 felt +# - attachment_kind [2] : 1 felt +# - attachment_scheme [3] : 1 felt +# - ATTACHMENT [4..7] : 4 felts +# - P2ID_SCRIPT_ROOT [8..11] : 4 felts +# - SERIAL_NUM [12..15] : 4 felts +# - account_id_suffix [16] : 1 felt +# - account_id_prefix [17] : 1 felt +const MINT_NOTE_NUM_STORAGE_ITEMS = 18 + +# P2ID output note constants +const OUTPUT_NOTE_TYPE_PUBLIC = 1 + +# P2ID attachment constants (the P2ID note created by the faucet has no attachment) +const P2ID_ATTACHMENT_SCHEME_NONE = 0 + +# Memory addresses for MINT note output construction +const MINT_NOTE_STORAGE_MEM_ADDR_0 = 800 +const MINT_NOTE_STORAGE_DEST_TAG = 800 +const MINT_NOTE_STORAGE_NATIVE_AMOUNT = 801 +const MINT_NOTE_STORAGE_ATTACHMENT_KIND = 802 +const MINT_NOTE_STORAGE_ATTACHMENT_SCHEME = 803 +const MINT_NOTE_STORAGE_ATTACHMENT = 804 +const MINT_NOTE_STORAGE_OUTPUT_SCRIPT_ROOT = 808 +const MINT_NOTE_STORAGE_OUTPUT_SERIAL_NUM = 812 +const MINT_NOTE_STORAGE_OUTPUT_NOTE_SUFFIX = 816 +const MINT_NOTE_STORAGE_OUTPUT_NOTE_PREFIX = 817 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Builds a PUBLIC MINT output note targeting the AggLayer Faucet. +#! +#! The MINT note uses public mode (18 storage items) so the AggLayer Faucet creates a PUBLIC P2ID +#! note on consumption. This procedure orchestrates three steps: +#! 1. Write all 18 MINT note storage items to global memory. +#! 2. Build the MINT note recipient digest from the storage. +#! 3. Create the output note, and set the attachment. +#! +#! Inputs: [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix] +#! Outputs: [] +#! +#! Invocation: exec +pub proc build_mint_output_note + # Step 1: Write all 18 MINT note storage items to global memory + exec.write_mint_note_storage + # => [faucet_id_suffix, faucet_id_prefix] + + # Step 2: Build the MINT note recipient digest + exec.build_mint_recipient + # => [MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] + + # Step 3: Create the output note and set the faucet attachment + exec.create_mint_note_with_attachment + # => [] +end + +# Offsets in the local memory of the `unlock_and_send` procedure +const UNLOCK_ASSET_KEY_LOC = 0 +const UNLOCK_ASSET_VALUE_LOC = 4 +const UNLOCK_DEST_SUFFIX_LOC = 8 +const UNLOCK_DEST_PREFIX_LOC = 9 + +#! Removes the fungible asset for the claim from the bridge's vault and creates a PUBLIC P2ID +#! output note targeted at the destination account. +#! +#! Used on the bridge-in claim path for Miden-native faucets (ones whose mint authority the bridge +#! does not hold). Instead of creating a MINT note for the faucet, the asset is removed from the +#! bridge's own vault (where it was placed by a prior `lock_asset` on the bridge-out side) and +#! attached to a new P2ID note. The P2ID serial number is derived from `CLAIM_PROOF_DATA_KEY` +#! (matching the MINT path's serial-number choice) so the resulting note commitment is +#! deterministic across runs. +#! +#! Replay safety does not rely on serial-number uniqueness. A replayed claim is rejected earlier +#! in `bridge_in::claim` by the nullifier check (`assert_claim_not_spent`), so `unlock_and_send` +#! only runs once per (leaf_index, source_bridge_network) pair even though its serial number is +#! deterministic. +#! +#! Inputs: [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix] +#! Outputs: [] +#! +#! Invocation: exec +@locals(10) +pub proc unlock_and_send + # Stash destination to locals so we can reload it later, after `native_account::remove_asset` + # has consumed the stack copy. + loc_store.UNLOCK_DEST_SUFFIX_LOC loc_store.UNLOCK_DEST_PREFIX_LOC + # => [faucet_id_suffix, faucet_id_prefix] + + # Build the fungible asset (ASSET_KEY, ASSET_VALUE) from the faucet id and the pre-computed + # Miden claim amount. `asset::create_fungible_asset` is pure MASM (no FPI). + mem_load.CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT movdn.2 + # => [faucet_id_suffix, faucet_id_prefix, amount] + + push.0 # enable_callbacks = 0 + # => [0, faucet_id_suffix, faucet_id_prefix, amount] + + exec.asset::create_fungible_asset + # => [ASSET_KEY, ASSET_VALUE] + + # Stash the asset to locals so we can re-use it for `output_note::add_asset` after + # `native_account::remove_asset` consumes its stack copy. + dupw.1 loc_storew_le.UNLOCK_ASSET_VALUE_LOC dropw + dupw loc_storew_le.UNLOCK_ASSET_KEY_LOC dropw + # => [ASSET_KEY, ASSET_VALUE] + + # Remove the asset from the bridge's vault. Panics if the vault does not contain enough of + # the asset, which is the desired failure mode for an invalid / double-spent claim. + exec.native_account::remove_asset + # => [REMAINING_ASSET_VALUE] + + dropw + # => [] + + # Build p2id::new's input [dest_suffix, dest_prefix, tag, note_type, SERIAL_NUM] from the + # bottom up. + padw mem_loadw_be.CLAIM_PROOF_DATA_KEY_MEM_ADDR + # => [SERIAL_NUM] + + push.OUTPUT_NOTE_TYPE_PUBLIC + # => [note_type, SERIAL_NUM] + + loc_load.UNLOCK_DEST_PREFIX_LOC + # => [dest_prefix, note_type, SERIAL_NUM] + + exec.note_tag::create_account_target + # => [dest_tag, note_type, SERIAL_NUM] + + loc_load.UNLOCK_DEST_PREFIX_LOC loc_load.UNLOCK_DEST_SUFFIX_LOC + # => [dest_suffix, dest_prefix, dest_tag, note_type, SERIAL_NUM] + + exec.p2id::new + # => [note_idx] + + # Reload the asset from locals and attach it to the newly created P2ID note. + padw loc_loadw_le.UNLOCK_ASSET_VALUE_LOC + # => [ASSET_VALUE, note_idx] + + padw loc_loadw_le.UNLOCK_ASSET_KEY_LOC + # => [ASSET_KEY, ASSET_VALUE, note_idx] + + exec.output_note::add_asset + # => [] +end + +# MINT-PATH HELPERS (used only by build_mint_output_note) +# ================================================================================================= + +#! Writes all 18 MINT note storage items to global memory. +#! +#! Storage layout: +#! - [0]: tag (note tag for the P2ID output note, targeting the destination account) +#! - [1]: amount (the scaled-down Miden amount to mint) +#! - [2]: attachment_kind (0 = no attachment) +#! - [3]: attachment_scheme (0 = no attachment) +#! - [4-7]: ATTACHMENT ([0, 0, 0, 0]) +#! - [8-11]: P2ID_SCRIPT_ROOT (script root of the P2ID note) +#! - [12-15]: SERIAL_NUM (serial number for the P2ID note, derived from PROOF_DATA_KEY) +#! - [16]: account_id_suffix (destination account suffix) +#! - [17]: account_id_prefix (destination account prefix) +#! +#! Inputs: [destination_id_suffix, destination_id_prefix] +#! Outputs: [] +#! +#! Invocation: exec +proc write_mint_note_storage + # Write P2ID storage items first (before prefix is consumed): [16..17] + # Write destination_id_suffix [16] + dup mem_store.MINT_NOTE_STORAGE_OUTPUT_NOTE_SUFFIX + # => [destination_id_suffix, destination_id_prefix] + + # Write destination_id_prefix [17] + dup.1 mem_store.MINT_NOTE_STORAGE_OUTPUT_NOTE_PREFIX + # => [destination_id_suffix, destination_id_prefix] + + drop + # => [destination_id_prefix] + + # Get the native amount from the pre-computed miden_claim_amount + mem_load.CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT + # => [native_amount, destination_id_prefix] + + # Compute the note tag for the destination account (consumes prefix) + swap + # => [destination_id_prefix, native_amount] + + exec.note_tag::create_account_target + # => [dest_tag, native_amount] + + # Write tag to MINT note storage [0] + mem_store.MINT_NOTE_STORAGE_DEST_TAG + # => [native_amount] + + # Write amount to MINT note storage [1] + mem_store.MINT_NOTE_STORAGE_NATIVE_AMOUNT + # => [] + + # Write P2ID attachment fields (the P2ID note has no attachment) + # attachment_kind = NONE [2] + push.ATTACHMENT_KIND_NONE mem_store.MINT_NOTE_STORAGE_ATTACHMENT_KIND + # => [] + + # attachment_scheme = NONE [3] + push.P2ID_ATTACHMENT_SCHEME_NONE mem_store.MINT_NOTE_STORAGE_ATTACHMENT_SCHEME + # => [] + + # ATTACHMENT = empty word [4..7] + padw mem_storew_le.MINT_NOTE_STORAGE_ATTACHMENT dropw + # => [] + + # Write P2ID_SCRIPT_ROOT to MINT note storage [8..11] + procref.::miden::standards::notes::p2id::main + # => [P2ID_SCRIPT_ROOT] + + mem_storew_le.MINT_NOTE_STORAGE_OUTPUT_SCRIPT_ROOT dropw + # => [] + + # Write SERIAL_NUM (PROOF_DATA_KEY) to MINT note storage [12..15] + mem_loadw_be.CLAIM_PROOF_DATA_KEY_MEM_ADDR + # => [SERIAL_NUM] + + mem_storew_le.MINT_NOTE_STORAGE_OUTPUT_SERIAL_NUM dropw + # => [] +end + +#! Builds the MINT note recipient digest from the storage items already written to global memory. +#! +#! Uses the MINT note script root and PROOF_DATA_KEY as serial number, then calls +#! `note::build_recipient` with the storage pointer and item count. +#! +#! Inputs: [] +#! Outputs: [MINT_RECIPIENT] +#! +#! Invocation: exec +proc build_mint_recipient + # Get the MINT note script root + procref.::miden::standards::notes::mint::main + # => [MINT_SCRIPT_ROOT] + + # Generate a serial number for the MINT note (use PROOF_DATA_KEY) + padw mem_loadw_be.CLAIM_PROOF_DATA_KEY_MEM_ADDR + # => [MINT_SERIAL_NUM, MINT_SCRIPT_ROOT] + + # Build the MINT note recipient + push.MINT_NOTE_NUM_STORAGE_ITEMS + # => [num_storage_items, MINT_SERIAL_NUM, MINT_SCRIPT_ROOT] + + push.MINT_NOTE_STORAGE_MEM_ADDR_0 + # => [storage_ptr, num_storage_items, MINT_SERIAL_NUM, MINT_SCRIPT_ROOT] + + exec.note::build_recipient + # => [MINT_RECIPIENT] +end + +#! Creates the MINT output note and sets the NetworkAccountTarget attachment on it. +#! +#! Creates a public output note with no assets, and sets the attachment so only the target faucet +#! can consume the note. +#! +#! Inputs: [MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] +#! Outputs: [] +#! +#! Invocation: exec +proc create_mint_note_with_attachment + # Create the MINT output note targeting the faucet + push.OUTPUT_NOTE_TYPE_PUBLIC + # => [note_type, MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] + + # Set tag to DEFAULT + push.DEFAULT_TAG + # => [tag, note_type, MINT_RECIPIENT, faucet_id_suffix, faucet_id_prefix] + + # Create the output note (no assets - MINT notes carry no assets) + exec.output_note::create + # => [note_idx, faucet_id_suffix, faucet_id_prefix] + + movdn.2 + # => [faucet_id_suffix, faucet_id_prefix, note_idx] + + # Set the attachment on the MINT note to target the faucet account + # NetworkAccountTarget attachment: targets the faucet so only it can consume the note + # network_account_target::new expects [suffix, prefix, exec_hint] + # and returns [attachment_scheme, attachment_kind, ATTACHMENT] + push.ALWAYS # exec_hint = ALWAYS + movdn.2 + # => [faucet_id_suffix, faucet_id_prefix, exec_hint, note_idx] + + exec.network_account_target::new + # => [attachment_scheme, attachment_kind, ATTACHMENT, note_idx] + + # Rearrange for set_attachment: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + movup.6 + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] + + exec.output_note::set_attachment + # => [] +end diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 9ec2b64d6c..c0bcff19af 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -3,7 +3,6 @@ use miden::protocol::active_account use miden::protocol::asset use miden::protocol::native_account use miden::protocol::note -use miden::protocol::tx use miden::standards::data_structures::double_word_array use miden::standards::attachments::network_account_target use miden::standards::note_tag::DEFAULT_TAG @@ -13,7 +12,7 @@ use miden::protocol::output_note use miden::core::crypto::hashes::poseidon2 use agglayer::common::constants::MIDEN_NETWORK_ID use agglayer::common::utils -use agglayer::faucet -> agglayer_faucet +use agglayer::common::asset_conversion use agglayer::bridge::bridge_config use agglayer::bridge::leaf_utils use agglayer::bridge::merkle_tree_frontier @@ -71,6 +70,7 @@ const DESTINATION_ADDRESS_2_LOC=10 const DESTINATION_ADDRESS_3_LOC=11 const DESTINATION_ADDRESS_4_LOC=12 const DESTINATION_NETWORK_LOC=13 +const BRIDGE_OUT_IS_NATIVE_LOC=14 # create_burn_note memory locals const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 @@ -110,10 +110,8 @@ const BURN_NOTE_NUM_STORAGE_ITEMS=0 #! - destination network ID is Miden's AggLayer network ID. #! #! Invocation: call -@locals(14) +@locals(15) pub proc bridge_out - # => [ASSET_KEY, ASSET_VALUE, dest_network_id, dest_address(5), pad(2)] - # Save ASSET to local memory for later BURN note creation locaddr.BRIDGE_OUT_BURN_ASSET_LOC exec.asset::store @@ -177,27 +175,28 @@ pub proc bridge_out exec.write_address_to_memory # => [pad(16)] - # --- 3. Fetch metadata hash from the faucet via FPI and write to memory --- - procref.agglayer_faucet::get_metadata_hash - # => [PROC_MAST_ROOT, pad(16)] - - # Reload asset to extract faucet ID for the FPI call + # --- 3. Fetch metadata hash from bridge storage and write to memory --- + # Reload asset to extract faucet id for the metadata lookup and the is_native flag. locaddr.BRIDGE_OUT_BURN_ASSET_LOC exec.asset::load swapw dropw - # => [ASSET_KEY, PROC_MAST_ROOT, pad(16)] - # ASSET_KEY layout: [0, 0, faucet_id_suffix, faucet_id_prefix] + # => [ASSET_KEY, pad(16)] - # Extract faucet ID, drop padding and amount - drop drop - # => [faucet_id_suffix, faucet_id_prefix, PROC_MAST_ROOT, pad(16)] + exec.asset::key_into_faucet_id + # => [faucet_id_suffix, faucet_id_prefix, pad(16)] + + # Stash the is_native flag for the lock/burn branch later in this procedure. + dup.1 dup.1 + exec.bridge_config::is_faucet_native + loc_store.BRIDGE_OUT_IS_NATIVE_LOC + # => [faucet_id_suffix, faucet_id_prefix, pad(16)] - exec.tx::execute_foreign_procedure - # => [METADATA_HASH_LO, METADATA_HASH_HI, pad(8)] + exec.bridge_config::get_faucet_metadata_hash + # => [METADATA_HASH_LO, METADATA_HASH_HI, pad(16)] push.LEAF_DATA_START_PTR push.METADATA_HASH_OFFSET add movdn.8 - # => [METADATA_HASH_LO, METADATA_HASH_HI, metadata_hash_ptr, pad(8)] + # => [METADATA_HASH_LO, METADATA_HASH_HI, metadata_hash_ptr, pad(16)] exec.utils::mem_store_double_word_unaligned # => [pad(16)] @@ -222,13 +221,21 @@ pub proc bridge_out exec.add_leaf_bridge # => [pad(16)] - # --- 4. Create BURN output note for ASSET --- + # --- 5. Dispatch on is_native: lock into the bridge vault or burn via an output note --- locaddr.BRIDGE_OUT_BURN_ASSET_LOC exec.asset::load # => [ASSET_KEY, ASSET_VALUE, pad(16)] - - exec.create_burn_note - # => [pad(16)] + + loc_load.BRIDGE_OUT_IS_NATIVE_LOC + # => [is_native, ASSET_KEY, ASSET_VALUE, pad(16)] + + if.true + exec.lock_asset + # => [pad(16)] + else + exec.create_burn_note + # => [pad(16)] + end end # HELPER PROCEDURES @@ -254,9 +261,9 @@ proc assert_destination_id_not_miden_id # => [] end -#! Validates that a faucet is registered in the bridge's faucet registry, then performs an FPI call -#! to the faucet's `asset_to_origin_asset` procedure to obtain the scaled amount, origin token -#! address, and origin network. +#! Validates that a faucet is registered in the bridge's faucet registry and converts the asset's +#! native Miden amount to the origin (AggLayer-side) U256 amount using bridge-local conversion +#! metadata. #! #! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [AMOUNT_U256_LO, AMOUNT_U256_HI, origin_addr(5), origin_network] @@ -270,43 +277,38 @@ end #! #! Panics if: #! - The faucet is not registered in the faucet registry. -#! - The FPI call to asset_to_origin_asset fails. #! #! Invocation: exec proc convert_asset - # --- Step 1: Assert faucet is registered --- - # pad in preparation for FPI call - repeat.2 - padw padw swapdw - end - # => [ASSET_KEY, ASSET_VALUE, pad(16)] - swapw exec.asset::fungible_value_into_amount movdn.4 - # => [ASSET_KEY, amount, pad(16)] + # => [ASSET_KEY, amount] exec.asset::key_into_faucet_id - # => [faucet_id_suffix, faucet_id_prefix, amount, pad(16)] + # => [faucet_id_suffix, faucet_id_prefix, amount] dup.1 dup.1 exec.bridge_config::assert_faucet_registered - # => [faucet_id_suffix, faucet_id_prefix, amount, pad(16)] + # => [faucet_id_suffix, faucet_id_prefix, amount] - # --- Step 2: FPI to faucet's asset_to_origin_asset --- + # Fetch origin token address, origin network, and scale from bridge storage. + exec.bridge_config::get_faucet_conversion_info + # => [addr0, addr1, addr2, addr3, addr4, origin_network, scale, amount] - procref.agglayer_faucet::asset_to_origin_asset - # => [PROC_MAST_ROOT, faucet_id_suffix, faucet_id_prefix, amount, pad(16)] + # Bring [amount, scale] to the top for scale_native_amount_to_u256. + movup.6 + # => [scale, addr0, addr1, addr2, addr3, addr4, origin_network, amount] - # Move faucet_id above PROC_MAST_ROOT - movup.5 movup.5 - # => [faucet_id_suffix, faucet_id_prefix, PROC_MAST_ROOT, amount, pad(15), pad(1)] + movup.7 + # => [amount, scale, addr0, addr1, addr2, addr3, addr4, origin_network] - exec.tx::execute_foreign_procedure - # => [AMOUNT_U256_LO, AMOUNT_U256_HI, origin_addr(5), origin_network, pad(2), pad(1)] + exec.asset_conversion::scale_native_amount_to_u256 + exec.asset_conversion::reverse_limbs_and_change_byte_endianness + # => [U256_LO, U256_HI, addr0, addr1, addr2, addr3, addr4, origin_network] - # drop the 3 trailing padding elements - repeat.3 - movup.14 drop - end + # Byte-swap origin_network to match the EVM-side big-endian encoding used in the leaf layout. + movup.13 + exec.utils::swap_u32_bytes + movdn.13 # => [AMOUNT_U256_LO, AMOUNT_U256_HI, origin_addr(5), origin_network] end @@ -661,3 +663,21 @@ proc create_burn_note dropw dropw drop drop drop # => [] end + +#! Locks a fungible asset in the bridge's own vault. +#! +#! Used on the bridge-out path for Miden-native faucets (ones whose mint/burn authority the bridge +#! does not hold). Instead of creating a BURN note, the asset is simply added to the bridge's own +#! vault; the bridge-in claim side will later remove it via `unlock_and_send`. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [] +#! +#! Invocation: exec +proc lock_asset + exec.native_account::add_asset + # => [ASSET_VALUE'] + + dropw + # => [] +end diff --git a/crates/miden-agglayer/asm/agglayer/common/utils.masm b/crates/miden-agglayer/asm/agglayer/common/utils.masm index 45bed28405..48b7de6c3b 100644 --- a/crates/miden-agglayer/asm/agglayer/common/utils.masm +++ b/crates/miden-agglayer/asm/agglayer/common/utils.masm @@ -90,3 +90,30 @@ proc mem_load_double_word(mem_ptr: MemoryAddress) -> DoubleWord padw movup.8 mem_loadw_le # => [WORD_1, WORD_2] end + +#! Loads two words from the provided unaligned (not a multiple of 4) memory address. +#! +#! Mirrors `mem_store_double_word_unaligned`: reads 8 consecutive felts via individual +#! `mem_load`s and stacks them as `[WORD_1, WORD_2]` (with WORD_1's first felt on top of +#! the stack, matching the order produced by the aligned `mem_load_double_word`). +#! +#! Inputs: [ptr] +#! Outputs: [WORD_1, WORD_2] +pub proc mem_load_double_word_unaligned(mem_ptr: MemoryAddress) -> DoubleWord + # Load WORD_2 first so it ends up underneath WORD_1 on the final stack. + dup add.7 mem_load + dup.1 add.6 mem_load + dup.2 add.5 mem_load + dup.3 add.4 mem_load + # => [w2_0, w2_1, w2_2, w2_3, ptr] + + # Load WORD_1 on top. + dup.4 add.3 mem_load + dup.5 add.2 mem_load + dup.6 add.1 mem_load + dup.7 mem_load + # => [w1_0, w1_1, w1_2, w1_3, w2_0, w2_1, w2_2, w2_3, ptr] + + movup.8 drop + # => [WORD_1, WORD_2] +end diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index f442a646e6..4c3e59a79c 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -1,146 +1,6 @@ -use miden::core::sys -use agglayer::common::asset_conversion -use miden::protocol::active_account - -# CONSTANTS -# ================================================================================================= - -# Storage slots for conversion metadata. -# Slot 1: [addr_felt0, addr_felt1, addr_felt2, addr_felt3] — first 4 felts of origin token address -const CONVERSION_INFO_1_SLOT = word("agglayer::faucet::conversion_info_1") -# Slot 2: [addr_felt4, origin_network, scale, 0] — remaining address felt + origin network + scale -const CONVERSION_INFO_2_SLOT = word("agglayer::faucet::conversion_info_2") - -# Storage slots for the pre-computed metadata hash (keccak256 of ABI-encoded token metadata). -# The 32-byte hash is split across two value slots, each holding 4 u32 felts. -const METADATA_HASH_LO_SLOT = word("agglayer::faucet::metadata_hash_lo") -const METADATA_HASH_HI_SLOT = word("agglayer::faucet::metadata_hash_hi") - # PUBLIC INTERFACE # ================================================================================================= -#! Returns the origin token address (5 felts) and origin network identifier from faucet conversion -#! storage. -#! -#! Reads conversion_info_1 (first 4 felts of address) and conversion_info_2 (5th felt + origin -#! network) from storage in a single pass. -#! -#! Inputs: [] -#! Outputs: [addr0, addr1, addr2, addr3, addr4, origin_network] -#! -#! Invocation: exec -proc get_origin_token_address_and_network - push.CONVERSION_INFO_1_SLOT[0..2] - exec.active_account::get_item - # => [addr0, addr1, addr2, addr3] - - push.CONVERSION_INFO_2_SLOT[0..2] - exec.active_account::get_item - # => [addr4, origin_network, scale, 0, addr0, addr1, addr2, addr3] - - movdn.7 movdn.7 drop drop - # => [addr0, addr1, addr2, addr3, addr4, origin_network] -end - -#! Returns the scale factor from faucet conversion storage. -#! -#! Inputs: [] -#! Outputs: [scale] -#! -#! Invocation: exec -proc get_scale_inner - push.CONVERSION_INFO_2_SLOT[0..2] - exec.active_account::get_item - # => [addr4, origin_network, scale, 0] - - drop drop swap drop - # => [scale] -end - -#! Returns the pre-computed metadata hash (8 u32 felts) from faucet storage. -#! -#! The metadata hash is `keccak256(abi.encode(name, symbol, decimals))` and is stored across two -#! value slots (lo and hi, 4 felts each). -#! -#! Inputs: [pad(16)] -#! Outputs: [METADATA_HASH_LO(4), METADATA_HASH_HI(4), pad(8)] -#! -#! Invocation: call -pub proc get_metadata_hash - push.METADATA_HASH_LO_SLOT[0..2] - exec.active_account::get_item - # => [lo0, lo1, lo2, lo3, pad(16)] - - push.METADATA_HASH_HI_SLOT[0..2] - exec.active_account::get_item - # => [hi0, hi1, hi2, hi3, lo0, lo1, lo2, lo3, pad(16)] - - # Rearrange: move hi below lo - swapw - # => [lo0, lo1, lo2, lo3, hi0, hi1, hi2, hi3, pad(16)] - - # Drop 8 excess padding elements (24 -> 16) - swapdw dropw dropw - # => [METADATA_HASH_LO(4), METADATA_HASH_HI(4), pad(8)] -end - -#! Returns the scale factor from faucet conversion storage. -#! -#! Called via FPI from the bridge account. -#! -#! Inputs: [pad(16)] -#! Outputs: [scale, pad(15)] -#! -#! Invocation: call -pub proc get_scale - exec.get_scale_inner - # => [scale, pad(16)] - - swap drop - # => [scale, pad(15)] -end - -#! Converts a native Miden asset amount to origin asset data using the stored conversion metadata -#! (origin_token_address, origin_network, and scale). -#! -#! This procedure is intended to be called via FPI from the bridge account. -#! It reads the faucet's conversion metadata from storage, scales the native amount to U256 format, -#! and returns the result along with origin token address and network. -#! -#! Inputs: [amount, pad(15)] -#! Outputs: [AMOUNT_U256_LO, AMOUNT_U256_HI, addr0, addr1, addr2, addr3, addr4, origin_network, pad(2)] -#! -#! Where: -#! - amount: The native Miden asset amount -#! - AMOUNT_U256: The scaled amount as 8 u32 limbs (little-endian U256) -#! - addr0..addr4: Origin token address (5 felts, u32 limbs) -#! - origin_network: Origin network identifier -#! -#! Invocation: call -pub proc asset_to_origin_asset - # => [amount, pad(15)] - - # Step 1: Get scale from storage - exec.get_scale_inner swap - # => [amount, scale, pad(15)] - - # Step 2: Scale amount to U256 - exec.asset_conversion::scale_native_amount_to_u256 - exec.asset_conversion::reverse_limbs_and_change_byte_endianness - # => [U256_LO, U256_HI, pad(15)] - - # Step 3: Get origin token address and network - exec.get_origin_token_address_and_network - # => [addr0, addr1, addr2, addr3, addr4, origin_network, U256_LO, U256_HI, pad(15)] - - # Move address + network below the U256 amount - repeat.6 movdn.13 end - # => [U256_LO, U256_HI, addr0, addr1, addr2, addr3, addr4, origin_network, pad(15)] - - exec.sys::truncate_stack - # => [U256_LO, U256_HI, addr0, addr1, addr2, addr3, addr4, origin_network, pad(2)] -end - #! Burns the fungible asset from the active note. #! #! This procedure retrieves the asset from the active note and burns it. The note must contain diff --git a/crates/miden-agglayer/asm/components/bridge.masm b/crates/miden-agglayer/asm/components/bridge.masm index 4c38d5a019..14c5169f93 100644 --- a/crates/miden-agglayer/asm/components/bridge.masm +++ b/crates/miden-agglayer/asm/components/bridge.masm @@ -4,11 +4,13 @@ # # The bridge exposes: # - `register_faucet` from the bridge_config module +# - `store_faucet_metadata_hash` from the bridge_config module # - `update_ger` from the bridge_config module # - `claim` for bridge-in # - `bridge_out` for bridge-out pub use ::agglayer::bridge::bridge_config::register_faucet +pub use ::agglayer::bridge::bridge_config::store_faucet_metadata_hash pub use ::agglayer::bridge::bridge_config::update_ger pub use ::agglayer::bridge::bridge_in::claim pub use ::agglayer::bridge::bridge_out::bridge_out diff --git a/crates/miden-agglayer/asm/components/faucet.masm b/crates/miden-agglayer/asm/components/faucet.masm index 71927c63d9..874c490bc0 100644 --- a/crates/miden-agglayer/asm/components/faucet.masm +++ b/crates/miden-agglayer/asm/components/faucet.masm @@ -5,13 +5,7 @@ # The faucet exposes: # - `mint_and_send` from the network fungible faucet (for MINT note consumption, with owner # verification) -# - `asset_to_origin_asset` for bridge-out FPI -# - `get_metadata_hash` for bridge-out FPI (metadata hash retrieval) -# - `get_scale` for bridge-in FPI (amount verification) # - `burn` for bridge-out pub use ::agglayer::faucet::mint_and_send -pub use ::agglayer::faucet::asset_to_origin_asset -pub use ::agglayer::faucet::get_metadata_hash -pub use ::agglayer::faucet::get_scale pub use ::agglayer::faucet::burn diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm index ba9bff7138..1324b31af7 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -1,55 +1,78 @@ use agglayer::bridge::bridge_config +use agglayer::common::utils use miden::protocol::active_note use miden::standards::attachments::network_account_target # CONSTANTS # ================================================================================================= -const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 8 +const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 18 const STORAGE_START_PTR = 0 -const ORIGIN_TOKEN_ADDR_0 = STORAGE_START_PTR + +const ORIGIN_TOKEN_ADDR_0 = 0 +const ORIGIN_TOKEN_ADDR_1 = 1 +const ORIGIN_TOKEN_ADDR_2 = 2 +const ORIGIN_TOKEN_ADDR_3 = 3 const ORIGIN_TOKEN_ADDR_4 = 4 +const FAUCET_ID_SUFFIX = 5 +const FAUCET_ID_PREFIX = 6 +const SCALE = 7 +const ORIGIN_NETWORK = 8 +const IS_NATIVE = 9 +const METADATA_HASH_LO_0 = 10 +const METADATA_HASH_LO_1 = 11 +const METADATA_HASH_LO_2 = 12 +const METADATA_HASH_LO_3 = 13 +const METADATA_HASH_HI_0 = 14 +const METADATA_HASH_HI_1 = 15 +const METADATA_HASH_HI_2 = 16 +const METADATA_HASH_HI_3 = 17 # ERRORS # ================================================================================================= -const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS = "CONFIG_AGG_BRIDGE expects exactly 8 note storage items" +const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS = "CONFIG_AGG_BRIDGE expects exactly 18 note storage items" const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" -#! Agglayer Bridge CONFIG_AGG_BRIDGE script: registers a faucet in the bridge's faucet registry and -#! token registry. +#! Agglayer Bridge CONFIG_AGG_BRIDGE script: registers a faucet in the bridge's faucet registry, +#! token registry, and faucet metadata map. #! #! This note can only be consumed by the Agglayer Bridge account that is targeted by the note #! attachment, and only if the note was sent by the bridge admin. -#! Upon consumption, it registers the faucet ID and origin token address mapping in the bridge. +#! Upon consumption, it registers the faucet ID, origin token address mapping, scale factor, +#! origin network, is_native flag, and metadata hash in the bridge. +#! +#! The registration is split into two calls due to the 16-element stack limit: +#! 1. register_faucet: stores address, scale, origin_network, is_native, and registry entries +#! 2. store_faucet_metadata_hash: stores the metadata hash #! #! Requires that the account exposes: #! - agglayer::bridge_config::register_faucet procedure. +#! - agglayer::bridge_config::store_faucet_metadata_hash procedure. #! #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] #! -#! NoteStorage layout (8 felts total): -#! - origin_token_addr_0 [0] : 1 felt -#! - origin_token_addr_1 [1] : 1 felt -#! - origin_token_addr_2 [2] : 1 felt -#! - origin_token_addr_3 [3] : 1 felt -#! - origin_token_addr_4 [4] : 1 felt -#! - origin_network [5] : 1 felt -#! - faucet_id_suffix [6] : 1 felt -#! - faucet_id_prefix [7] : 1 felt -#! -#! Where: -#! - origin_network: Origin network identifier (LE-packed u32). Together with -#! `origin_token_addr` it forms the token registry key. -#! - faucet_id_suffix: Suffix felt of the faucet account ID to register. -#! - faucet_id_prefix: Prefix felt of the faucet account ID to register. +#! NoteStorage layout (18 felts total): +#! - origin_token_addr_0 [0] : 1 felt +#! - origin_token_addr_1 [1] : 1 felt +#! - origin_token_addr_2 [2] : 1 felt +#! - origin_token_addr_3 [3] : 1 felt +#! - origin_token_addr_4 [4] : 1 felt +#! - faucet_id_suffix [5] : 1 felt +#! - faucet_id_prefix [6] : 1 felt +#! - scale [7] : 1 felt +#! - origin_network [8] : 1 felt (LE-packed u32, paired with origin_token_addr in the +#! token-registry key per agglayer #2860) +#! - is_native [9] : 1 felt +#! - metadata_hash_lo [10] : 4 felts +#! - metadata_hash_hi [14] : 4 felts #! #! Panics if: #! - The note attachment target account does not match the consuming bridge account. -#! - The note does not contain exactly 8 storage items. -#! - The account does not expose the register_faucet procedure. +#! - The note does not contain exactly 18 storage items. +#! - The account does not expose the register_faucet or store_faucet_metadata_hash procedures. begin dropw # => [pad(16)] @@ -67,27 +90,50 @@ begin push.CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS assert_eq.err=ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS drop # => [pad(16)] - # Load origin_token_addr(5), origin_network, and faucet_id from memory. - # register_faucet expects: - # [origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix, pad(8)] - # - # Memory layout starting at STORAGE_START_PTR: - # [addr0, addr1, addr2, addr3, addr4, origin_network, faucet_id_suffix, faucet_id_prefix] - # so the word at ORIGIN_TOKEN_ADDR_4 covers exactly - # [addr4, origin_network, faucet_id_suffix, faucet_id_prefix]. - mem_loadw_le.ORIGIN_TOKEN_ADDR_4 - # => [addr4, origin_network, faucet_id_suffix, faucet_id_prefix, pad(12)] + # --- Call 1: register_faucet --- + # Expects: [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] - # Load remaining origin_token_addr_[0..3] onto the stack - padw mem_loadw_le.ORIGIN_TOKEN_ADDR_0 - # => [addr0, addr1, addr2, addr3, addr4, origin_network, faucet_id_suffix, faucet_id_prefix, pad(12)] + # Push the call's 6 trailing pad zeros first, then build the 10 args on top. + padw push.0.0 + # => [pad(6), pad(16)] - # Register the faucet in the bridge - # => [addr0, addr1, addr2, addr3, addr4, origin_network, faucet_id_suffix, faucet_id_prefix, pad(8), pad(4)] + mem_load.IS_NATIVE + mem_load.ORIGIN_NETWORK + mem_load.SCALE + mem_load.FAUCET_ID_PREFIX + mem_load.FAUCET_ID_SUFFIX + mem_load.ORIGIN_TOKEN_ADDR_4 + # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6), pad(16)] + + mem_load.ORIGIN_TOKEN_ADDR_3 + mem_load.ORIGIN_TOKEN_ADDR_2 + mem_load.ORIGIN_TOKEN_ADDR_1 + mem_load.ORIGIN_TOKEN_ADDR_0 + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6), pad(16)] call.bridge_config::register_faucet - # => [pad(16), pad(4)] + # => [pad(32)] - dropw + # --- Call 2: store_faucet_metadata_hash --- + # Expects: [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] + + # Push the call's 6 trailing pad zeros first, then build the 10 args on top. + padw push.0.0 + # => [pad(6), pad(32)] + + push.METADATA_HASH_LO_0 exec.utils::mem_load_double_word_unaligned + # => [MH_LO, MH_HI, pad(6), pad(32)] + + mem_load.FAUCET_ID_PREFIX + mem_load.FAUCET_ID_SUFFIX + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6), pad(32)] + + call.bridge_config::store_faucet_metadata_hash + # => [pad(48)] + + # Drop 32 to bring sdepth back to the 16-minimum. + repeat.8 + dropw + end # => [pad(16)] end diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 996c863992..ee7bf1da44 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -70,6 +70,10 @@ static TOKEN_REGISTRY_MAP_SLOT_NAME: LazyLock = LazyLock::new(| StorageSlotName::new("agglayer::bridge::token_registry_map") .expect("token registry map storage slot name should be valid") }); +static FAUCET_METADATA_MAP_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("agglayer::bridge::faucet_metadata_map") + .expect("faucet metadata map storage slot name should be valid") +}); // bridge in // ------------------------------------------------------------------------------------------------ @@ -125,6 +129,9 @@ static LET_NUM_LEAVES_SLOT_NAME: LazyLock = LazyLock::new(|| { /// - [`Self::ger_map_slot_name`]: Stores the GERs. /// - [`Self::faucet_registry_map_slot_name`]: Stores the faucet registry map. /// - [`Self::token_registry_map_slot_name`]: Stores the token address → faucet ID map. +/// - [`Self::faucet_metadata_map_slot_name`]: Stores conversion metadata (origin address, origin +/// network, scale, metadata hash) for all registered faucets, keyed by sub-key scheme based on +/// faucet ID. /// - [`Self::claim_nullifiers_slot_name`]: Stores the CLAIM note nullifiers map (RPO(leaf_index, /// source_bridge_network) → \[1, 0, 0, 0\]). /// - [`Self::cgi_chain_hash_lo_slot_name`]: Stores the lower 128 bits of the CGI chain hash. @@ -195,6 +202,14 @@ impl AggLayerBridge { &TOKEN_REGISTRY_MAP_SLOT_NAME } + /// Storage slot name for the faucet metadata map. + /// + /// This map stores conversion metadata (origin address, origin network, scale, metadata hash) + /// for all registered faucets, keyed by sub-key scheme based on faucet ID. + pub fn faucet_metadata_map_slot_name() -> &'static StorageSlotName { + &FAUCET_METADATA_MAP_SLOT_NAME + } + // --- bridge in -------- /// Storage slot name for the CLAIM note nullifiers map. @@ -421,6 +436,7 @@ impl AggLayerBridge { &*LET_NUM_LEAVES_SLOT_NAME, &*FAUCET_REGISTRY_MAP_SLOT_NAME, &*TOKEN_REGISTRY_MAP_SLOT_NAME, + &*FAUCET_METADATA_MAP_SLOT_NAME, &*BRIDGE_ADMIN_ID_SLOT_NAME, &*GER_MANAGER_ID_SLOT_NAME, &*CGI_CHAIN_HASH_LO_SLOT_NAME, @@ -443,6 +459,7 @@ impl From for AccountComponent { StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()), StorageSlot::with_empty_map(FAUCET_REGISTRY_MAP_SLOT_NAME.clone()), StorageSlot::with_empty_map(TOKEN_REGISTRY_MAP_SLOT_NAME.clone()), + StorageSlot::with_empty_map(FAUCET_METADATA_MAP_SLOT_NAME.clone()), StorageSlot::with_value(BRIDGE_ADMIN_ID_SLOT_NAME.clone(), bridge_admin_word), StorageSlot::with_value(GER_MANAGER_ID_SLOT_NAME.clone(), ger_manager_word), StorageSlot::with_value(CGI_CHAIN_HASH_LO_SLOT_NAME.clone(), Word::empty()), diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index 91c39b4c93..cfabee7ec7 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -28,7 +28,7 @@ use miden_protocol::vm::Program; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use miden_utils_sync::LazyLock; -use crate::EthAddress; +use crate::{EthAddress, MetadataHash}; // NOTE SCRIPT // ================================================================================================ @@ -42,13 +42,62 @@ static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock = LazyLock::new(|| { NoteScript::new(program) }); +// CONVERSION METADATA +// ================================================================================================ + +/// The conversion metadata registered on the bridge for a single faucet. +/// +/// Encapsulates the origin-chain identity and bridge-side policy of a faucet: the EVM token +/// address, network id, decimal scale, whether the faucet is Miden-native (lock/unlock) or +/// bridge-owned (burn/mint), and the keccak256 metadata hash. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConversionMetadata { + /// Account ID of the faucet being registered. + pub faucet_account_id: AccountId, + /// Origin EVM token address the faucet wraps. + pub origin_token_address: EthAddress, + /// Decimal scaling factor between the origin-chain unit and the Miden-side unit + /// (e.g. 0 for USDC, 8 for ETH). + pub scale: u8, + /// Origin network / chain ID the token lives on. + pub origin_network: u32, + /// `true` for Miden-native faucets (bridge-in unlocks from the bridge vault, bridge-out + /// 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`). + pub metadata_hash: MetadataHash, +} + +impl ConversionMetadata { + /// Serializes the metadata to the 18-felt layout consumed by `CONFIG_AGG_BRIDGE`. + /// + /// `origin_network` is written in raw u32 form (no byte swap). The bridge stores it as-is + /// in `faucet_metadata_map`; `bridge_out::convert_asset` later applies `swap_u32_bytes` to + /// produce the leaf-side representation. The token-registry side of registration applies + /// the matching swap inside `register_faucet`'s MASM before hashing, keeping the hash + /// byte-identical with the leaf-side `lookup_faucet_by_token_address` input. + pub fn to_elements(&self) -> Vec { + let mut v = Vec::with_capacity(ConfigAggBridgeNote::NUM_STORAGE_ITEMS); + v.extend(self.origin_token_address.to_elements()); + v.push(self.faucet_account_id.suffix()); + v.push(self.faucet_account_id.prefix().as_felt()); + v.push(Felt::from(self.scale)); + v.push(Felt::from(self.origin_network)); + v.push(Felt::from(u8::from(self.is_native))); + v.extend(self.metadata_hash.to_elements()); + v + } +} + // CONFIG_AGG_BRIDGE NOTE // ================================================================================================ /// CONFIG_AGG_BRIDGE note. /// -/// This note is used to register a faucet in the bridge's faucet and token registries. -/// It carries the origin token address and faucet account ID, and is always public. +/// This note is used to register a faucet in the bridge's faucet and token registries, +/// and to store full conversion metadata (origin address, origin network, scale, metadata hash) +/// in the bridge's faucet metadata map. pub struct ConfigAggBridgeNote; impl ConfigAggBridgeNote { @@ -56,8 +105,19 @@ impl ConfigAggBridgeNote { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for a CONFIG_AGG_BRIDGE note. - /// Layout: [origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix] - pub const NUM_STORAGE_ITEMS: usize = 8; + /// + /// Layout (18 felts): + /// - `[0..4]` origin_token_addr (5 felts) + /// - `[5]` faucet_id_suffix + /// - `[6]` faucet_id_prefix + /// - `[7]` scale + /// - `[8]` origin_network (raw u32; the MASM register flow byte-swaps it before hashing + /// into the token-registry key, and `bridge_out` byte-swaps it before placing it in the LET + /// leaf) + /// - `[9]` is_native (0 or 1) + /// - `[10..13]` METADATA_HASH_LO (4 felts) + /// - `[14..17]` METADATA_HASH_HI (4 felts) + pub const NUM_STORAGE_ITEMS: usize = 18; // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -77,41 +137,28 @@ impl ConfigAggBridgeNote { /// Creates a CONFIG_AGG_BRIDGE note to register a faucet in the bridge's registry. /// - /// The note storage contains 8 felts: - /// - `origin_token_addr[0..5]`: The 5 u32 felts of the origin EVM token address - /// - `origin_network`: The origin network identifier (LE-packed u32) - /// - `faucet_id_suffix`: The suffix of the faucet account ID - /// - `faucet_id_prefix`: The prefix of the faucet account ID - /// /// # Parameters - /// - `faucet_account_id`: The account ID of the faucet to register - /// - `origin_token_address`: The origin EVM token address for the token registry - /// - `origin_network`: The origin network identifier; together with `origin_token_address` it - /// forms the registry key - /// - `sender_account_id`: The account ID of the note creator - /// - `target_account_id`: The bridge account ID that will consume this note - /// - `rng`: Random number generator for creating the note serial number + /// - `metadata`: The conversion metadata to register for the faucet. + /// - `sender_account_id`: The account ID of the note creator. + /// - `target_account_id`: The bridge account ID that will consume this note. + /// - `rng`: Random number generator for creating the note serial number. /// /// # Errors /// Returns an error if note creation fails. pub fn create( - faucet_account_id: AccountId, - origin_token_address: &EthAddress, - origin_network: u32, + metadata: ConversionMetadata, sender_account_id: AccountId, target_account_id: AccountId, rng: &mut R, ) -> Result { - // Create note storage with 8 felts: - // [origin_token_addr(5), origin_network, faucet_id_suffix, faucet_id_prefix] - let addr_elements = origin_token_address.to_elements(); - let mut storage_values: Vec = addr_elements; - // Pack origin_network using the same byte order as LeafData::to_elements and the faucet's - // conversion slots, so the felt is byte-identical across config note, leaf, and faucet. - let origin_network_packed = u32::from_le_bytes(origin_network.to_be_bytes()); - storage_values.push(Felt::from(origin_network_packed)); - storage_values.push(faucet_account_id.suffix()); - storage_values.push(faucet_account_id.prefix().as_felt()); + let storage_values = metadata.to_elements(); + + debug_assert_eq!( + storage_values.len(), + Self::NUM_STORAGE_ITEMS, + "CONFIG_AGG_BRIDGE storage must have exactly {} felts", + Self::NUM_STORAGE_ITEMS + ); let note_storage = NoteStorage::new(storage_values)?; @@ -124,12 +171,56 @@ impl ConfigAggBridgeNote { NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) .map_err(|e| NoteError::other(e.to_string()))?, ); - let metadata = + let note_metadata = NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); // CONFIG_AGG_BRIDGE notes don't carry assets let assets = NoteAssets::new(vec![])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::new(assets, note_metadata, recipient)) + } +} + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; + + use super::*; + + /// Locks in the 18-felt wire layout of `CONFIG_AGG_BRIDGE` note storage. Any reordering in + /// `to_elements` would silently desync from the indices the MASM `CONFIG_AGG_BRIDGE` script + /// reads from (`ORIGIN_TOKEN_ADDR_0..4`, `FAUCET_ID_SUFFIX=5`, ... `METADATA_HASH_HI_3=17`). + #[test] + fn to_elements_layout_matches_masm_storage_indices() { + let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET) + .expect("valid faucet account id"); + let origin_token_address = + EthAddress::from_hex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let metadata_hash = MetadataHash::from_token_info("USD Coin", "USDC", 6); + + let metadata = ConversionMetadata { + faucet_account_id: faucet, + origin_token_address, + scale: 6, + origin_network: 42, + is_native: true, + metadata_hash, + }; + + let elements = metadata.to_elements(); + + assert_eq!(elements.len(), ConfigAggBridgeNote::NUM_STORAGE_ITEMS); + assert_eq!(&elements[0..5], origin_token_address.to_elements().as_slice()); + assert_eq!(elements[5], faucet.suffix()); + assert_eq!(elements[6], faucet.prefix().as_felt()); + assert_eq!(elements[7], Felt::from(6_u8)); + // origin_network is stored raw (the MASM bridge-side does any required byte-swap + // before hashing into the token-registry or placing into the LET leaf). + assert_eq!(elements[8], Felt::from(42_u32)); + assert_eq!(elements[9], Felt::from(1_u8)); + assert_eq!(&elements[10..18], metadata_hash.to_elements().as_slice()); } } diff --git a/crates/miden-agglayer/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index 16f09b3a1b..c4c37a7629 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -3,7 +3,6 @@ extern crate alloc; use alloc::vec; use alloc::vec::Vec; -use miden_core::utils::bytes_to_packed_u32_elements; use miden_core::{Felt, Word}; use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ @@ -19,7 +18,6 @@ use miden_protocol::errors::AccountIdError; use miden_standards::account::access::Ownable2Step; use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata}; use miden_standards::account::mint_policies::OwnerControlled; -use miden_utils_sync::LazyLock; use thiserror::Error; use super::agglayer_faucet_component_library; @@ -51,40 +49,16 @@ include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs")); // AGGLAYER FAUCET STRUCT // ================================================================================================ -static CONVERSION_INFO_1_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("agglayer::faucet::conversion_info_1") - .expect("conversion info 1 storage slot name should be valid") -}); -static CONVERSION_INFO_2_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("agglayer::faucet::conversion_info_2") - .expect("conversion info 2 storage slot name should be valid") -}); -static METADATA_HASH_LO_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("agglayer::faucet::metadata_hash_lo") - .expect("metadata hash lo storage slot name should be valid") -}); -static METADATA_HASH_HI_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("agglayer::faucet::metadata_hash_hi") - .expect("metadata hash hi storage slot name should be valid") -}); /// An [`AccountComponent`] implementing the AggLayer Faucet. /// -/// It reexports the procedures from `agglayer::faucet`. When linking against this -/// component, the `agglayer` library must be available to the assembler. -/// The procedures of this component are: -/// - `distribute`, which mints assets and creates output notes (with owner verification). -/// - `asset_to_origin_asset`, which converts an asset to the origin asset (used in FPI from -/// bridge). -/// - `burn`, which burns an asset. +/// It re-exports `mint_and_send` (network fungible faucet) and `burn` (basic fungible faucet) +/// from the agglayer library. 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 /// /// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. -/// - [`Self::conversion_info_1_slot`]: Stores the first 4 felts of the origin token address. -/// - [`Self::conversion_info_2_slot`]: Stores the remaining 5th felt of the origin token address + -/// origin network + scale. -/// - [`Self::metadata_hash_lo_slot`]: Stores the first 4 u32 felts of the metadata hash. -/// - [`Self::metadata_hash_hi_slot`]: Stores the last 4 u32 felts of the metadata hash. /// /// ## Required Companion Components /// @@ -96,10 +70,6 @@ static METADATA_HASH_HI_SLOT_NAME: LazyLock = LazyLock::new(|| #[derive(Debug, Clone)] pub struct AggLayerFaucet { metadata: TokenMetadata, - origin_token_address: EthAddress, - origin_network: u32, - scale: u8, - metadata_hash: MetadataHash, } impl AggLayerFaucet { @@ -113,25 +83,14 @@ impl AggLayerFaucet { /// - The decimals parameter exceeds maximum value of [`TokenMetadata::MAX_DECIMALS`]. /// - The max supply exceeds maximum possible amount for a fungible asset. /// - The token supply exceeds the max supply. - #[allow(clippy::too_many_arguments)] pub fn new( symbol: TokenSymbol, decimals: u8, max_supply: Felt, token_supply: Felt, - origin_token_address: EthAddress, - origin_network: u32, - scale: u8, - metadata_hash: MetadataHash, ) -> Result { let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply)?; - Ok(Self { - metadata, - origin_token_address, - origin_network, - scale, - metadata_hash, - }) + Ok(Self { metadata }) } /// Sets the token supply for an existing faucet (e.g. for testing scenarios). @@ -151,25 +110,6 @@ impl AggLayerFaucet { TokenMetadata::metadata_slot() } - /// Storage slot name for the first 4 felts of the origin token address. - pub fn conversion_info_1_slot() -> &'static StorageSlotName { - &CONVERSION_INFO_1_SLOT_NAME - } - - /// Storage slot name for the 5th felt of the origin token address, origin network, and scale. - pub fn conversion_info_2_slot() -> &'static StorageSlotName { - &CONVERSION_INFO_2_SLOT_NAME - } - - /// Storage slot name for the first 4 u32 felts of the metadata hash. - pub fn metadata_hash_lo_slot() -> &'static StorageSlotName { - &METADATA_HASH_LO_SLOT_NAME - } - - /// Storage slot name for the last 4 u32 felts of the metadata hash. - pub fn metadata_hash_hi_slot() -> &'static StorageSlotName { - &METADATA_HASH_HI_SLOT_NAME - } /// Storage slot name for the owner account ID (bridge), provided by the /// [`Ownable2Step`] companion component. pub fn owner_config_slot() -> &'static StorageSlotName { @@ -209,89 +149,6 @@ impl AggLayerFaucet { ownership.owner().ok_or(AgglayerFaucetError::OwnershipRenounced) } - /// Extracts the origin token address from the corresponding storage slot of the provided - /// account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn origin_token_address( - faucet_account: &Account, - ) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - let conversion_info_1 = faucet_account - .storage() - .get_item(&CONVERSION_INFO_1_SLOT_NAME) - .expect("should be able to read the first conversion info slot"); - - let conversion_info_2 = faucet_account - .storage() - .get_item(&CONVERSION_INFO_2_SLOT_NAME) - .expect("should be able to read the second conversion info slot"); - - let addr_bytes_vec = conversion_info_1 - .iter() - .chain([&conversion_info_2[0]]) - .flat_map(|felt| { - u32::try_from(felt.as_canonical_u64()) - .expect("Felt value does not fit into u32") - .to_le_bytes() - }) - .collect::>(); - - Ok(EthAddress::new( - addr_bytes_vec - .try_into() - .expect("origin token addr vector should consist of exactly 20 bytes"), - )) - } - - /// Extracts the origin network ID in form of the u32 from the corresponding storage slot of the - /// provided account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn origin_network(faucet_account: &Account) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - let conversion_info_2 = faucet_account - .storage() - .get_item(&CONVERSION_INFO_2_SLOT_NAME) - .expect("should be able to read the second conversion info slot"); - - let le_packed = u32::try_from(conversion_info_2[1].as_canonical_u64()) - .expect("origin network ID should fit into u32"); - Ok(u32::from_be_bytes(le_packed.to_le_bytes())) - } - - /// Extracts the scaling factor in form of the u8 from the corresponding storage slot of the - /// provided account. - /// - /// # Errors - /// - /// Returns an error if: - /// - the provided account is not an [`AggLayerFaucet`] account. - pub fn scale(faucet_account: &Account) -> Result { - // check that the provided account is a faucet account - Self::assert_faucet_account(faucet_account)?; - - let conversion_info_2 = faucet_account - .storage() - .get_item(&CONVERSION_INFO_2_SLOT_NAME) - .expect("should be able to read the second conversion info slot"); - - Ok(conversion_info_2[2] - .as_canonical_u64() - .try_into() - .expect("scaling factor should fit into u8")) - } - // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- @@ -357,10 +214,6 @@ impl AggLayerFaucet { /// Returns a vector of all [`AggLayerFaucet`] storage slot names. fn slot_names() -> Vec<&'static StorageSlotName> { vec![ - &*CONVERSION_INFO_1_SLOT_NAME, - &*CONVERSION_INFO_2_SLOT_NAME, - &*METADATA_HASH_LO_SLOT_NAME, - &*METADATA_HASH_HI_SLOT_NAME, TokenMetadata::metadata_slot(), Ownable2Step::slot_name(), OwnerControlled::active_policy_proc_root_slot(), @@ -373,35 +226,7 @@ impl AggLayerFaucet { impl From for AccountComponent { fn from(faucet: AggLayerFaucet) -> Self { let metadata_slot = StorageSlot::from(faucet.metadata); - - let (conversion_slot1_word, conversion_slot2_word) = agglayer_faucet_conversion_slots( - &faucet.origin_token_address, - faucet.origin_network, - faucet.scale, - ); - let conversion_slot1 = - StorageSlot::with_value(CONVERSION_INFO_1_SLOT_NAME.clone(), conversion_slot1_word); - let conversion_slot2 = - StorageSlot::with_value(CONVERSION_INFO_2_SLOT_NAME.clone(), conversion_slot2_word); - - let hash_elements = faucet.metadata_hash.to_elements(); - let metadata_hash_lo = StorageSlot::with_value( - METADATA_HASH_LO_SLOT_NAME.clone(), - Word::new([hash_elements[0], hash_elements[1], hash_elements[2], hash_elements[3]]), - ); - let metadata_hash_hi = StorageSlot::with_value( - METADATA_HASH_HI_SLOT_NAME.clone(), - Word::new([hash_elements[4], hash_elements[5], hash_elements[6], hash_elements[7]]), - ); - - let agglayer_storage_slots = vec![ - metadata_slot, - conversion_slot1, - conversion_slot2, - metadata_hash_lo, - metadata_hash_hi, - ]; - agglayer_faucet_component(agglayer_storage_slots) + agglayer_faucet_component(vec![metadata_slot]) } } @@ -427,57 +252,14 @@ pub enum AgglayerFaucetError { OwnershipRenounced, } -// FAUCET CONVERSION STORAGE HELPERS -// ================================================================================================ - -/// Builds the two storage slot values for faucet conversion metadata. -/// -/// The conversion metadata is stored in two value storage slots: -/// - Slot 1 (`agglayer::faucet::conversion_info_1`): `[addr0, addr1, addr2, addr3]` — first 4 felts -/// of the origin token address (5 × u32 limbs). -/// - Slot 2 (`agglayer::faucet::conversion_info_2`): `[addr4, origin_network, scale, 0]` — -/// remaining address felt + origin network (LE-packed) + scale factor. -/// -/// # Parameters -/// - `origin_token_address`: The EVM token address in Ethereum format -/// - `origin_network`: The origin network/chain ID -/// - `scale`: The decimal scaling factor (exponent for 10^scale) -/// -/// # Returns -/// A tuple of two `Word` values representing the two storage slot contents. -fn agglayer_faucet_conversion_slots( - origin_token_address: &EthAddress, - origin_network: u32, - scale: u8, -) -> (Word, Word) { - let addr_elements = origin_token_address.to_elements(); - - let slot1 = Word::new([addr_elements[0], addr_elements[1], addr_elements[2], addr_elements[3]]); - - let origin_network_packed = bytes_to_packed_u32_elements(&origin_network.to_be_bytes()); - assert_eq!( - origin_network_packed.len(), - 1, - "origin_network should pack into exactly one Felt" - ); - let slot2 = - Word::new([addr_elements[4], origin_network_packed[0], Felt::from(scale), Felt::ZERO]); - - (slot1, slot2) -} - // HELPER FUNCTIONS // ================================================================================================ /// Creates an Agglayer Faucet component with the specified storage slots. -/// -/// This component combines network faucet functionality with bridge validation -/// via Foreign Procedure Invocation (FPI). It provides a "claim" procedure that -/// validates CLAIM notes against a bridge MMR account before minting assets. fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { let library = agglayer_faucet_component_library(); let metadata = AccountComponentMetadata::new("agglayer::faucet", [AccountType::FungibleFaucet]) - .with_description("AggLayer faucet component with bridge validation"); + .with_description("AggLayer faucet component"); AccountComponent::new(library, 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 dde029da27..91f9119678 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -45,7 +45,7 @@ pub use claim_note::{ SmtNode, create_claim_note, }; -pub use config_note::ConfigAggBridgeNote; +pub use config_note::{ConfigAggBridgeNote, ConversionMetadata}; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{ @@ -114,51 +114,30 @@ fn agglayer_faucet_component_library() -> Library { /// Creates an agglayer faucet account component with the specified configuration. /// -/// This function creates all the necessary storage slots for an agglayer faucet: -/// - Network faucet metadata slot (token_supply, max_supply, decimals, token_symbol) -/// - Conversion info slot 1: first 4 felts of origin token address -/// - Conversion info slot 2: 5th address felt + origin network + scale -/// - Owner config slot: bridge account ID for MINT note authorization +/// 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. /// /// # Parameters /// - `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 /// - `token_supply`: Initial outstanding token supply (0 for new faucets) -/// - `bridge_account_id`: The account ID of the bridge account for validation -/// - `origin_token_address`: The EVM origin token address -/// - `origin_network`: The origin network/chain ID -/// - `scale`: The decimal scaling factor (exponent for 10^scale) /// /// # Returns /// Returns an [`AccountComponent`] configured for agglayer faucet operations. /// /// # Panics /// Panics if the token symbol is invalid or metadata validation fails. -#[allow(clippy::too_many_arguments)] fn create_agglayer_faucet_component( token_symbol: &str, decimals: u8, max_supply: Felt, token_supply: Felt, - origin_token_address: &EthAddress, - origin_network: u32, - scale: u8, - metadata_hash: MetadataHash, ) -> AccountComponent { let symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); - AggLayerFaucet::new( - symbol, - decimals, - max_supply, - token_supply, - *origin_token_address, - origin_network, - scale, - metadata_hash, - ) - .expect("agglayer faucet metadata should be valid") - .into() + AggLayerFaucet::new(symbol, decimals, max_supply, token_supply) + .expect("agglayer faucet metadata should be valid") + .into() } /// Creates a complete bridge account builder with the standard configuration. @@ -207,11 +186,10 @@ pub fn create_existing_bridge_account( /// Creates a complete agglayer faucet account builder with the specified configuration. /// /// The builder includes: -/// - The `AggLayerFaucet` component (conversion metadata + token metadata). +/// - The `AggLayerFaucet` component (token metadata only; conversion metadata lives on the bridge). /// - The `Ownable2Step` component (bridge account ID as owner for mint authorization). /// - The `OwnerControlled` component (mint policy management required by /// `network_fungible::mint_and_send`). -#[allow(clippy::too_many_arguments)] fn create_agglayer_faucet_builder( seed: Word, token_symbol: &str, @@ -219,21 +197,9 @@ fn create_agglayer_faucet_builder( max_supply: Felt, token_supply: Felt, bridge_account_id: AccountId, - origin_token_address: &EthAddress, - origin_network: u32, - scale: u8, - metadata_hash: MetadataHash, ) -> AccountBuilder { - let agglayer_component = create_agglayer_faucet_component( - token_symbol, - decimals, - max_supply, - token_supply, - origin_token_address, - origin_network, - scale, - metadata_hash, - ); + let agglayer_component = + create_agglayer_faucet_component(token_symbol, decimals, max_supply, token_supply); Account::builder(seed.into()) .account_type(AccountType::FungibleFaucet) @@ -246,17 +212,12 @@ fn create_agglayer_faucet_builder( /// Creates a new agglayer faucet account with the specified configuration. /// /// This creates a new account suitable for production use. -#[allow(clippy::too_many_arguments)] pub fn create_agglayer_faucet( seed: Word, token_symbol: &str, decimals: u8, max_supply: Felt, bridge_account_id: AccountId, - origin_token_address: &EthAddress, - origin_network: u32, - scale: u8, - metadata_hash: MetadataHash, ) -> Account { create_agglayer_faucet_builder( seed, @@ -265,10 +226,6 @@ pub fn create_agglayer_faucet( max_supply, Felt::ZERO, bridge_account_id, - origin_token_address, - origin_network, - scale, - metadata_hash, ) .with_auth_component(AccountComponent::from(NoAuth)) .build() @@ -279,7 +236,6 @@ pub fn create_agglayer_faucet( /// /// This creates an existing account suitable for testing scenarios. #[cfg(any(feature = "testing", test))] -#[allow(clippy::too_many_arguments)] pub fn create_existing_agglayer_faucet( seed: Word, token_symbol: &str, @@ -287,10 +243,6 @@ pub fn create_existing_agglayer_faucet( max_supply: Felt, token_supply: Felt, bridge_account_id: AccountId, - origin_token_address: &EthAddress, - origin_network: u32, - scale: u8, - metadata_hash: MetadataHash, ) -> Account { create_agglayer_faucet_builder( seed, @@ -299,10 +251,6 @@ pub fn create_existing_agglayer_faucet( max_supply, token_supply, bridge_account_id, - origin_token_address, - origin_network, - scale, - metadata_hash, ) .with_auth_component(AccountComponent::from(NoAuth)) .build_existing() diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 9ddecf31d2..77be4252a4 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -4,15 +4,13 @@ use alloc::slice; use alloc::string::String; use anyhow::Context; -use miden_agglayer::errors::{ - ERR_CLAIM_ALREADY_SPENT, - ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH, - ERR_TOKEN_NOT_REGISTERED, -}; +use miden_agglayer::errors::{ERR_CLAIM_ALREADY_SPENT, ERR_TOKEN_NOT_REGISTERED}; use miden_agglayer::{ - AggLayerBridge, + B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, + ConversionMetadata, + EthAddress, EthEmbeddedAccountId, ExitRoot, LeafValue, @@ -24,27 +22,28 @@ use miden_agglayer::{ create_existing_bridge_account, }; use miden_protocol::Felt; -use miden_protocol::account::Account; use miden_protocol::account::auth::AuthScheme; +use miden_protocol::account::{ + Account, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, +}; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; -use miden_protocol::note::NoteType; +use miden_protocol::note::{NoteAssets, NoteType}; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use miden_protocol::transaction::RawOutputNote; +use miden_standards::account::mint_policies::OwnerControlledInitConfig; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::P2idNote; use miden_standards::testing::account_component::IncrNonceAuthComponent; use miden_standards::testing::mock_account::MockAccountExt; use miden_testing::utils::create_p2id_note_exact; -use miden_testing::{ - AccountState, - Auth, - MockChain, - TransactionContextBuilder, - assert_transaction_executor_error, -}; +use miden_testing::{AccountState, Auth, MockChain, TransactionContextBuilder}; use miden_tx::utils::hex_to_bytes; use rand::Rng; @@ -118,21 +117,20 @@ fn merkle_proof_verification_code( /// TX1: UPDATE_GER → bridge (stores GER) /// TX2: CLAIM → bridge (validates proof, creates MINT note) /// TX3: MINT → aggfaucet (mints asset, creates P2ID note) -/// TX4: P2ID → destination (runs for both `Mainnet` and `Rollup` simulated cases) +/// TX4: P2ID → destination (simulated case only) /// /// Parameterized over two claim data sources: /// - [`ClaimDataSource::L1ToMiden`]: uses locally generated [`ProofData`] and [`LeafData`] from /// `claim_asset_vectors_l1_tx.json`, produced by simulating a `bridgeAsset()` call. /// - [`ClaimDataSource::L2ToMiden`]: uses rollup deposit data from /// `claim_asset_vectors_l2_tx.json`, produced by simulating a rollup deposit. -/// -/// Note: Modifying anything in the real test vectors would invalidate the Merkle proof, -/// as the proof was computed for the original leaf data including the original destination. #[rstest::rstest] -#[case::simulated_l1_to_miden(ClaimDataSource::L1ToMiden)] -#[case::simulated_l2_to_miden(ClaimDataSource::L2ToMiden)] +#[case::l1_to_miden(ClaimDataSource::L1ToMiden)] +#[case::l2_to_miden(ClaimDataSource::L2ToMiden)] #[tokio::test] async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { + use miden_agglayer::AggLayerBridge; + let mut builder = MockChain::builder(); // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) @@ -157,11 +155,6 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // GET CLAIM DATA FROM JSON (source depends on the test case) // -------------------------------------------------------------------------------------------- let (proof_data, leaf_data, ger, cgi_chain_hash) = data_source.get_data(); - assert_eq!( - leaf_data.destination_network, - AggLayerBridge::MIDEN_NETWORK_ID, - "test vectors must target Miden as destination (MIDEN_NETWORK_ID)" - ); // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) // Use the origin token address and network from the claim data. @@ -182,10 +175,6 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a max_supply, Felt::ZERO, bridge_account.id(), - &origin_token_address, - origin_network, - scale, - leaf_data.metadata_hash, ); builder.add_account(agglayer_faucet.clone())?; @@ -196,12 +185,9 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a .expect("destination address is not an embedded Miden AccountId") .into_account_id(); - // For mainnet and rollup fixtures, create the destination account so we can consume the P2ID - // note. - let destination_account = if matches!( - data_source, - ClaimDataSource::L1ToMiden | ClaimDataSource::L2ToMiden - ) { + // Create the destination account so we can consume the P2ID note. Both supported data + // sources are simulated, so the destination ID is deterministic and mockable. + let destination_account = { let dest = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, IncrNonceAuthComponent); // Ensure the mock account ID matches the destination embedded in the JSON test vector, @@ -213,8 +199,6 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a ); builder.add_account(dest.clone())?; Some(dest) - } else { - None }; // CREATE SENDER ACCOUNT (for creating the claim note) @@ -239,6 +223,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a .scale_to_token_amount(scale as u32) .expect("amount should scale successfully"); + let metadata_hash = leaf_data.metadata_hash; let claim_inputs = ClaimNoteStorage { proof_data, leaf_data, @@ -258,9 +243,14 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) // -------------------------------------------------------------------------------------------- let config_note = ConfigAggBridgeNote::create( - agglayer_faucet.id(), - &origin_token_address, - origin_network, + ConversionMetadata { + faucet_account_id: agglayer_faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -398,9 +388,9 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a assert_eq!(RawOutputNote::Full(expected_output_p2id_note.clone()), *output_note); - // TX4: CONSUME THE P2ID NOTE WITH THE DESTINATION ACCOUNT (mainnet and rollup fixtures) + // TX4: CONSUME THE P2ID NOTE WITH THE DESTINATION ACCOUNT (simulated case only) // -------------------------------------------------------------------------------------------- - // The harness owns the destination account from the JSON vectors, so we can verify the full + // For the simulated case, we control the destination account and can verify the full // end-to-end flow including P2ID consumption and balance updates. if let Some(destination_account) = destination_account { // Add the faucet transaction to the chain and prove the next block so the P2ID note is @@ -432,49 +422,44 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a Ok(()) } -/// CLAIM must reject a leaf whose `destination_network` does not match the global Miden -/// AggLayer network ID (`MIDEN_NETWORK_ID` in `constants.masm`), even when the rest of the proof -/// data is unchanged. +/// Tests that consuming a CLAIM note with the same PROOF_DATA_KEY twice fails. +/// +/// This test verifies the nullifier tracking mechanism: +/// 1. Sets up the bridge (CONFIG + UPDATE_GER) +/// 2. Executes the first CLAIM note successfully +/// 3. Creates a second CLAIM note with the same proof data +/// 4. Attempts to execute the second CLAIM note and asserts it fails with "claim note has already +/// been spent" #[tokio::test] -async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { +async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { let data_source = ClaimDataSource::L1ToMiden; let mut builder = MockChain::builder(); - // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) - // -------------------------------------------------------------------------------------------- + // CREATE BRIDGE ADMIN ACCOUNT let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2, })?; - // CREATE GER MANAGER ACCOUNT (sends the UPDATE_GER note) - // -------------------------------------------------------------------------------------------- + // CREATE GER MANAGER ACCOUNT let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2, })?; // CREATE BRIDGE ACCOUNT - // -------------------------------------------------------------------------------------------- let bridge_seed = builder.rng_mut().draw_word(); let bridge_account = create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id()); builder.add_account(bridge_account.clone())?; // GET CLAIM DATA FROM JSON - // -------------------------------------------------------------------------------------------- - let (proof_data, mut leaf_data, ger, _cgi) = data_source.get_data(); - - // Override destination_network so it no longer matches the bridge's MIDEN_NETWORK_ID. - // Proof data is unchanged; the bridge should fail before Merkle verification. - // -------------------------------------------------------------------------------------------- - leaf_data.destination_network = AggLayerBridge::MIDEN_NETWORK_ID.saturating_add(1); + let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); - // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) - // Use the origin token address and network from the claim data. - // -------------------------------------------------------------------------------------------- + // CREATE AGGLAYER FAUCET ACCOUNT let token_symbol = "AGG"; let decimals = 8u8; let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let agglayer_faucet_seed = builder.rng_mut().draw_word(); + let origin_token_address = leaf_data.origin_token_address; let origin_network = leaf_data.origin_network; let scale = 10u8; @@ -486,233 +471,587 @@ async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { max_supply, Felt::ZERO, bridge_account.id(), - &origin_token_address, - origin_network, - scale, - leaf_data.metadata_hash, ); builder.add_account(agglayer_faucet.clone())?; // Calculate the scaled-down Miden amount - // -------------------------------------------------------------------------------------------- let miden_claim_amount = leaf_data .amount .scale_to_token_amount(scale as u32) .expect("amount should scale successfully"); - // CREATE CLAIM NOTE (targets the bridge) - // -------------------------------------------------------------------------------------------- - let claim_note = create_claim_note( - ClaimNoteStorage { - proof_data, - leaf_data, - miden_claim_amount, - }, + // CREATE FIRST CLAIM NOTE + let claim_inputs_1 = ClaimNoteStorage { + proof_data: proof_data.clone(), + leaf_data: leaf_data.clone(), + miden_claim_amount, + }; + + let claim_note_1 = create_claim_note( + claim_inputs_1, bridge_account.id(), bridge_admin.id(), builder.rng_mut(), )?; - builder.add_output_note(RawOutputNote::Full(claim_note.clone())); + builder.add_output_note(RawOutputNote::Full(claim_note_1.clone())); - // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) - // -------------------------------------------------------------------------------------------- + // CREATE SECOND CLAIM NOTE (same proof data = same PROOF_DATA_KEY) + let claim_inputs_2 = ClaimNoteStorage { + proof_data: proof_data.clone(), + leaf_data: leaf_data.clone(), + miden_claim_amount, + }; + + let claim_note_2 = create_claim_note( + claim_inputs_2, + bridge_account.id(), + bridge_admin.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(claim_note_2.clone())); + + // CREATE CONFIG_AGG_BRIDGE NOTE let config_note = ConfigAggBridgeNote::create( - agglayer_faucet.id(), - &origin_token_address, - origin_network, + ConversionMetadata { + faucet_account_id: agglayer_faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: false, + metadata_hash: leaf_data.metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), )?; builder.add_output_note(RawOutputNote::Full(config_note.clone())); - // CREATE UPDATE_GER NOTE WITH GLOBAL EXIT ROOT - // -------------------------------------------------------------------------------------------- + // CREATE UPDATE_GER NOTE let update_ger_note = UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?; builder.add_output_note(RawOutputNote::Full(update_ger_note.clone())); - // BUILD MOCK CHAIN WITH ALL ACCOUNTS - // -------------------------------------------------------------------------------------------- + // BUILD MOCK CHAIN let mut mock_chain = builder.clone().build()?; - // TX0: EXECUTE CONFIG_AGG_BRIDGE NOTE TO REGISTER FAUCET IN BRIDGE - // -------------------------------------------------------------------------------------------- + // TX0: CONFIG_AGG_BRIDGE let config_tx_context = mock_chain .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? .build()?; - mock_chain.add_pending_executed_transaction(&config_tx_context.execute().await?)?; + let config_executed = config_tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&config_executed)?; mock_chain.prove_next_block()?; - // TX1: EXECUTE UPDATE_GER NOTE TO STORE GER IN BRIDGE ACCOUNT - // -------------------------------------------------------------------------------------------- + // TX1: UPDATE_GER let update_ger_tx_context = mock_chain .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? .build()?; - mock_chain.add_pending_executed_transaction(&update_ger_tx_context.execute().await?)?; + let update_ger_executed = update_ger_tx_context.execute().await?; + mock_chain.add_pending_executed_transaction(&update_ger_executed)?; mock_chain.prove_next_block()?; - // TX2: EXECUTE CLAIM NOTE AGAINST BRIDGE (must fail: wrong destination_network) - // -------------------------------------------------------------------------------------------- - let faucet_foreign_inputs = mock_chain.get_foreign_account_inputs(agglayer_faucet.id())?; - let claim_tx_context = mock_chain - .build_tx_context(bridge_account.id(), &[], &[claim_note])? - .foreign_accounts(vec![faucet_foreign_inputs]) + // TX2: FIRST CLAIM (should succeed) + let faucet_foreign_inputs_1 = mock_chain.get_foreign_account_inputs(agglayer_faucet.id())?; + let claim_tx_context_1 = mock_chain + .build_tx_context(bridge_account.id(), &[], &[claim_note_1])? + .foreign_accounts(vec![faucet_foreign_inputs_1]) + .build()?; + let claim_executed_1 = claim_tx_context_1.execute().await?; + assert_eq!(claim_executed_1.output_notes().num_notes(), 1); + + mock_chain.add_pending_executed_transaction(&claim_executed_1)?; + mock_chain.prove_next_block()?; + + // TX3: SECOND CLAIM WITH SAME PROOF_DATA_KEY (should fail) + let faucet_foreign_inputs_2 = mock_chain.get_foreign_account_inputs(agglayer_faucet.id())?; + let claim_tx_context_2 = mock_chain + .build_tx_context(bridge_account.id(), &[], &[claim_note_2])? + .foreign_accounts(vec![faucet_foreign_inputs_2]) .build()?; - let result = claim_tx_context.execute().await; - assert_transaction_executor_error!(result, ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH); + let result = claim_tx_context_2.execute().await; + + assert!(result.is_err(), "Second claim with same PROOF_DATA_KEY should fail"); + let error_msg = result.unwrap_err().to_string(); + let expected_err_code = ERR_CLAIM_ALREADY_SPENT.code().to_string(); + assert!( + error_msg.contains(&expected_err_code), + "expected error code {expected_err_code} for 'claim note has already been spent', got: {error_msg}" + ); Ok(()) } -/// Tests that consuming a CLAIM note with the same PROOF_DATA_KEY twice fails. +/// Tests the bridge-in unlock path for Miden-native faucets. /// -/// This test verifies the nullifier tracking mechanism: -/// 1. Sets up the bridge (CONFIG + UPDATE_GER) -/// 2. Executes the first CLAIM note successfully -/// 3. Creates a second CLAIM note with the same proof data -/// 4. Attempts to execute the second CLAIM note and asserts it fails with "claim note has already -/// been spent" +/// When a faucet is registered with `is_native = true`, a valid CLAIM note does NOT go through +/// the MINT→faucet→P2ID flow. Instead, the bridge removes the asset from its own vault and +/// emits a P2ID note directly to the recipient. +/// +/// Flow: +/// 1. Register a native (non-bridge-owned) faucet with `is_native = true` using the +/// origin_token_address and metadata_hash from a simulated L1→Miden claim vector. +/// 2. Seed the bridge vault by running one lock transaction (bridge-out of a B2AGG note carrying +/// `miden_claim_amount` of the native asset). +/// 3. Store a GER that covers the claim's Merkle proof. +/// 4. Execute the CLAIM against the bridge — the `claim` proc dispatches into `unlock_and_send` +/// because the faucet is registered with `is_native = true`. +/// 5. Assert that exactly one output P2ID note is produced, its asset matches what was locked, the +/// bridge vault is drained to 0, and the destination can consume the P2ID. #[tokio::test] -async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { +async fn bridge_in_unlock_native_token() -> anyhow::Result<()> { let data_source = ClaimDataSource::L1ToMiden; let mut builder = MockChain::builder(); - // CREATE BRIDGE ADMIN ACCOUNT + // Bridge admin / GER manager / bridge account. let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2, })?; - - // CREATE GER MANAGER ACCOUNT let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Poseidon2, })?; - // CREATE BRIDGE ACCOUNT let bridge_seed = builder.rng_mut().draw_word(); - let bridge_account = + let mut bridge_account = create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id()); builder.add_account(bridge_account.clone())?; - // GET CLAIM DATA FROM JSON + // Claim data: leaf data's origin_token_address + metadata_hash must match the registration + // below so the bridge's token-registry lookup resolves to the native faucet. let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); - - // CREATE AGGLAYER FAUCET ACCOUNT - let token_symbol = "AGG"; - let decimals = 8u8; - let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); - let agglayer_faucet_seed = builder.rng_mut().draw_word(); - let origin_token_address = leaf_data.origin_token_address; let origin_network = leaf_data.origin_network; + let metadata_hash = leaf_data.metadata_hash; let scale = 10u8; - let agglayer_faucet = create_existing_agglayer_faucet( - agglayer_faucet_seed, - token_symbol, - decimals, - max_supply, - Felt::ZERO, + // The amount the claim will attempt to unlock: scaled from the leaf's U256 amount. + let miden_claim_amount = leaf_data + .amount + .scale_to_token_amount(scale as u32) + .expect("amount should scale successfully"); + let miden_claim_amount_u64 = miden_claim_amount.as_canonical_u64(); + + // Native faucet: use the network-faucet pattern (bridge is not the owner). + let faucet_owner_account_id = AccountId::dummy( + [3; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let native_faucet = builder.add_existing_network_faucet( + "NATIVE", + miden_claim_amount_u64.saturating_mul(4), + faucet_owner_account_id, + // Seed enough native supply for the lock step's sender to bundle into the B2AGG note. + Some(miden_claim_amount_u64.saturating_mul(2)), + OwnerControlledInitConfig::OwnerOnly, + )?; + + // Destination of the claim (derived from leaf data's destination_address). + let destination_account_id = EthEmbeddedAccountId::try_from(leaf_data.destination_address) + .expect("destination address is not an embedded Miden AccountId") + .into_account_id(); + let destination_account = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, IncrNonceAuthComponent); + assert_eq!( + destination_account.id(), + destination_account_id, + "mock destination account ID must match the destination_account_id from the claim data" + ); + builder.add_account(destination_account.clone())?; + + // Sender of the CLAIM note (any wallet — just a note creator). + let claim_sender = { + let account_builder = + Account::builder(builder.rng_mut().random()).with_component(BasicWallet); + builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)? + }; + + // Sender of the B2AGG note used to seed the bridge vault with the native asset. + let lock_sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // Register the native faucet with is_native = true. + let config_note = ConfigAggBridgeNote::create( + ConversionMetadata { + faucet_account_id: native_faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: true, + metadata_hash, + }, + bridge_admin.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(config_note.clone())); + + // B2AGG note that will seed the bridge's vault with `miden_claim_amount_u64` of native asset. + let bridge_asset: Asset = + FungibleAsset::new(native_faucet.id(), miden_claim_amount_u64).unwrap().into(); + let b2agg_destination_address = + EthAddress::from_hex("0x1234567890abcdef1122334455667788990011aa") + .expect("valid destination address"); + let b2agg_note = B2AggNote::create( + 1u32, + b2agg_destination_address, + NoteAssets::new(vec![bridge_asset])?, bridge_account.id(), - &origin_token_address, - origin_network, - scale, - leaf_data.metadata_hash, + lock_sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(b2agg_note.clone())); + + // CLAIM note targeting the bridge. + let serial_num = proof_data.to_commitment(); + let claim_inputs = ClaimNoteStorage { + proof_data, + leaf_data, + miden_claim_amount, + }; + let claim_note = + create_claim_note(claim_inputs, bridge_account.id(), claim_sender.id(), builder.rng_mut())?; + builder.add_output_note(RawOutputNote::Full(claim_note.clone())); + + // GER for the claim's Merkle proof. + let update_ger_note = + UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?; + builder.add_output_note(RawOutputNote::Full(update_ger_note.clone())); + + let mut mock_chain = builder.clone().build()?; + + // TX0: CONFIG — registers native faucet with is_native = true. + let config_executed = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()? + .execute() + .await?; + bridge_account.apply_delta(config_executed.account_delta())?; + mock_chain.add_pending_executed_transaction(&config_executed)?; + mock_chain.prove_next_block()?; + + // TX1: LOCK — bridge consumes the B2AGG note, asset goes into bridge vault. + let lock_executed = mock_chain + .build_tx_context(bridge_account.clone(), &[b2agg_note.id()], &[])? + .build()? + .execute() + .await?; + assert_eq!( + lock_executed.output_notes().num_notes(), + 0, + "Lock transaction should not emit any output note" ); - builder.add_account(agglayer_faucet.clone())?; + bridge_account.apply_delta(lock_executed.account_delta())?; + assert_eq!( + bridge_account.vault().get_balance(native_faucet.id())?, + miden_claim_amount_u64, + "Bridge vault should hold the locked native asset before the claim" + ); + mock_chain.add_pending_executed_transaction(&lock_executed)?; + mock_chain.prove_next_block()?; + + // TX2: UPDATE_GER. + let update_ger_executed = mock_chain + .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? + .build()? + .execute() + .await?; + bridge_account.apply_delta(update_ger_executed.account_delta())?; + mock_chain.add_pending_executed_transaction(&update_ger_executed)?; + mock_chain.prove_next_block()?; + + // TX3: CLAIM — bridge validates the proof, hits the is_native branch, unlocks and emits P2ID. + let claim_executed = mock_chain + .build_tx_context(bridge_account.clone(), &[], &[claim_note])? + .build()? + .execute() + .await + .context("CLAIM execution against bridge failed")?; + + // Exactly one output note — a PUBLIC P2ID carrying the native asset, sent by the bridge. + assert_eq!( + claim_executed.output_notes().num_notes(), + 1, + "Unlock path should emit exactly one P2ID output note" + ); + let output_note = match claim_executed.output_notes().get_note(0) { + RawOutputNote::Full(note) => note.clone(), + other => panic!("expected Full output note, got {other:?}"), + }; + + let expected_asset: Asset = + FungibleAsset::new(native_faucet.id(), miden_claim_amount_u64).unwrap().into(); + + assert_eq!(output_note.metadata().sender(), bridge_account.id()); + assert_eq!(output_note.metadata().note_type(), NoteType::Public); + assert_eq!( + output_note.recipient().script().root(), + P2idNote::script().root(), + "Output note should use the P2ID script" + ); + assert_eq!(output_note.recipient().serial_num(), serial_num); + + let mut assets_iter = output_note.assets().iter_fungible(); + let unlocked_asset = assets_iter + .next() + .expect("P2ID output note should carry exactly one fungible asset"); + assert!(assets_iter.next().is_none(), "P2ID output note should carry only one asset"); + assert_eq!(Felt::new(unlocked_asset.amount()), miden_claim_amount); + assert_eq!(unlocked_asset.faucet_id(), native_faucet.id()); + + // Cross-check storage directly: it should encode the destination account ID the same way + // `P2idNoteStorage::from` does ([suffix, prefix]). + let expected_p2id_note = create_p2id_note_exact( + bridge_account.id(), + destination_account_id, + vec![expected_asset], + NoteType::Public, + serial_num, + ) + .unwrap(); + let actual_storage = output_note.recipient().storage(); + let expected_storage = expected_p2id_note.recipient().storage(); + assert_eq!( + actual_storage, expected_storage, + "P2ID note storage items (encoding the target account ID) should match \ + the standard P2idNoteStorage encoding for destination_account_id={destination_account_id:?}" + ); + assert_eq!( + output_note.recipient().digest(), + expected_p2id_note.recipient().digest(), + "Recipient digest should match an independently constructed P2ID to the destination" + ); + + // Bridge vault is drained after the unlock. + bridge_account.apply_delta(claim_executed.account_delta())?; + assert_eq!( + bridge_account.vault().get_balance(native_faucet.id())?, + 0, + "Bridge vault should be empty after the unlock" + ); + + mock_chain.add_pending_executed_transaction(&claim_executed)?; + mock_chain.prove_next_block()?; + + // TX4: destination consumes the P2ID note and receives the unlocked asset. + let consume_executed = mock_chain + .build_tx_context(destination_account.id(), &[], slice::from_ref(&expected_p2id_note))? + .build()? + .execute() + .await?; + + let mut destination_account = destination_account; + destination_account.apply_delta(consume_executed.account_delta())?; + assert_eq!( + destination_account.vault().get_balance(native_faucet.id())?, + miden_claim_amount_u64, + "Destination account should receive the unlocked asset from the P2ID" + ); + + Ok(()) +} + +/// Tests that a second CLAIM reusing the same leaf against the native unlock path is rejected. +/// +/// The native unlock path in `bridge_in_output::unlock_and_send` uses a deterministic P2ID serial +/// number derived from `CLAIM_PROOF_DATA_KEY`. Replay safety therefore depends on the claim +/// nullifier check in `bridge_in::claim` running before the branch into `unlock_and_send`. This +/// test seeds the bridge vault with enough native supply to serve two unlocks, then confirms the +/// second CLAIM with the same proof data is rejected with `ERR_CLAIM_ALREADY_SPENT` rather than +/// draining the vault a second time. +#[tokio::test] +async fn bridge_in_unlock_native_duplicate_rejected() -> anyhow::Result<()> { + let data_source = ClaimDataSource::L1ToMiden; + let mut builder = MockChain::builder(); + + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let bridge_seed = builder.rng_mut().draw_word(); + let mut bridge_account = + create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id()); + builder.add_account(bridge_account.clone())?; + + let (proof_data, leaf_data, ger, _cgi_chain_hash) = data_source.get_data(); + let origin_token_address = leaf_data.origin_token_address; + let origin_network = leaf_data.origin_network; + let metadata_hash = leaf_data.metadata_hash; + let scale = 10u8; - // Calculate the scaled-down Miden amount let miden_claim_amount = leaf_data .amount .scale_to_token_amount(scale as u32) .expect("amount should scale successfully"); + let miden_claim_amount_u64 = miden_claim_amount.as_canonical_u64(); + + // Seed the native faucet and the lock sender with enough supply to cover two unlocks. If the + // nullifier check is ever weakened, the second claim would otherwise succeed and drain the + // vault a second time. + let faucet_owner_account_id = AccountId::dummy( + [3; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let native_faucet = builder.add_existing_network_faucet( + "NATIVE", + miden_claim_amount_u64.saturating_mul(4), + faucet_owner_account_id, + Some(miden_claim_amount_u64.saturating_mul(4)), + OwnerControlledInitConfig::OwnerOnly, + )?; + + let destination_account_id = EthEmbeddedAccountId::try_from(leaf_data.destination_address) + .expect("destination address is not an embedded Miden AccountId") + .into_account_id(); + let destination_account = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, IncrNonceAuthComponent); + assert_eq!(destination_account.id(), destination_account_id); + builder.add_account(destination_account)?; + + let claim_sender = { + let account_builder = + Account::builder(builder.rng_mut().random()).with_component(BasicWallet); + builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)? + }; + + let lock_sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let config_note = ConfigAggBridgeNote::create( + ConversionMetadata { + faucet_account_id: native_faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: true, + metadata_hash, + }, + bridge_admin.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(config_note.clone())); + + // Lock 2x the claim amount so the bridge vault could (if nullifier were broken) serve the + // replayed claim. + let bridge_asset: Asset = + FungibleAsset::new(native_faucet.id(), miden_claim_amount_u64.saturating_mul(2)) + .unwrap() + .into(); + let b2agg_destination_address = + EthAddress::from_hex("0x1234567890abcdef1122334455667788990011aa") + .expect("valid destination address"); + let b2agg_note = B2AggNote::create( + 1u32, + b2agg_destination_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + lock_sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(b2agg_note.clone())); - // CREATE FIRST CLAIM NOTE let claim_inputs_1 = ClaimNoteStorage { proof_data: proof_data.clone(), leaf_data: leaf_data.clone(), miden_claim_amount, }; - let claim_note_1 = create_claim_note( claim_inputs_1, bridge_account.id(), - bridge_admin.id(), + claim_sender.id(), builder.rng_mut(), )?; builder.add_output_note(RawOutputNote::Full(claim_note_1.clone())); - // CREATE SECOND CLAIM NOTE (same proof data = same PROOF_DATA_KEY) let claim_inputs_2 = ClaimNoteStorage { proof_data: proof_data.clone(), leaf_data: leaf_data.clone(), miden_claim_amount, }; - let claim_note_2 = create_claim_note( claim_inputs_2, bridge_account.id(), - bridge_admin.id(), + claim_sender.id(), builder.rng_mut(), )?; builder.add_output_note(RawOutputNote::Full(claim_note_2.clone())); - // CREATE CONFIG_AGG_BRIDGE NOTE - let config_note = ConfigAggBridgeNote::create( - agglayer_faucet.id(), - &origin_token_address, - origin_network, - bridge_admin.id(), - bridge_account.id(), - builder.rng_mut(), - )?; - builder.add_output_note(RawOutputNote::Full(config_note.clone())); - - // CREATE UPDATE_GER NOTE let update_ger_note = UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?; builder.add_output_note(RawOutputNote::Full(update_ger_note.clone())); - // BUILD MOCK CHAIN let mut mock_chain = builder.clone().build()?; - // TX0: CONFIG_AGG_BRIDGE - let config_tx_context = mock_chain + // TX0: CONFIG — register native faucet. + let config_executed = mock_chain .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? - .build()?; - let config_executed = config_tx_context.execute().await?; + .build()? + .execute() + .await?; + bridge_account.apply_delta(config_executed.account_delta())?; mock_chain.add_pending_executed_transaction(&config_executed)?; mock_chain.prove_next_block()?; - // TX1: UPDATE_GER - let update_ger_tx_context = mock_chain + // TX1: LOCK — seed bridge vault with 2x miden_claim_amount. + let lock_executed = mock_chain + .build_tx_context(bridge_account.clone(), &[b2agg_note.id()], &[])? + .build()? + .execute() + .await?; + bridge_account.apply_delta(lock_executed.account_delta())?; + assert_eq!( + bridge_account.vault().get_balance(native_faucet.id())?, + miden_claim_amount_u64.saturating_mul(2), + ); + mock_chain.add_pending_executed_transaction(&lock_executed)?; + mock_chain.prove_next_block()?; + + // TX2: UPDATE_GER. + let update_ger_executed = mock_chain .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? - .build()?; - let update_ger_executed = update_ger_tx_context.execute().await?; + .build()? + .execute() + .await?; + bridge_account.apply_delta(update_ger_executed.account_delta())?; mock_chain.add_pending_executed_transaction(&update_ger_executed)?; mock_chain.prove_next_block()?; - // TX2: FIRST CLAIM (should succeed) - let faucet_foreign_inputs_1 = mock_chain.get_foreign_account_inputs(agglayer_faucet.id())?; - let claim_tx_context_1 = mock_chain - .build_tx_context(bridge_account.id(), &[], &[claim_note_1])? - .foreign_accounts(vec![faucet_foreign_inputs_1]) - .build()?; - let claim_executed_1 = claim_tx_context_1.execute().await?; + // TX3: FIRST CLAIM — should succeed and drain half the vault. + let claim_executed_1 = mock_chain + .build_tx_context(bridge_account.clone(), &[], &[claim_note_1])? + .build()? + .execute() + .await?; assert_eq!(claim_executed_1.output_notes().num_notes(), 1); - + bridge_account.apply_delta(claim_executed_1.account_delta())?; + assert_eq!( + bridge_account.vault().get_balance(native_faucet.id())?, + miden_claim_amount_u64, + "Bridge vault should hold exactly the remaining half after the first unlock" + ); mock_chain.add_pending_executed_transaction(&claim_executed_1)?; mock_chain.prove_next_block()?; - // TX3: SECOND CLAIM WITH SAME PROOF_DATA_KEY (should fail) - let faucet_foreign_inputs_2 = mock_chain.get_foreign_account_inputs(agglayer_faucet.id())?; - let claim_tx_context_2 = mock_chain - .build_tx_context(bridge_account.id(), &[], &[claim_note_2])? - .foreign_accounts(vec![faucet_foreign_inputs_2]) - .build()?; - let result = claim_tx_context_2.execute().await; - - assert!(result.is_err(), "Second claim with same PROOF_DATA_KEY should fail"); + // TX4: SECOND CLAIM with same proof data — should fail on the nullifier, before reaching + // `unlock_and_send`. Vault still has enough to serve it, so a pass here would mean the + // nullifier gate is broken. + let result = mock_chain + .build_tx_context(bridge_account, &[], &[claim_note_2])? + .build()? + .execute() + .await; + assert!( + result.is_err(), + "Second native-path claim with the same PROOF_DATA_KEY should fail" + ); let error_msg = result.unwrap_err().to_string(); let expected_err_code = ERR_CLAIM_ALREADY_SPENT.code().to_string(); assert!( @@ -723,7 +1062,31 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { Ok(()) } -/// Regression test for issue #2799. +#[tokio::test] +async fn solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { + let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; + + assert_eq!(merkle_paths.leaves.len(), merkle_paths.roots.len()); + assert_eq!(merkle_paths.leaves.len() * 32, merkle_paths.merkle_paths.len()); + + for leaf_index in 0..32 { + let source = merkle_proof_verification_code(leaf_index, merkle_paths); + + let tx_script = CodeBuilder::new() + .with_statically_linked_library(&agglayer_library())? + .compile_tx_script(source)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script.clone()) + .build()? + .execute() + .await + .context(format!("failed to execute transaction with leaf index {leaf_index}"))?; + } + Ok(()) +} + +/// Regression test for issue #2799 (ported from agglayer in #2860). /// /// Registers a faucet for `(origin_token_address, registered_origin_network)` then submits a /// CLAIM whose leaf carries the same `origin_token_address` but a different `origin_network`. @@ -758,14 +1121,15 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( let origin_token_address = leaf_data.origin_token_address; let leaf_origin_network = leaf_data.origin_network; - // The faucet itself stores the leaf's origin_network (so amount scaling is consistent), but - // the bridge is configured to recognize this faucet for a *different* origin_network. + // The CLAIM's leaf carries `leaf_origin_network`; the bridge is configured to recognize + // this faucet for a *different* origin_network, so token-registry lookup must miss. let registered_origin_network = leaf_origin_network.wrapping_add(1); assert_ne!( registered_origin_network, leaf_origin_network, "test setup: registered network must differ from the leaf's network" ); let scale = 10u8; + let metadata_hash = leaf_data.metadata_hash; let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, @@ -774,10 +1138,6 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( max_supply, Felt::ZERO, bridge_account.id(), - &origin_token_address, - leaf_origin_network, - scale, - leaf_data.metadata_hash, ); builder.add_account(agglayer_faucet.clone())?; @@ -811,9 +1171,14 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( // `leaf_origin_network`, so `lookup_faucet_by_token_address` will compute a different key // and find no entry. let config_note = ConfigAggBridgeNote::create( - agglayer_faucet.id(), - &origin_token_address, - registered_origin_network, + ConversionMetadata { + faucet_account_id: agglayer_faucet.id(), + origin_token_address, + scale, + origin_network: registered_origin_network, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -860,27 +1225,3 @@ async fn test_claim_fails_when_origin_network_unregistered() -> anyhow::Result<( Ok(()) } - -#[tokio::test] -async fn solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { - let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; - - assert_eq!(merkle_paths.leaves.len(), merkle_paths.roots.len()); - assert_eq!(merkle_paths.leaves.len() * 32, merkle_paths.merkle_paths.len()); - - for leaf_index in 0..32 { - let source = merkle_proof_verification_code(leaf_index, merkle_paths); - - let tx_script = CodeBuilder::new() - .with_statically_linked_library(&agglayer_library())? - .compile_tx_script(source)?; - - TransactionContextBuilder::with_existing_mock_account() - .tx_script(tx_script.clone()) - .build()? - .execute() - .await - .context(format!("failed to execute transaction with leaf index {leaf_index}"))?; - } - Ok(()) -} diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 943f3ca1cf..4438ee2aa1 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -9,6 +9,7 @@ use miden_agglayer::{ AggLayerBridge, B2AggNote, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, ExitRoot, Keccak256Output, @@ -100,7 +101,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { .collect::>(); let total_burned: u64 = expected_amounts.iter().sum(); - // CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI) + // CREATE AGGLAYER FAUCET ACCOUNT // -------------------------------------------------------------------------------------------- let origin_token_address = EthAddress::from_hex(&vectors.origin_token_address) .expect("valid shared origin token address"); @@ -118,18 +119,19 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { Felt::new(FungibleAsset::MAX_AMOUNT), Felt::new(total_burned), bridge_account.id(), - &origin_token_address, - origin_network, - scale, - metadata_hash, ); builder.add_account(faucet.clone())?; // CONFIG_AGG_BRIDGE note to register the faucet in the bridge (sent by bridge admin) let config_note = ConfigAggBridgeNote::create( - faucet.id(), - &origin_token_address, - origin_network, + ConversionMetadata { + faucet_account_id: faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -176,11 +178,8 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { let mut burn_note_ids = Vec::with_capacity(note_count); for (i, note) in notes.iter().enumerate() { - let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; - let executed_tx = mock_chain .build_tx_context(bridge_account.clone(), &[note.id()], &[])? - .foreign_accounts(vec![foreign_account_inputs]) .build()? .execute() .await?; @@ -388,17 +387,18 @@ async fn bridge_out_at_high_num_leaves(#[case] initial_num_leaves: u32) -> anyho Felt::new(FungibleAsset::MAX_AMOUNT), Felt::new(amount), bridge_account.id(), - &origin_token_address, - origin_network, - scale, - metadata_hash, ); builder.add_account(faucet.clone())?; let config_note = ConfigAggBridgeNote::create( - faucet.id(), - &origin_token_address, - origin_network, + ConversionMetadata { + faucet_account_id: faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -497,12 +497,6 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> // CREATE AGGLAYER FAUCET ACCOUNT (NOT registered in the bridge) // -------------------------------------------------------------------------------------------- let vectors = &*SOLIDITY_MTF_VECTORS; - let origin_token_address = EthAddress::new([0u8; 20]); - 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_symbol, @@ -510,10 +504,6 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> Felt::new(FungibleAsset::MAX_AMOUNT), Felt::new(100), bridge_account.id(), - &origin_token_address, - 0, // origin_network - 0, // scale - metadata_hash, ); builder.add_account(faucet.clone())?; @@ -540,11 +530,8 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> // ATTEMPT TO BRIDGE OUT WITHOUT REGISTERING THE FAUCET (SHOULD FAIL) // -------------------------------------------------------------------------------------------- - let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; - let result = mock_chain .build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])? - .foreign_accounts(vec![foreign_account_inputs]) .build()? .execute() .await; @@ -600,19 +587,20 @@ async fn test_bridge_out_fails_when_destination_is_miden_network() -> anyhow::Re Felt::new(FungibleAsset::MAX_AMOUNT), Felt::new(100), bridge_account.id(), - &origin_token_address, - origin_network, - 0u8, - metadata_hash, ); builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) // -------------------------------------------------------------------------------------------- let config_note = ConfigAggBridgeNote::create( - faucet.id(), - &origin_token_address, - origin_network, + ConversionMetadata { + faucet_account_id: faucet.id(), + origin_token_address, + scale: 0u8, + origin_network, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -889,3 +877,140 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { Ok(()) } + +/// Tests the bridge-out lock path for Miden-native faucets. +/// +/// When a faucet is registered with `is_native = true`, the bridge does not burn the asset on +/// bridge-out; it locks it in its own vault instead. This test verifies: +/// 1. Registration stores the `is_native = true` flag on the bridge. +/// 2. Consuming a B2AGG note carrying a native asset produces **no** output note (no BURN). +/// 3. The asset ends up in the bridge account's vault. +/// 4. The Local Exit Tree is still advanced (the leaf is committed the same way). +#[tokio::test] +async fn bridge_out_lock_native_token() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // Bridge admin / GER manager / bridge account. + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let mut bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + // Native faucet: network-faucet pattern (not bridge-owned). + let faucet_owner_account_id = AccountId::dummy( + [2; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + let native_faucet = builder.add_existing_network_faucet( + "NATIVE", + 1000, + faucet_owner_account_id, + Some(500), + OwnerControlledInitConfig::OwnerOnly, + )?; + + // Sender of the B2AGG note (any regular wallet). + let sender_account = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // Register the native faucet in the bridge with `is_native = true`. + let origin_token_address = EthAddress::from_hex("0x00000000000000000000000000000000deadbeef") + .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); + + let config_note = ConfigAggBridgeNote::create( + ConversionMetadata { + faucet_account_id: native_faucet.id(), + origin_token_address, + scale, + origin_network, + is_native: true, + metadata_hash, + }, + bridge_admin.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(config_note.clone())); + + // B2AGG note carrying a native asset. + let amount = 42u64; + let bridge_asset: Asset = FungibleAsset::new(native_faucet.id(), amount).unwrap().into(); + let destination_network = 1u32; + let destination_address = EthAddress::from_hex("0x1234567890abcdef1122334455667788990011aa") + .expect("valid destination address"); + + let b2agg_note = B2AggNote::create( + destination_network, + destination_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + sender_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(b2agg_note.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // TX0: register the faucet. + let config_executed = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()? + .execute() + .await?; + bridge_account.apply_delta(config_executed.account_delta())?; + mock_chain.add_pending_executed_transaction(&config_executed)?; + mock_chain.prove_next_block()?; + + // TX1: consume the B2AGG note against the bridge (triggers lock_asset). + let executed_tx = mock_chain + .build_tx_context(bridge_account.clone(), &[b2agg_note.id()], &[])? + .build()? + .execute() + .await?; + + // No BURN note is emitted on the lock path. + assert_eq!( + executed_tx.output_notes().num_notes(), + 0, + "Lock path should not emit any output note" + ); + + bridge_account.apply_delta(executed_tx.account_delta())?; + + // The asset now lives in the bridge's own vault. + let bridge_balance = bridge_account.vault().get_balance(native_faucet.id())?; + assert_eq!(bridge_balance, amount, "Bridge vault should hold the locked asset"); + + // Leaf was still committed to the LET; LER is non-zero. + assert_eq!( + AggLayerBridge::read_let_num_leaves(&bridge_account), + 1, + "LET should have exactly one leaf after the lock" + ); + let local_exit_root = AggLayerBridge::read_local_exit_root(&bridge_account)?; + assert!( + local_exit_root.iter().any(|f| f.as_canonical_u64() != 0), + "Local Exit Root should be non-zero after the lock" + ); + + mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.prove_next_block()?; + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index cfbb11745e..ae2f95b51d 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -5,7 +5,9 @@ use alloc::vec::Vec; use miden_agglayer::{ AggLayerBridge, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, + MetadataHash, create_existing_bridge_account, }; use miden_protocol::account::auth::AuthScheme; @@ -76,14 +78,20 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { ); // CREATE CONFIG_AGG_BRIDGE NOTE - // Use a dummy origin token address for this test let origin_token_address = EthAddress::from_hex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let scale = 0u8; let origin_network = 1u32; + let metadata_hash = MetadataHash::from_token_info("USD Coin", "USDC", 6); let config_note = ConfigAggBridgeNote::create( - faucet_to_register, - &origin_token_address, - origin_network, + ConversionMetadata { + faucet_account_id: faucet_to_register, + origin_token_address, + scale, + origin_network, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -93,9 +101,8 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { let mock_chain = builder.build()?; // CONSUME THE CONFIG_AGG_BRIDGE NOTE WITH THE BRIDGE ACCOUNT - let tx_context = mock_chain - .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? - .build()?; + let tx_context = + mock_chain.build_tx_context(bridge_account.id(), &[], &[config_note])?.build()?; let executed_transaction = tx_context.execute().await?; // VERIFY FAUCET IS NOW REGISTERED @@ -164,18 +171,29 @@ async fn test_config_agg_bridge_distinguishes_origin_network() -> anyhow::Result let origin_network_1: u32 = 1; let origin_network_2: u32 = 2; + let metadata_hash = MetadataHash::from_token_info("USD Coin", "USDC", 6); let config_note_1 = ConfigAggBridgeNote::create( - faucet_network_1, - &origin_token_address, - origin_network_1, + ConversionMetadata { + faucet_account_id: faucet_network_1, + origin_token_address, + scale: 0, + origin_network: origin_network_1, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), )?; let config_note_2 = ConfigAggBridgeNote::create( - faucet_network_2, - &origin_token_address, - origin_network_2, + ConversionMetadata { + faucet_account_id: faucet_network_2, + origin_token_address, + scale: 0, + origin_network: origin_network_2, + is_native: false, + metadata_hash, + }, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index 84ea5b226c..6bdb62c53e 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -2,8 +2,6 @@ extern crate alloc; use miden_agglayer::{ AggLayerFaucet, - EthAddress, - MetadataHash, create_existing_agglayer_faucet, create_existing_bridge_account, }; @@ -36,13 +34,6 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let token_supply = Felt::new(123_456); - let origin_token_address = EthAddress::from_hex("0x0102030405060708090a0b0c0d0e0f1011121314") - .expect("invalid token address"); - let origin_network = 42u32; - let scale = 6u8; - - let metadata_hash = MetadataHash::from_token_info(token_symbol, token_symbol, decimals); - let faucet = create_existing_agglayer_faucet( builder.rng_mut().draw_word(), token_symbol, @@ -50,16 +41,9 @@ fn test_faucet_helper_methods() -> anyhow::Result<()> { max_supply, token_supply, bridge_account.id(), - &origin_token_address, - origin_network, - scale, - metadata_hash, ); assert_eq!(AggLayerFaucet::owner_account_id(&faucet)?, bridge_account.id()); - assert_eq!(AggLayerFaucet::origin_token_address(&faucet)?, origin_token_address); - assert_eq!(AggLayerFaucet::origin_network(&faucet)?, origin_network); - assert_eq!(AggLayerFaucet::scale(&faucet)?, scale); Ok(()) }