From a1db91ef4e48f5b8cd516f93f0ad3d9c5945a753 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 14 Apr 2026 15:27:12 -0400 Subject: [PATCH 01/28] feat: add scale data to bridge storage --- bin/bench-transaction/src/context_setups.rs | 2 + .../asm/agglayer/bridge/bridge_config.masm | 90 +++++++++++++++---- .../asm/agglayer/bridge/bridge_in.masm | 32 ++----- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 45 ++++++---- crates/miden-agglayer/src/bridge.rs | 17 ++++ crates/miden-agglayer/src/config_note.rs | 14 +-- crates/miden-agglayer/src/errors/agglayer.rs | 4 +- .../miden-testing/tests/agglayer/bridge_in.rs | 2 + .../tests/agglayer/bridge_out.rs | 1 + .../tests/agglayer/config_bridge.rs | 3 +- 10 files changed, 142 insertions(+), 68 deletions(-) diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs index b399402fe1..b493a4ded1 100644 --- a/bin/bench-transaction/src/context_setups.rs +++ b/bin/bench-transaction/src/context_setups.rs @@ -246,6 +246,7 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result Result { let config_note = ConfigAggBridgeNote::create( faucet.id(), &origin_token_address, + scale, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 4df7db2aac..2aa3d3a677 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -22,6 +22,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 @@ -102,14 +103,16 @@ 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. #! #! 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]) -> [faucet_id_suffix, faucet_id_prefix, 0, 0]` into the #! `token_registry` map. +#! 3. Writes `[1, 0, faucet_id_suffix, faucet_id_prefix] -> [0, 0, scale, 0]` into the +#! `faucet_metadata` map. #! -#! Inputs: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)] +#! Inputs: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] #! Outputs: [pad(16)] #! #! Panics if: @@ -119,12 +122,16 @@ end pub proc register_faucet # assert the note sender is the bridge admin. exec.assert_sender_is_bridge_admin - # => [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)] + # => [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] - # Save faucet ID for later use in token_registry - dup.6 dup.6 - # => [faucet_id_suffix, faucet_id_prefix, origin_token_addr(5), - # faucet_id_suffix, faucet_id_prefix, pad(9)] + # Save scale for later use in faucet_metadata_map + movup.7 + # => [scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + + # Save faucet ID for later use in token_registry and metadata_map + dup.7 dup.7 + # => [faucet_id_suffix, faucet_id_prefix, scale, origin_token_addr(5), + # faucet_id_suffix, faucet_id_prefix, pad(8)] # --- 1. Register faucet in faucet_registry --- @@ -132,39 +139,66 @@ pub proc register_faucet # 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), - # faucet_id_suffix, faucet_id_prefix, pad(9)] + # faucet_id_suffix, faucet_id_prefix, scale, origin_token_addr(5), + # faucet_id_suffix, faucet_id_prefix, pad(8)] 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), faucet_id_suffix, faucet_id_prefix, pad(9) + # scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8) # ] push.FAUCET_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item - # => [OLD_VALUE, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)] + # => [OLD_VALUE, scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + + dropw + # => [scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + + # --- 2. Store scale in faucet_metadata_map --- + # KEY = [1, 0, faucet_id_suffix, faucet_id_prefix] + # VALUE = [0, 0, scale, 0] + + # Save faucet ID copies for step 3 + dup.6 dup.6 + # => [faucet_id_suffix, faucet_id_prefix, scale, origin_token_addr(5), + # faucet_id_suffix, faucet_id_prefix, pad(8)] + + # Build VALUE = [0, 0, scale, 0] + movup.2 push.0 swap push.0.0 + # => [0, 0, scale, 0, faucet_id_suffix, faucet_id_prefix, origin_token_addr(5), + # faucet_id_suffix, faucet_id_prefix, pad(8)] + + # Build KEY = [1, 0, faucet_id_suffix, faucet_id_prefix] + movup.5 movup.5 push.0.1 + # => [1, 0, faucet_id_suffix, faucet_id_prefix, + # 0, 0, scale, 0, origin_token_addr(5), + # faucet_id_suffix, faucet_id_prefix, pad(8)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item + # => [OLD_VALUE, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] dropw - # => [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)] + # => [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] - # --- 2. Register token address → faucet ID in token_registry --- + # --- 3. Register token address → faucet ID in token_registry --- # Hash the token address exec.hash_token_address - # => [TOKEN_ADDR_HASH, faucet_id_suffix, faucet_id_prefix, pad(10)] + # => [TOKEN_ADDR_HASH, faucet_id_suffix, faucet_id_prefix, pad(9)] # 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(10)] + # => [0, 0, faucet_id_suffix, faucet_id_prefix, TOKEN_ADDR_HASH, pad(9)] swapw - # => [TOKEN_ADDR_HASH, 0, 0, faucet_id_suffix, faucet_id_prefix, pad(10)] + # => [TOKEN_ADDR_HASH, 0, 0, faucet_id_suffix, faucet_id_prefix, pad(9)] push.TOKEN_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item - # => [OLD_VALUE, pad(12)] + # => [OLD_VALUE, pad(11)] dropw # => [pad(16)] @@ -195,6 +229,28 @@ proc assert_faucet_registered # => [] end +#! Returns the scale factor for a registered faucet from the bridge's faucet metadata map. +#! +#! Reads the scale from the faucet_metadata_map at key [1, 0, faucet_id_suffix, faucet_id_prefix]. +#! The stored value is [0, 0, scale, 0]. +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Outputs: [scale] +#! +#! Invocation: exec +proc get_faucet_scale + # Build KEY = [1, 0, faucet_id_suffix, faucet_id_prefix] + push.0.1 + # => [1, 0, faucet_id_suffix, faucet_id_prefix] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [0, 0, scale, 0] + + drop drop swap drop + # => [scale] +end + #! Looks up the faucet account ID for a given origin token address. #! #! Hashes the origin token address (5 felts) and looks up the result in the token_registry map. diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index f101c90b40..857cd83df1 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -3,7 +3,6 @@ use agglayer::bridge::leaf_utils 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 @@ -13,7 +12,6 @@ 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 @@ -432,7 +430,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)`. @@ -441,44 +439,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 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 98df2690fd..90a2f68f77 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -5,24 +5,26 @@ use miden::standards::attachments::network_account_target # CONSTANTS # ================================================================================================= -const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 7 +const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 8 const STORAGE_START_PTR = 0 const ORIGIN_TOKEN_ADDR_0 = STORAGE_START_PTR const ORIGIN_TOKEN_ADDR_4 = 4 +const SCALE_ADDR = 7 # ERRORS # ================================================================================================= -const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS = "CONFIG_AGG_BRIDGE expects exactly 7 note storage items" +const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS = "CONFIG_AGG_BRIDGE expects exactly 8 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, and scale factor +#! in the bridge. #! #! Requires that the account exposes: #! - agglayer::bridge_config::register_faucet procedure. @@ -30,7 +32,7 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] #! -#! NoteStorage layout (7 felts total): +#! 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 @@ -38,14 +40,16 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! - origin_token_addr_4 [4] : 1 felt #! - faucet_id_suffix [5] : 1 felt #! - faucet_id_prefix [6] : 1 felt +#! - scale [7] : 1 felt #! #! Where: #! - faucet_id_suffix: Suffix felt of the faucet account ID to register. #! - faucet_id_prefix: Prefix felt of the faucet account ID to register. +#! - scale: Decimal scaling factor for amount conversion (e.g. 0 for USDC, 8 for ETH). #! #! Panics if: #! - The note attachment target account does not match the consuming bridge account. -#! - The note does not contain exactly 7 storage items. +#! - The note does not contain exactly 8 storage items. #! - The account does not expose the register_faucet procedure. begin dropw @@ -64,22 +68,29 @@ 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) and faucet_id from memory - # register_faucet expects: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(9)] + # Load origin_token_addr(5), faucet_id, and scale from memory + # register_faucet expects: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] - # Load origin_token_addr_4, faucet_id_suffix, and faucet_id_prefix onto the sack. Notice that we - # can use `mem_loadw_le` here: that allows us to reuse the existing zeros on the stack, and - # since note memory was not initialized, fourth element on the stack will be equal ZERO, which - # is what we want. - mem_loadw_le.ORIGIN_TOKEN_ADDR_4 - # => [addr4, faucet_id_suffix, faucet_id_prefix, pad(13)] + # Load scale from memory + mem_load.SCALE_ADDR + # => [scale, pad(16)] + + # Load origin_token_addr_4, faucet_id_suffix, and faucet_id_prefix onto the stack. + # Note: mem_loadw_le loads 4 felts; the 4th element at ORIGIN_TOKEN_ADDR_4+3 = addr 7 = scale, + # but we already loaded scale above, so we use the word load and drop the extra element. + padw mem_loadw_le.ORIGIN_TOKEN_ADDR_4 + # => [addr4, faucet_id_suffix, faucet_id_prefix, scale_dup, scale, pad(12)] + + # Drop the duplicate scale value from the word load + movup.3 drop + # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(12)] # Load remaining origin_token_addr_[0..3] onto the stack padw mem_loadw_le.ORIGIN_TOKEN_ADDR_0 - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, pad(13)] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(12)] # Register the faucet in the bridge - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, pad(9), pad(4)] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(8), pad(4)] call.bridge_config::register_faucet # => [pad(16), pad(4)] diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 2ec155232b..3a41dc6500 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. @@ -186,6 +193,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. @@ -412,6 +427,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, @@ -434,6 +450,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 efdd9f6663..b5c2962742 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -56,8 +56,8 @@ impl ConfigAggBridgeNote { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for a CONFIG_AGG_BRIDGE note. - /// Layout: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix] - pub const NUM_STORAGE_ITEMS: usize = 7; + /// Layout: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale] + pub const NUM_STORAGE_ITEMS: usize = 8; // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -77,14 +77,16 @@ impl ConfigAggBridgeNote { /// Creates a CONFIG_AGG_BRIDGE note to register a faucet in the bridge's registry. /// - /// The note storage contains 7 felts: + /// The note storage contains 8 felts: /// - `origin_token_addr[0..5]`: The 5 u32 felts of the origin EVM token address /// - `faucet_id_suffix`: The suffix of the faucet account ID /// - `faucet_id_prefix`: The prefix of the faucet account ID + /// - `scale`: The decimal scaling factor for amount conversion /// /// # Parameters /// - `faucet_account_id`: The account ID of the faucet to register /// - `origin_token_address`: The origin EVM token address for the token registry + /// - `scale`: The decimal scaling factor (e.g. 0 for USDC, 8 for ETH) /// - `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 @@ -94,16 +96,18 @@ impl ConfigAggBridgeNote { pub fn create( faucet_account_id: AccountId, origin_token_address: &EthAddress, + scale: u8, sender_account_id: AccountId, target_account_id: AccountId, rng: &mut R, ) -> Result { - // Create note storage with 7 felts: [origin_token_addr(5), faucet_id_suffix, - // faucet_id_prefix] + // Create note storage with 8 felts: [origin_token_addr(5), faucet_id_suffix, + // faucet_id_prefix, scale] let addr_elements = origin_token_address.to_elements(); let mut storage_values: Vec = addr_elements; storage_values.push(faucet_account_id.suffix()); storage_values.push(faucet_account_id.prefix().as_felt()); + storage_values.push(Felt::from(scale)); let note_storage = NoteStorage::new(storage_values)?; diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 045c3226d6..cc40179562 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -28,8 +28,8 @@ pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str /// Error Message: "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" pub const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE note attachment target account does not match consuming account"); -/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 7 note storage items" -pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 7 note storage items"); +/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 8 note storage items" +pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 8 note storage items"); /// Error Message: "faucet is not registered in the bridge's faucet registry" pub const ERR_FAUCET_NOT_REGISTERED: MasmError = MasmError::from_static_str("faucet is not registered in the bridge's faucet registry"); diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 9e45a965ab..1fba7155d4 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -248,6 +248,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a let config_note = ConfigAggBridgeNote::create( agglayer_faucet.id(), &origin_token_address, + scale, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -515,6 +516,7 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { let config_note = ConfigAggBridgeNote::create( agglayer_faucet.id(), &origin_token_address, + scale, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index e0a61d3e47..a1f947d0eb 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -113,6 +113,7 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { let config_note = ConfigAggBridgeNote::create( faucet.id(), &origin_token_address, + scale, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index f4e760ba58..893bad3110 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -63,12 +63,13 @@ 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 config_note = ConfigAggBridgeNote::create( faucet_to_register, &origin_token_address, + scale, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), From 7cf98435d86508881a98c54b24c275e4d6799a0d Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 15 Apr 2026 13:12:54 -0400 Subject: [PATCH 02/28] feat: update config_agg_bridge note & fix register_faucet proc --- .../asm/agglayer/bridge/bridge_config.masm | 2 +- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 29 +++++-------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 2aa3d3a677..72fee6f69c 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -161,7 +161,7 @@ pub proc register_faucet # VALUE = [0, 0, scale, 0] # Save faucet ID copies for step 3 - dup.6 dup.6 + dup.7 dup.7 # => [faucet_id_suffix, faucet_id_prefix, scale, origin_token_addr(5), # faucet_id_suffix, faucet_id_prefix, pad(8)] 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 90a2f68f77..d8ed9d2f24 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -10,7 +10,6 @@ const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 8 const STORAGE_START_PTR = 0 const ORIGIN_TOKEN_ADDR_0 = STORAGE_START_PTR const ORIGIN_TOKEN_ADDR_4 = 4 -const SCALE_ADDR = 7 # ERRORS # ================================================================================================= @@ -71,30 +70,16 @@ begin # Load origin_token_addr(5), faucet_id, and scale from memory # register_faucet expects: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] - # Load scale from memory - mem_load.SCALE_ADDR - # => [scale, pad(16)] - - # Load origin_token_addr_4, faucet_id_suffix, and faucet_id_prefix onto the stack. - # Note: mem_loadw_le loads 4 felts; the 4th element at ORIGIN_TOKEN_ADDR_4+3 = addr 7 = scale, - # but we already loaded scale above, so we use the word load and drop the extra element. - padw mem_loadw_le.ORIGIN_TOKEN_ADDR_4 - # => [addr4, faucet_id_suffix, faucet_id_prefix, scale_dup, scale, pad(12)] - - # Drop the duplicate scale value from the word load - movup.3 drop + # Load origin_token_addr_4, faucet_id_suffix, faucet_id_prefix, and scale as a word. + # Memory layout at addr 4: [addr4, faucet_id_suffix, faucet_id_prefix, scale] + mem_loadw_le.ORIGIN_TOKEN_ADDR_4 # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(12)] - # Load remaining origin_token_addr_[0..3] onto the stack - padw mem_loadw_le.ORIGIN_TOKEN_ADDR_0 - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(12)] + # Load remaining origin_token_addr_[0..3] onto the stack, replacing 4 pad elements + swapw mem_loadw_le.ORIGIN_TOKEN_ADDR_0 + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(8)] - # Register the faucet in the bridge - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(8), pad(4)] - + # Register the faucet in the bridge (stack is exactly 16) call.bridge_config::register_faucet - # => [pad(16), pad(4)] - - dropw # => [pad(16)] end From 8ba2cd532e29bdff417b6b38ec1f4aa0e53c843a Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 15 Apr 2026 18:32:06 -0400 Subject: [PATCH 03/28] feat: extend faucet registration with full metadata, is_native flag, and metadata hash --- bin/bench-transaction/src/context_setups.rs | 8 + .../asm/agglayer/bridge/bridge_config.masm | 310 ++++++++++++++---- .../asm/agglayer/bridge/bridge_in.masm | 2 +- .../miden-agglayer/asm/components/bridge.masm | 2 + .../asm/note_scripts/CLAIM.masm | 2 + .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 157 +++++++-- crates/miden-agglayer/src/config_note.rs | 53 ++- crates/miden-agglayer/src/errors/agglayer.rs | 4 +- .../miden-testing/tests/agglayer/bridge_in.rs | 7 + .../tests/agglayer/bridge_out.rs | 3 + .../tests/agglayer/config_bridge.rs | 11 +- 11 files changed, 442 insertions(+), 117 deletions(-) diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs index b493a4ded1..a116cac421 100644 --- a/bin/bench-transaction/src/context_setups.rs +++ b/bin/bench-transaction/src/context_setups.rs @@ -227,6 +227,7 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result Result Result { builder.add_account(faucet.clone())?; // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) + let metadata_hash = MetadataHash::from_token_info("AGG", "AGG", 8); let config_note = ConfigAggBridgeNote::create( faucet.id(), &origin_token_address, scale, + origin_network, + false, + &metadata_hash, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 72fee6f69c..8fe1d7baec 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -31,6 +31,14 @@ 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 offsets for `register_faucet` +const REG_TOKEN_HASH_LOC = 0 +const REG_FID_S_LOC = 4 +const REG_FID_P_LOC = 5 +const REG_SCALE_LOC = 6 +const REG_ORIGIN_NETWORK_LOC = 7 +const REG_IS_NATIVE_LOC = 8 + # PUBLIC INTERFACE # ================================================================================================= @@ -105,101 +113,167 @@ end #! Registers a faucet in the bridge's faucet registry, token registry, and metadata map. #! -#! 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]) -> [faucet_id_suffix, faucet_id_prefix, 0, 0]` into the -#! `token_registry` map. -#! 3. Writes `[1, 0, faucet_id_suffix, faucet_id_prefix] -> [0, 0, scale, 0]` into the -#! `faucet_metadata` map. +#! Stores conversion metadata for the faucet using a sub-key scheme in faucet_metadata_map: +#! 1. KEY [0, 0, fid_s, fid_p] -> [addr0, addr1, addr2, addr3] (origin address part 1) +#! 2. KEY [1, 0, fid_s, fid_p] -> [addr4, origin_network, scale, 0] (origin address part 2) +#! +#! Also registers: +#! 3. faucet_registry_map: [0, 0, fid_s, fid_p] -> [1, is_native, 0, 0] +#! 4. token_registry_map: hash(tokenAddress) -> [fid_s, fid_p, 0, 0] #! -#! Inputs: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] +#! Inputs: [origin_token_addr(5), fid_s, fid_p, 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), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] + # => [addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native, pad(6)] - # Save scale for later use in faucet_metadata_map - movup.7 - # => [scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + # Save non-address data to locals + movup.9 loc_store.REG_IS_NATIVE_LOC + movup.8 loc_store.REG_ORIGIN_NETWORK_LOC + movup.7 loc_store.REG_SCALE_LOC + movup.6 loc_store.REG_FID_P_LOC + movup.5 loc_store.REG_FID_S_LOC + # => [addr0, addr1, addr2, addr3, addr4, pad(11)] - # Save faucet ID for later use in token_registry and metadata_map - dup.7 dup.7 - # => [faucet_id_suffix, faucet_id_prefix, scale, origin_token_addr(5), - # faucet_id_suffix, faucet_id_prefix, pad(8)] + # Duplicate address for hashing before it gets consumed + dup.4 dup.4 dup.4 dup.4 dup.4 + # => [addr0, addr1, addr2, addr3, addr4, addr0, addr1, addr2, addr3, addr4, pad(6)] - # --- 1. Register faucet in faucet_registry --- + exec.hash_token_address + # => [TOKEN_ADDR_HASH(4), addr0, addr1, addr2, addr3, addr4, pad(6)] - # 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, scale, origin_token_addr(5), - # faucet_id_suffix, faucet_id_prefix, pad(8)] + loc_storew_le.REG_TOKEN_HASH_LOC dropw + # => [addr0, addr1, addr2, addr3, addr4, pad(11)] - movup.5 movup.5 push.0.0 - # => [ - # [0, 0, faucet_id_suffix, faucet_id_prefix], - # [IS_FAUCET_REGISTERED_FLAG, 0, 0, 0], - # scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8) - # ] + # --- Step 1: Store origin address part 1 in faucet_metadata_map --- + # KEY = [0, 0, fid_s, fid_p], VALUE = [addr0, addr1, addr2, addr3] - push.FAUCET_REGISTRY_MAP_SLOT[0..2] - exec.native_account::set_map_item - # => [OLD_VALUE, scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC + # => [fid_s, fid_p, addr0, addr1, addr2, addr3, addr4, pad(11)] + + push.0.0 + # => [0, 0, fid_s, fid_p, addr0, addr1, addr2, addr3, addr4, pad(11)] + + movup.7 movup.7 movup.7 movup.7 + # => [addr0, addr1, addr2, addr3, 0, 0, fid_s, fid_p, addr4, pad(11)] + + swapw + # => [0, 0, fid_s, fid_p, addr0, addr1, addr2, addr3, addr4, pad(11)] + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item dropw - # => [scale, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + # => [addr4, pad(11)] - # --- 2. Store scale in faucet_metadata_map --- - # KEY = [1, 0, faucet_id_suffix, faucet_id_prefix] - # VALUE = [0, 0, scale, 0] + # --- Step 2: Store origin address part 2 + origin_network + scale --- + # KEY = [1, 0, fid_s, fid_p], VALUE = [addr4, origin_network, scale, 0] - # Save faucet ID copies for step 3 - dup.7 dup.7 - # => [faucet_id_suffix, faucet_id_prefix, scale, origin_token_addr(5), - # faucet_id_suffix, faucet_id_prefix, pad(8)] + loc_load.REG_ORIGIN_NETWORK_LOC + swap + loc_load.REG_SCALE_LOC + movdn.2 + push.0 movdn.3 + # => [addr4, origin_network, scale, 0, pad(11)] - # Build VALUE = [0, 0, scale, 0] - movup.2 push.0 swap push.0.0 - # => [0, 0, scale, 0, faucet_id_suffix, faucet_id_prefix, origin_token_addr(5), - # faucet_id_suffix, faucet_id_prefix, pad(8)] + loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC + # => [fid_s, fid_p, addr4, origin_network, scale, 0, pad(11)] - # Build KEY = [1, 0, faucet_id_suffix, faucet_id_prefix] - movup.5 movup.5 push.0.1 - # => [1, 0, faucet_id_suffix, faucet_id_prefix, - # 0, 0, scale, 0, origin_token_addr(5), - # faucet_id_suffix, faucet_id_prefix, pad(8)] + push.0.1 + # => [1, 0, fid_s, fid_p, addr4, origin_network, scale, 0, pad(11)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item - # => [OLD_VALUE, origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] - dropw - # => [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, pad(8)] + # => [pad(11)] - # --- 3. Register token address → faucet ID in token_registry --- + # --- Step 3: Store [1, is_native, 0, 0] in faucet_registry_map --- - # Hash the token address - exec.hash_token_address - # => [TOKEN_ADDR_HASH, faucet_id_suffix, faucet_id_prefix, pad(9)] + loc_load.REG_IS_NATIVE_LOC + loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC + # => [fid_s, fid_p, is_native, pad(11)] + + push.0.0 movup.4 push.IS_FAUCET_REGISTERED_FLAG + # => [1, is_native, 0, 0, fid_s, fid_p, pad(11)] - # 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(9)] + # => [0, 0, fid_s, fid_p, 1, is_native, 0, 0, pad(11)] - swapw - # => [TOKEN_ADDR_HASH, 0, 0, faucet_id_suffix, faucet_id_prefix, pad(9)] + push.FAUCET_REGISTRY_MAP_SLOT[0..2] + exec.native_account::set_map_item + dropw + # => [pad(11)] + + # --- Step 4: Store TOKEN_ADDR_HASH -> [fid_s, fid_p, 0, 0] in token_registry --- + + loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC + # => [fid_s, fid_p, pad(11)] + + push.0.0 + # => [0, 0, fid_s, fid_p, pad(11)] + + padw loc_loadw_le.REG_TOKEN_HASH_LOC + # => [TOKEN_ADDR_HASH(4), 0, 0, fid_s, fid_p, pad(7)] push.TOKEN_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item - # => [OLD_VALUE, pad(11)] + 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, fid_s, fid_p] -> METADATA_HASH_LO +#! - KEY [3, 0, fid_s, fid_p] -> METADATA_HASH_HI +#! +#! Inputs: [fid_s, fid_p, METADATA_HASH_LO(4), METADATA_HASH_HI(4), 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 + # => [fid_s, fid_p, mh_lo0, mh_lo1, mh_lo2, mh_lo3, mh_hi0, mh_hi1, mh_hi2, mh_hi3, pad(6)] + # --- Store METADATA_HASH_LO at key [2, 0, fid_s, fid_p] --- + dup.1 dup.1 + # => [fid_s, fid_p, fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + + push.0.2 + # => [2, 0, fid_s, fid_p, fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + + movup.7 movup.7 movup.7 movup.7 + # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, 2, 0, fid_s, fid_p, fid_s, fid_p, MH_HI(4), pad(6)] + + swapw + # => [2, 0, fid_s, fid_p, MH_LO(4), fid_s, fid_p, MH_HI(4), pad(6)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item + dropw + # => [fid_s, fid_p, MH_HI(4), pad(6)] + + # --- Store METADATA_HASH_HI at key [3, 0, fid_s, fid_p] --- + push.0.3 + # => [3, 0, fid_s, fid_p, MH_HI(4), pad(6)] + + movup.7 movup.7 movup.7 movup.7 + # => [mh_hi0, mh_hi1, mh_hi2, mh_hi3, 3, 0, fid_s, fid_p, pad(6)] + + swapw + # => [3, 0, fid_s, fid_p, MH_HI(4), pad(6)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.native_account::set_map_item dropw # => [pad(16)] end @@ -207,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 [1, is_native, 0, 0] where element 0 is the registration flag. #! #! Inputs: [faucet_id_suffix, faucet_id_prefix] #! Outputs: [] @@ -224,15 +299,16 @@ 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) is non-zero 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 scale from the faucet_metadata_map at key [1, 0, faucet_id_suffix, faucet_id_prefix]. -#! The stored value is [0, 0, scale, 0]. +#! 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] @@ -245,12 +321,116 @@ proc get_faucet_scale push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item - # => [0, 0, scale, 0] + # => [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, fid_s, fid_p] -> [addr0, addr1, addr2, addr3] +#! - Key [1, 0, fid_s, fid_p] -> [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 + # Save faucet_id for second read + dup.1 dup.1 + # => [fid_s, fid_p, fid_s, fid_p] + + # Read sub-key 1: [addr4, origin_network, scale, 0] + push.0.1 + # => [1, 0, fid_s, fid_p, fid_s, fid_p] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [addr4, origin_network, scale, 0, fid_s, fid_p] + + # Drop the trailing 0 + drop + # => [addr4, origin_network, scale, fid_s, fid_p] + + # Save scale, origin_network, addr4 temporarily by moving them below fid + movdn.4 movdn.4 movdn.4 + # => [fid_s, fid_p, addr4, origin_network, scale] + + # Read sub-key 0: [addr0, addr1, addr2, addr3] + push.0.0 + # => [0, 0, fid_s, fid_p, addr4, origin_network, scale] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [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, fid_s, fid_p] -> METADATA_HASH_LO (4 felts) +#! - Key [3, 0, fid_s, fid_p] -> METADATA_HASH_HI (4 felts) +#! +#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Outputs: [METADATA_HASH_LO, METADATA_HASH_HI] +#! +#! Invocation: exec +proc get_faucet_metadata_hash + # Save faucet_id for second read + dup.1 dup.1 + # => [fid_s, fid_p, fid_s, fid_p] + + # Read sub-key 2: METADATA_HASH_LO + push.0.2 + # => [2, 0, fid_s, fid_p, fid_s, fid_p] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [MH_LO(4), fid_s, fid_p] + + # Move fid below MH_LO + movup.5 movup.5 + # => [fid_s, fid_p, MH_LO(4)] + + # Read sub-key 3: METADATA_HASH_HI + push.0.3 + # => [3, 0, fid_s, fid_p, MH_LO(4)] + + push.FAUCET_METADATA_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [MH_HI(4), MH_LO(4)] + + # Rearrange: MH_LO should be on top + swapw + # => [MH_LO(4), MH_HI(4)] +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), keep is_native, drop rest + drop swap drop swap drop + # => [is_native] +end + #! Looks up the faucet account ID for a given origin token address. #! #! Hashes the origin token address (5 felts) and looks up the result in the token_registry map. diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 857cd83df1..40ab5dc0c5 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -232,7 +232,7 @@ 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)] 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/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index 715ab6d7f9..ff698e34c4 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -97,6 +97,8 @@ begin movdn.8 # => [PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(16)] + debug.stack + # call the Bridge Claim procedure call.bridge::claim # => [pad(16), pad(9)] 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 d8ed9d2f24..1a3d7bd088 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -5,16 +5,47 @@ 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 + +# Memory layout after get_storage (18 felts): +# [0] origin_token_addr_0 +# [1] origin_token_addr_1 +# [2] origin_token_addr_2 +# [3] origin_token_addr_3 +# [4] origin_token_addr_4 +# [5] faucet_id_suffix +# [6] faucet_id_prefix +# [7] scale +# [8] origin_network +# [9] is_native +# [10..13] METADATA_HASH_LO_0..3 (4 felts) +# [14..17] METADATA_HASH_HI_0..3 (4 felts) + const ORIGIN_TOKEN_ADDR_0 = STORAGE_START_PTR +const ORIGIN_TOKEN_ADDR_1 = STORAGE_START_PTR + 1 +const ORIGIN_TOKEN_ADDR_2 = STORAGE_START_PTR + 2 +const ORIGIN_TOKEN_ADDR_3 = STORAGE_START_PTR + 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, @@ -22,64 +53,122 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! #! 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, origin token address mapping, and scale factor -#! 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 -#! - faucet_id_suffix [5] : 1 felt -#! - faucet_id_prefix [6] : 1 felt -#! - scale [7] : 1 felt -#! -#! Where: -#! - faucet_id_suffix: Suffix felt of the faucet account ID to register. -#! - faucet_id_prefix: Prefix felt of the faucet account ID to register. -#! - scale: Decimal scaling factor for amount conversion (e.g. 0 for USDC, 8 for ETH). +#! 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 +#! - is_native [9] : 1 felt (0 or 1) +#! - 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)] + # => [pad(12)] # Ensure note attachment targets the consuming bridge account. exec.network_account_target::active_account_matches_target_account assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH - # => [pad(16)] + # => [pad(12)] # Load note storage to memory push.STORAGE_START_PTR exec.active_note::get_storage - # => [num_storage_items, dest_ptr, pad(16)] + # => [num_storage_items, dest_ptr, pad(12)] # Validate the number of storage items push.CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS assert_eq.err=ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS drop - # => [pad(16)] + # => [pad(12)] + + # Drop the remaining pad to start fresh + dropw dropw dropw + # => [] + + # --- Call 1: register_faucet --- + # Expects: [addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native, pad(6)] + + # Build the stack bottom-up: load deepest elements first, then push shallower ones on top. + # This avoids complex rearrangement since each mem_load pushes onto the top. + 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, fid_s, fid_p, scale, origin_network, is_native] - # Load origin_token_addr(5), faucet_id, and scale from memory - # register_faucet expects: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, pad(8)] + # Load addr0..addr3 on top in reverse order so addr0 ends up on top + 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, fid_s, fid_p, scale, origin_network, is_native] - # Load origin_token_addr_4, faucet_id_suffix, faucet_id_prefix, and scale as a word. - # Memory layout at addr 4: [addr4, faucet_id_suffix, faucet_id_prefix, scale] - mem_loadw_le.ORIGIN_TOKEN_ADDR_4 - # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(12)] + # Pad to 16 (need 6 more zeros) + padw push.0.0 + # => [0, 0, 0, 0, 0, 0, addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native] - # Load remaining origin_token_addr_[0..3] onto the stack, replacing 4 pad elements - swapw mem_loadw_le.ORIGIN_TOKEN_ADDR_0 - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, pad(8)] + # Move 6 pad zeros to the end + movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 + # => [addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native, pad(6)] - # Register the faucet in the bridge (stack is exactly 16) call.bridge_config::register_faucet # => [pad(16)] + + # --- Call 2: store_faucet_metadata_hash --- + # Expects: [fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + + # Drop all 16 pad elements and rebuild the stack + dropw dropw dropw dropw + # => [] + + # Load MH_HI from memory addresses 14..17 (individual loads — not word-aligned) + mem_load.METADATA_HASH_HI_3 mem_load.METADATA_HASH_HI_2 + mem_load.METADATA_HASH_HI_1 mem_load.METADATA_HASH_HI_0 + # => [mh_hi0, mh_hi1, mh_hi2, mh_hi3] + + # Load MH_LO from memory addresses 10..13 (individual loads — not word-aligned) + mem_load.METADATA_HASH_LO_3 mem_load.METADATA_HASH_LO_2 + mem_load.METADATA_HASH_LO_1 mem_load.METADATA_HASH_LO_0 + # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, mh_hi0, mh_hi1, mh_hi2, mh_hi3] + + # Load fid_p, fid_s + mem_load.FAUCET_ID_PREFIX + mem_load.FAUCET_ID_SUFFIX + # => [fid_s, fid_p, MH_LO(4), MH_HI(4)] + + # Pad to 16 (need 6 more) + padw push.0.0 + # => [0, 0, 0, 0, 0, 0, fid_s, fid_p, MH_LO(4), MH_HI(4)] + + # Move pad to end + movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 + # => [fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + + call.bridge_config::store_faucet_metadata_hash + # => [pad(16)] + + dropw dropw dropw dropw end diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index b5c2962742..6bb8f08490 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 // ================================================================================================ @@ -47,8 +47,9 @@ static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock = LazyLock::new(|| { /// 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 +57,17 @@ impl ConfigAggBridgeNote { // -------------------------------------------------------------------------------------------- /// Expected number of storage items for a CONFIG_AGG_BRIDGE note. - /// Layout: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale] - 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 + /// - `[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,37 +87,56 @@ 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 - /// - `faucet_id_suffix`: The suffix of the faucet account ID - /// - `faucet_id_prefix`: The prefix of the faucet account ID - /// - `scale`: The decimal scaling factor for amount conversion + /// The note storage contains 18 felts carrying all the data needed for faucet registration: + /// - Origin token address (5 felts) + /// - Faucet account ID (2 felts) + /// - Scale factor (1 felt) + /// - Origin network (1 felt) + /// - Is-native flag (1 felt) + /// - Metadata hash (8 felts) /// /// # Parameters /// - `faucet_account_id`: The account ID of the faucet to register /// - `origin_token_address`: The origin EVM token address for the token registry /// - `scale`: The decimal scaling factor (e.g. 0 for USDC, 8 for ETH) + /// - `origin_network`: The origin network/chain ID + /// - `is_native`: Whether this is a Miden-native faucet (lock/unlock) vs bridge-owned + /// (burn/mint) + /// - `metadata_hash`: The keccak256 hash of ABI-encoded token metadata /// - `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. + #[allow(clippy::too_many_arguments)] pub fn create( faucet_account_id: AccountId, origin_token_address: &EthAddress, scale: u8, + origin_network: u32, + is_native: bool, + metadata_hash: &MetadataHash, sender_account_id: AccountId, target_account_id: AccountId, rng: &mut R, ) -> Result { - // Create note storage with 8 felts: [origin_token_addr(5), faucet_id_suffix, - // faucet_id_prefix, scale] + // Create note storage with 18 felts let addr_elements = origin_token_address.to_elements(); let mut storage_values: Vec = addr_elements; storage_values.push(faucet_account_id.suffix()); storage_values.push(faucet_account_id.prefix().as_felt()); storage_values.push(Felt::from(scale)); + storage_values.push(Felt::from(origin_network)); + storage_values.push(Felt::from(u8::from(is_native))); + storage_values.extend(metadata_hash.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)?; diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index cc40179562..0bbd6b18da 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -28,8 +28,8 @@ pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str /// Error Message: "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" pub const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE note attachment target account does not match consuming account"); -/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 8 note storage items" -pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 8 note storage items"); +/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 18 note storage items" +pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 18 note storage items"); /// Error Message: "faucet is not registered in the bridge's faucet registry" pub const ERR_FAUCET_NOT_REGISTERED: MasmError = MasmError::from_static_str("faucet is not registered in the bridge's faucet registry"); diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 1fba7155d4..5dab888f51 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -227,6 +227,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, @@ -249,6 +250,9 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a agglayer_faucet.id(), &origin_token_address, scale, + origin_network, + false, + &metadata_hash, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -517,6 +521,9 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { agglayer_faucet.id(), &origin_token_address, scale, + origin_network, + false, + &leaf_data.metadata_hash, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index a1f947d0eb..866196badf 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -114,6 +114,9 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { faucet.id(), &origin_token_address, scale, + origin_network, + false, + &metadata_hash, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index 893bad3110..aac36cf181 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -4,6 +4,7 @@ use miden_agglayer::{ AggLayerBridge, ConfigAggBridgeNote, EthAddress, + MetadataHash, create_existing_bridge_account, }; use miden_protocol::Felt; @@ -66,10 +67,15 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { let origin_token_address = EthAddress::from_hex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let scale = 0u8; + let origin_network = 0u32; + let metadata_hash = MetadataHash::from_token_info("USD Coin", "USDC", 6); let config_note = ConfigAggBridgeNote::create( faucet_to_register, &origin_token_address, scale, + origin_network, + false, + &metadata_hash, bridge_admin.id(), bridge_account.id(), builder.rng_mut(), @@ -79,9 +85,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 From 096496de3fa1baacb1d66344286679021b32f5ef Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 15 Apr 2026 21:24:17 -0400 Subject: [PATCH 04/28] feat: replace FPI with bridge-local reads in bridge_out, add lock path for native tokens --- .../asm/agglayer/bridge/bridge_config.masm | 8 +- .../asm/agglayer/bridge/bridge_out.masm | 127 +++++++++++------- .../tests/agglayer/bridge_out.rs | 6 - 3 files changed, 85 insertions(+), 56 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 8fe1d7baec..234db0297d 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -251,7 +251,9 @@ pub proc store_faucet_metadata_hash push.0.2 # => [2, 0, fid_s, fid_p, fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] - movup.7 movup.7 movup.7 movup.7 + # Bring MH_LO(4) to the top. The key prefix `[2, 0]` plus both the duplicated and original + # `[fid_s, fid_p]` pairs sit beneath MH_LO, so MH_LO_3 is at idx 9 (not idx 7). + movup.9 movup.9 movup.9 movup.9 # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, 2, 0, fid_s, fid_p, fid_s, fid_p, MH_HI(4), pad(6)] swapw @@ -351,8 +353,8 @@ proc get_faucet_conversion_info exec.active_account::get_map_item # => [addr4, origin_network, scale, 0, fid_s, fid_p] - # Drop the trailing 0 - drop + # Drop the trailing 0 (sits at idx 3, not at the top of the stack). + movup.3 drop # => [addr4, origin_network, scale, fid_s, fid_p] # Save scale, origin_network, addr4 temporarily by moving them below fid diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 824ad3da19..9733b1100a 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 @@ -12,7 +11,7 @@ use miden::protocol::types::MemoryAddress use miden::protocol::output_note use miden::core::crypto::hashes::poseidon2 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 @@ -65,6 +64,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 @@ -101,10 +101,8 @@ const BURN_NOTE_NUM_STORAGE_ITEMS=0 #! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. #! #! 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 @@ -165,27 +163,34 @@ 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)] + + 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.bridge_config::get_faucet_metadata_hash + # => [METADATA_HASH_LO, METADATA_HASH_HI, pad(16)] - # Extract faucet ID, drop padding and amount - drop drop - # => [faucet_id_suffix, faucet_id_prefix, PROC_MAST_ROOT, pad(16)] - exec.tx::execute_foreign_procedure - # => [METADATA_HASH_LO, METADATA_HASH_HI, pad(8)] 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)] @@ -210,21 +215,29 @@ 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 # ================================================================================================= -#! 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] @@ -238,44 +251,47 @@ 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] + + movup.7 + # => [amount, scale, addr0, addr1, addr2, addr3, addr4, origin_network] - # 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)] - exec.tx::execute_foreign_procedure - # => [AMOUNT_U256_LO, AMOUNT_U256_HI, origin_addr(5), origin_network, pad(2), pad(1)] - # drop the 3 trailing padding elements - repeat.3 - movup.14 drop - end + exec.asset_conversion::scale_native_amount_to_u256 + exec.asset_conversion::reverse_limbs_and_change_byte_endianness + # => [U256_LO(4), U256_HI(4), addr0, addr1, addr2, addr3, addr4, origin_network] + + + + # 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 #! Computes the leaf value from the leaf data in memory and appends it to the Local Exit Tree. @@ -563,3 +579,20 @@ 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-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 866196badf..187188b151 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -163,11 +163,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?; @@ -342,11 +339,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; From 3761f552dc1ad210c5356b363820e8553ca1abb4 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 16 Apr 2026 16:11:03 -0400 Subject: [PATCH 05/28] feat: add unlock path for native tokens in bridge_in, plus lock/unlock tests --- .../asm/agglayer/bridge/bridge_in.masm | 111 ++++++- .../miden-testing/tests/agglayer/bridge_in.rs | 282 +++++++++++++++++- .../tests/agglayer/bridge_out.rs | 135 +++++++++ 3 files changed, 521 insertions(+), 7 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 40ab5dc0c5..4f2c14addf 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -7,6 +7,7 @@ use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::poseidon2 use miden::core::mem use miden::core::word +use miden::protocol::asset use miden::protocol::note use miden::protocol::output_note use miden::protocol::output_note::ATTACHMENT_KIND_NONE @@ -14,6 +15,7 @@ use miden::protocol::active_account use miden::protocol::native_account 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 use miden::protocol::types::DoubleWord @@ -233,12 +235,24 @@ pub proc claim 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)] + # 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)] - exec.build_mint_output_note - # => [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.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.build_mint_output_note + # => [pad(16)] + end end # HELPER PROCEDURES @@ -949,6 +963,93 @@ proc create_mint_note_with_attachment # => [] end +#! Local memory layout inside `unlock_and_send`: +#! - [0..3]: ASSET_KEY +#! - [4..7]: ASSET_VALUE +#! - [8]: destination_id_suffix +#! - [9]: destination_id_prefix +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. +#! +#! Inputs: [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix] +#! Outputs: [] +#! +#! Invocation: exec +@locals(10) +proc unlock_and_send + # Stash destination to locals (claim's CLAIM_DEST_ID_*_LOCAL is in a different frame and is + # not visible here — `exec` invocations get their own local frame). + 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 + #! 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-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 5dab888f51..cf5b6360b6 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -6,8 +6,10 @@ use alloc::string::String; use anyhow::Context; use miden_agglayer::errors::ERR_CLAIM_ALREADY_SPENT; use miden_agglayer::{ + B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, + EthAddress, EthEmbeddedAccountId, ExitRoot, LeafValue, @@ -19,14 +21,21 @@ 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; @@ -585,6 +594,275 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { Ok(()) } +/// Tests the bridge-in unlock path for Miden-native faucets. +/// +/// 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 bridge_in_unlock_native_token() -> anyhow::Result<()> { + let data_source = ClaimDataSource::SimulatedL1ToMiden; + 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 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())?; + + // 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(); + 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; + + // 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( + native_faucet.id(), + &origin_token_address, + scale, + origin_network, + true, // is_native + &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(), + 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" + ); + 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(()) +} + #[tokio::test] async fn solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 187188b151..46ed517eb6 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -567,3 +567,138 @@ 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( + native_faucet.id(), + &origin_token_address, + scale, + origin_network, + true, // is_native + &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(()) +} From ac2f76d1f4c0dc9da269fffd053321bb0bf4d5d3 Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 17 Apr 2026 13:46:22 -0400 Subject: [PATCH 06/28] chore: add changelog entry & bump rand to fix cargo-deny MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps rand 0.9.2 → 0.9.4 and rand 0.10.0 → 0.10.1 to resolve RUSTSEC-2026-0097 flagged by `cargo deny check` in CI. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6fc7e8d8..747864d6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 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)). + ## 0.14.0 (2026-03-23) ### Features From 179c1011b2176392f3f73a823ab8b9f6eda0c70a Mon Sep 17 00:00:00 2001 From: riemann Date: Mon, 20 Apr 2026 12:38:42 -0400 Subject: [PATCH 07/28] refactor: slim faucet & update SPEC section --- bin/bench-transaction/src/context_setups.rs | 8 - crates/miden-agglayer/SPEC.md | 137 +++++++---- .../asm/agglayer/faucet/mod.masm | 165 ------------- .../miden-agglayer/asm/components/faucet.masm | 6 - crates/miden-agglayer/src/faucet.rs | 226 +----------------- crates/miden-agglayer/src/lib.rs | 68 +----- .../miden-testing/tests/agglayer/bridge_in.rs | 8 - .../tests/agglayer/bridge_out.rs | 16 +- .../tests/agglayer/faucet_helpers.rs | 22 +- 9 files changed, 101 insertions(+), 555 deletions(-) diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs index a116cac421..9511fa501c 100644 --- a/bin/bench-transaction/src/context_setups.rs +++ b/bin/bench-transaction/src/context_setups.rs @@ -204,10 +204,6 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result Result { Felt::new(FungibleAsset::MAX_AMOUNT), Felt::new(bridge_amount), bridge_account.id(), - &origin_token_address, - origin_network, - scale, - MetadataHash::from_token_info("AGG", "AGG", 8), ); builder.add_account(faucet.clone())?; diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index cddf6461fd..d5a6eff16e 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -988,17 +988,33 @@ Terminology: deployed wrapped ERC20 contract. A faucet must be registered in the [Bridge Contract](#31-bridge-account-component) before it can participate in bridging. The -bridge maintains two registry maps: +bridge maintains three registry maps, all keyed by faucet account ID and populated atomically +by `bridge_config::register_faucet` during the [`CONFIG_AGG_BRIDGE`](#43-config_agg_bridge) note +consumption: - **Faucet registry** (`agglayer::bridge::faucet_registry_map`): maps faucet account IDs - to a registration flag. Used during bridge-out to verify an asset's faucet is authorized - (see `bridge_config::assert_faucet_registered`). + to a registration value `[1, is_native, 0, 0]`. Used during bridge-out to verify an + asset's faucet is authorized (`bridge_config::assert_faucet_registered`) and, via the + `is_native` flag, to branch between burn/lock on bridge-out and mint/unlock on bridge-in. - **Token registry** (`agglayer::bridge::token_registry_map`): maps Poseidon2 hashes of native token addresses to faucet account IDs. Used during bridge-in to look up the - correct faucet for a given origin token (see - `bridge_config::lookup_faucet_by_token_address`). - -Both registries are populated atomically by `bridge_config::register_faucet` during the [`CONFIG_AGG_BRIDGE`](#43-config_agg_bridge) note consumption. + correct faucet for a given origin token (`bridge_config::lookup_faucet_by_token_address`). +- **Faucet metadata map** (`agglayer::bridge::faucet_metadata_map`): stores all conversion + metadata — origin address, origin network, scale, and the precomputed + `keccak256(abi.encode(name, symbol, decimals))` metadata hash — for every registered + faucet. A single map with four sub-keys per faucet ID is enough to hold the full set: + + | Sub-key | Value | + | ------------------------------------------ | ---------------------------------------------- | + | `[0, 0, faucet_id_suffix, faucet_id_prefix]` | `[addr0, addr1, addr2, addr3]` | + | `[1, 0, faucet_id_suffix, faucet_id_prefix]` | `[addr4, origin_network, scale, 0]` | + | `[2, 0, faucet_id_suffix, faucet_id_prefix]` | `[mh_lo0, mh_lo1, mh_lo2, mh_lo3]` | + | `[3, 0, faucet_id_suffix, faucet_id_prefix]` | `[mh_hi0, mh_hi1, mh_hi2, mh_hi3]` | + + The metadata map lets `bridge_out` and `bridge_in` read conversion data from bridge-local + storage rather than issuing foreign-procedure-invocation (FPI) calls into the faucet; this + is required for native-token support, where the faucet is not under the bridge's control + and does not necessarily expose any AggLayer-specific procedures. ### 7.1 Bridging-in: Registering non-native faucets on Miden @@ -1006,42 +1022,44 @@ When a new ERC20 token is bridged to Miden for the first time, a corresponding A faucet account must be created and registered. The faucet serves as the mint/burn authority for the wrapped token on Miden. -The `AggLayerFaucet` struct (Rust, `src/faucet.rs`) captures the faucet-specific -configuration: - -- Token metadata: symbol, decimals, max_supply, token_supply (TODO Missing information about the token name ([#2585](https://github.com/0xMiden/protocol/issues/2585))) -- Origin token address: the ERC20 contract address on the origin chain -- Origin network: the chain ID of the origin chain -- Scale factor: the exponent used to convert between EVM U256 amounts and Field elements on Miden -- Metadata hash: `keccak256(abi.encode(name, symbol, decimals))`. This is precomputed by the bridge admin at faucet creation time and is currently not verified onchain (TODO Verify metadata hash onchain ([#2586](https://github.com/0xMiden/protocol/issues/2586))) +The `AggLayerFaucet` Rust struct (`src/faucet.rs`) holds only +token metadata — symbol, decimals, max supply, and token supply +(TODO Missing token name ([#2585](https://github.com/0xMiden/protocol/issues/2585))). +Conversion metadata (origin address, origin network, scale, and metadata hash) is +*not* stored on the faucet; it is carried by the `CONFIG_AGG_BRIDGE` note at registration +time and written directly into the bridge's `faucet_metadata_map`. The metadata hash is +precomputed by the bridge admin and is currently not verified onchain +(TODO Verify metadata hash onchain ([#2586](https://github.com/0xMiden/protocol/issues/2586))). Registration is performed via [`CONFIG_AGG_BRIDGE`](#43-config_agg_bridge) notes. The bridge -operator creates a `CONFIG_AGG_BRIDGE` note containing the faucet's account ID and the -origin token address, then sends it to the bridge account. On consumption, the note -script calls `bridge_config::register_faucet`, which performs a two-step registration: - -1. Writes a registration flag under the faucet ID key in the `faucet_registry_map`: - `[0, 0, faucet_id_suffix, faucet_id_prefix]` -> `[1, 0, 0, 0]`. -2. Hashes the origin token address using Poseidon2 and writes - the mapping into the `token_registry_map`: - `hash(origin_token_addr)` -> `[0, 0, faucet_id_suffix, faucet_id_prefix]`. - -The token registry enables the bridge to resolve which Miden-side faucet corresponds to a given -origin token address during CLAIM note processing. When the bridge -processes a [`CLAIM`](#42-claim) note, it reads the origin token address from the leaf data and calls -`bridge_config::lookup_faucet_by_token_address` to find the registered faucet. This -lookup hashes the address with Poseidon2 and retrieves the faucet ID from the token -registry map. If the token address 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. +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)` → `[0, 0, fid_s, fid_p]`. + +The token registry enables the bridge to resolve which Miden-side faucet corresponds to a +given origin token address during CLAIM note processing (`bridge_config::lookup_faucet_by_token_address`). +If the token address 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)). + +### 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. @@ -1051,22 +1069,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::build_mint_output_note` — emits a MINT note consumed by the faucet. | `bridge_in::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/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index 408c0e93e1..4c3e59a79c 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -1,171 +1,6 @@ -use miden::core::sys -use agglayer::common::utils -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) from faucet conversion storage. -#! -#! Reads conversion_info_1 (first 4 felts of address) and conversion_info_2 (5th felt) from storage. -#! -#! Inputs: [] -#! Outputs: [addr0, addr1, addr2, addr3, addr4] -#! -#! Invocation: exec -pub proc get_origin_token_address - push.CONVERSION_INFO_1_SLOT[0..2] - exec.active_account::get_item - # => [addr0, addr1, addr2, addr3] - - # Read slot 2: [addr4, origin_network, scale, 0] - push.CONVERSION_INFO_2_SLOT[0..2] - exec.active_account::get_item - # => [addr4, origin_network, scale, 0, addr0, addr1, addr2, addr3] - - # Keep only addr4, drop origin_network, scale, 0 - movdn.7 drop drop drop - # => [addr0, addr1, addr2, addr3, addr4] -end - -#! Returns the origin network identifier from faucet conversion storage. -#! -#! Inputs: [] -#! Outputs: [origin_network] -#! -#! Invocation: exec -pub proc get_origin_network - push.CONVERSION_INFO_2_SLOT[0..2] - exec.active_account::get_item - # => [addr4, origin_network, scale, 0] - - drop movdn.2 drop drop - # => [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 - exec.get_origin_token_address - # => [addr0, addr1, addr2, addr3, addr4, U256_LO, U256_HI, pad(15)] - - # Move address below the U256 amount - repeat.5 movdn.12 end - # => [U256_LO, U256_HI, addr0, addr1, addr2, addr3, addr4, pad(15)] - - # Step 4: Get origin network - exec.get_origin_network - exec.utils::swap_u32_bytes - # => [origin_network, U256_LO, U256_HI, addr0..addr4, pad(15)] - - # Move origin_network after the address fields - movdn.13 - # => [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/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/src/faucet.rs b/crates/miden-agglayer/src/faucet.rs index 306a8acc99..c4c37a7629 100644 --- a/crates/miden-agglayer/src/faucet.rs +++ b/crates/miden-agglayer/src/faucet.rs @@ -18,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; @@ -50,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 /// @@ -95,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 { @@ -112,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). @@ -150,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 { @@ -208,90 +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"); - - Ok(conversion_info_2[1] - .as_canonical_u64() - .try_into() - .expect("origin network ID should fit into u32")) - } - - /// 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,51 +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 + 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 slot2 = - Word::new([addr_elements[4], Felt::from(origin_network), 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..6df9319f9d 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -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 cf5b6360b6..7d9bdc062a 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -180,10 +180,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())?; @@ -482,10 +478,6 @@ async fn test_duplicate_claim_note_rejected() -> 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())?; diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 46ed517eb6..66f9c92d7d 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -84,7 +84,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"); @@ -102,10 +102,6 @@ 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())?; @@ -296,12 +292,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, @@ -309,10 +299,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())?; diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index 84ea5b226c..51155e0bc5 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -1,12 +1,6 @@ extern crate alloc; -use miden_agglayer::{ - AggLayerFaucet, - EthAddress, - MetadataHash, - create_existing_agglayer_faucet, - create_existing_bridge_account, -}; +use miden_agglayer::{AggLayerFaucet, create_existing_agglayer_faucet, create_existing_bridge_account}; use miden_protocol::Felt; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::FungibleAsset; @@ -36,13 +30,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 +37,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(()) } From ff53888518434c79ec1022fd2be1943bbfd2fe37 Mon Sep 17 00:00:00 2001 From: riemann Date: Mon, 20 Apr 2026 12:43:03 -0400 Subject: [PATCH 08/28] style: rustfmt import grouping --- crates/miden-testing/tests/agglayer/faucet_helpers.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/miden-testing/tests/agglayer/faucet_helpers.rs b/crates/miden-testing/tests/agglayer/faucet_helpers.rs index 51155e0bc5..6bdb62c53e 100644 --- a/crates/miden-testing/tests/agglayer/faucet_helpers.rs +++ b/crates/miden-testing/tests/agglayer/faucet_helpers.rs @@ -1,6 +1,10 @@ extern crate alloc; -use miden_agglayer::{AggLayerFaucet, create_existing_agglayer_faucet, create_existing_bridge_account}; +use miden_agglayer::{ + AggLayerFaucet, + create_existing_agglayer_faucet, + create_existing_bridge_account, +}; use miden_protocol::Felt; use miden_protocol::account::auth::AuthScheme; use miden_protocol::asset::FungibleAsset; From 67a28aa419460b033bc85f7fb0ad3b9d32f87b60 Mon Sep 17 00:00:00 2001 From: riemann Date: Mon, 20 Apr 2026 17:39:18 -0400 Subject: [PATCH 09/28] refactor: tighten MASM stack ops, normalize stack-comment style --- .../asm/agglayer/bridge/bridge_config.masm | 63 +++++++++---------- .../asm/agglayer/bridge/bridge_in.masm | 4 +- .../asm/agglayer/bridge/bridge_out.masm | 19 +----- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 8 +-- 4 files changed, 39 insertions(+), 55 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 234db0297d..eb638fa964 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -146,7 +146,7 @@ pub proc register_faucet # => [addr0, addr1, addr2, addr3, addr4, addr0, addr1, addr2, addr3, addr4, pad(6)] exec.hash_token_address - # => [TOKEN_ADDR_HASH(4), addr0, addr1, addr2, addr3, addr4, pad(6)] + # => [TOKEN_ADDR_HASH, addr0, addr1, addr2, addr3, addr4, pad(6)] loc_storew_le.REG_TOKEN_HASH_LOC dropw # => [addr0, addr1, addr2, addr3, addr4, pad(11)] @@ -174,11 +174,10 @@ pub proc register_faucet # --- Step 2: Store origin address part 2 + origin_network + scale --- # KEY = [1, 0, fid_s, fid_p], VALUE = [addr4, origin_network, scale, 0] - loc_load.REG_ORIGIN_NETWORK_LOC - swap + push.0 loc_load.REG_SCALE_LOC - movdn.2 - push.0 movdn.3 + loc_load.REG_ORIGIN_NETWORK_LOC + movup.3 # => [addr4, origin_network, scale, 0, pad(11)] loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC @@ -218,7 +217,7 @@ pub proc register_faucet # => [0, 0, fid_s, fid_p, pad(11)] padw loc_loadw_le.REG_TOKEN_HASH_LOC - # => [TOKEN_ADDR_HASH(4), 0, 0, fid_s, fid_p, pad(7)] + # => [TOKEN_ADDR_HASH, 0, 0, fid_s, fid_p, pad(7)] push.TOKEN_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -233,7 +232,7 @@ end #! - KEY [2, 0, fid_s, fid_p] -> METADATA_HASH_LO #! - KEY [3, 0, fid_s, fid_p] -> METADATA_HASH_HI #! -#! Inputs: [fid_s, fid_p, METADATA_HASH_LO(4), METADATA_HASH_HI(4), pad(6)] +#! Inputs: [fid_s, fid_p, METADATA_HASH_LO, METADATA_HASH_HI, pad(6)] #! Outputs: [pad(16)] #! #! Panics if: @@ -246,33 +245,33 @@ pub proc store_faucet_metadata_hash # --- Store METADATA_HASH_LO at key [2, 0, fid_s, fid_p] --- dup.1 dup.1 - # => [fid_s, fid_p, fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + # => [fid_s, fid_p, fid_s, fid_p, MH_LO, MH_HI, pad(6)] push.0.2 - # => [2, 0, fid_s, fid_p, fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + # => [2, 0, fid_s, fid_p, fid_s, fid_p, MH_LO, MH_HI, pad(6)] - # Bring MH_LO(4) to the top. The key prefix `[2, 0]` plus both the duplicated and original + # Bring MH_LO to the top. The key prefix `[2, 0]` plus both the duplicated and original # `[fid_s, fid_p]` pairs sit beneath MH_LO, so MH_LO_3 is at idx 9 (not idx 7). movup.9 movup.9 movup.9 movup.9 - # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, 2, 0, fid_s, fid_p, fid_s, fid_p, MH_HI(4), pad(6)] + # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, 2, 0, fid_s, fid_p, fid_s, fid_p, MH_HI, pad(6)] swapw - # => [2, 0, fid_s, fid_p, MH_LO(4), fid_s, fid_p, MH_HI(4), pad(6)] + # => [2, 0, fid_s, fid_p, MH_LO, fid_s, fid_p, MH_HI, pad(6)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item dropw - # => [fid_s, fid_p, MH_HI(4), pad(6)] + # => [fid_s, fid_p, MH_HI, pad(6)] # --- Store METADATA_HASH_HI at key [3, 0, fid_s, fid_p] --- push.0.3 - # => [3, 0, fid_s, fid_p, MH_HI(4), pad(6)] + # => [3, 0, fid_s, fid_p, MH_HI, pad(6)] movup.7 movup.7 movup.7 movup.7 # => [mh_hi0, mh_hi1, mh_hi2, mh_hi3, 3, 0, fid_s, fid_p, pad(6)] swapw - # => [3, 0, fid_s, fid_p, MH_HI(4), pad(6)] + # => [3, 0, fid_s, fid_p, MH_HI, pad(6)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -374,8 +373,10 @@ end #! metadata map. #! #! Reads sub-keys 2 and 3 from faucet_metadata_map: -#! - Key [2, 0, fid_s, fid_p] -> METADATA_HASH_LO (4 felts) -#! - Key [3, 0, fid_s, fid_p] -> METADATA_HASH_HI (4 felts) +#! - Key [2, 0, fid_s, fid_p] -> METADATA_HASH_LO +#! - Key [3, 0, fid_s, fid_p] -> METADATA_HASH_HI +#! +#! Sub-key 3 (HI) is read first so the final stack has MH_LO on top without a swapw. #! #! Inputs: [faucet_id_suffix, faucet_id_prefix] #! Outputs: [METADATA_HASH_LO, METADATA_HASH_HI] @@ -386,29 +387,25 @@ proc get_faucet_metadata_hash dup.1 dup.1 # => [fid_s, fid_p, fid_s, fid_p] - # Read sub-key 2: METADATA_HASH_LO - push.0.2 - # => [2, 0, fid_s, fid_p, fid_s, fid_p] + # Read sub-key 3 (MH_HI) first so MH_LO ends up on top after the second read. + push.0.3 + # => [3, 0, fid_s, fid_p, fid_s, fid_p] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item - # => [MH_LO(4), fid_s, fid_p] + # => [MH_HI, fid_s, fid_p] - # Move fid below MH_LO + # Move fid below MH_HI movup.5 movup.5 - # => [fid_s, fid_p, MH_LO(4)] + # => [fid_s, fid_p, MH_HI] - # Read sub-key 3: METADATA_HASH_HI - push.0.3 - # => [3, 0, fid_s, fid_p, MH_LO(4)] + # Read sub-key 2: METADATA_HASH_LO + push.0.2 + # => [2, 0, fid_s, fid_p, MH_HI] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item - # => [MH_HI(4), MH_LO(4)] - - # Rearrange: MH_LO should be on top - swapw - # => [MH_LO(4), MH_HI(4)] + # => [MH_LO, MH_HI] end #! Returns whether a faucet is native (not owned by the bridge). @@ -428,8 +425,8 @@ proc is_faucet_native exec.active_account::get_map_item # => [1, is_native, 0, 0] - # Drop element 0 (registration flag), keep is_native, drop rest - drop swap drop swap drop + # Drop element 0 (registration flag), sink is_native below the trailing zeros, drop them. + drop movdn.2 drop drop # => [is_native] end diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 4f2c14addf..361d216b49 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -957,7 +957,7 @@ proc create_mint_note_with_attachment # Rearrange for set_attachment: [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] movup.6 - # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT(4)] + # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT] exec.output_note::set_attachment # => [] @@ -1008,7 +1008,7 @@ proc unlock_and_send # 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 + 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 diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 9733b1100a..203cad797d 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -173,21 +173,15 @@ pub proc bridge_out 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.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(16)] @@ -254,8 +248,6 @@ end #! #! Invocation: exec proc convert_asset - - swapw exec.asset::fungible_value_into_amount movdn.4 # => [ASSET_KEY, amount] @@ -273,25 +265,19 @@ proc convert_asset # Bring [amount, scale] to the top for scale_native_amount_to_u256. movup.6 # => [scale, addr0, addr1, addr2, addr3, addr4, origin_network, amount] - + movup.7 # => [amount, scale, addr0, addr1, addr2, addr3, addr4, origin_network] - - exec.asset_conversion::scale_native_amount_to_u256 exec.asset_conversion::reverse_limbs_and_change_byte_endianness - # => [U256_LO(4), U256_HI(4), addr0, addr1, addr2, addr3, addr4, origin_network] - - + # => [U256_LO, U256_HI, addr0, addr1, addr2, addr3, addr4, origin_network] # 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 #! Computes the leaf value from the leaf data in memory and appends it to the Local Exit Tree. @@ -593,6 +579,7 @@ end proc lock_asset exec.native_account::add_asset # => [ASSET_VALUE'] + dropw # => [] end 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 1a3d7bd088..7aaa469e27 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -138,7 +138,7 @@ begin # => [pad(16)] # --- Call 2: store_faucet_metadata_hash --- - # Expects: [fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + # Expects: [fid_s, fid_p, MH_LO, MH_HI, pad(6)] # Drop all 16 pad elements and rebuild the stack dropw dropw dropw dropw @@ -157,15 +157,15 @@ begin # Load fid_p, fid_s mem_load.FAUCET_ID_PREFIX mem_load.FAUCET_ID_SUFFIX - # => [fid_s, fid_p, MH_LO(4), MH_HI(4)] + # => [fid_s, fid_p, MH_LO, MH_HI] # Pad to 16 (need 6 more) padw push.0.0 - # => [0, 0, 0, 0, 0, 0, fid_s, fid_p, MH_LO(4), MH_HI(4)] + # => [0, 0, 0, 0, 0, 0, fid_s, fid_p, MH_LO, MH_HI] # Move pad to end movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [fid_s, fid_p, MH_LO(4), MH_HI(4), pad(6)] + # => [fid_s, fid_p, MH_LO, MH_HI, pad(6)] call.bridge_config::store_faucet_metadata_hash # => [pad(16)] From 5531a6d3c3f94697a2824ae0af8fdce2cb8db53f Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 22 Apr 2026 12:05:56 -0400 Subject: [PATCH 10/28] refactor: cleanup masm & stack comments --- .../asm/agglayer/bridge/bridge_config.masm | 161 ++++++++---------- .../asm/note_scripts/CLAIM.masm | 2 - .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 34 ++-- 3 files changed, 84 insertions(+), 113 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index eb638fa964..4afd3ac6fb 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -31,10 +31,10 @@ 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 offsets for `register_faucet` +# Offsets in the local memory of the `register_faucet` procedure const REG_TOKEN_HASH_LOC = 0 -const REG_FID_S_LOC = 4 -const REG_FID_P_LOC = 5 +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 @@ -114,14 +114,14 @@ end #! 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, fid_s, fid_p] -> [addr0, addr1, addr2, addr3] (origin address part 1) -#! 2. KEY [1, 0, fid_s, fid_p] -> [addr4, origin_network, scale, 0] (origin address part 2) +#! 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) #! #! Also registers: -#! 3. faucet_registry_map: [0, 0, fid_s, fid_p] -> [1, is_native, 0, 0] -#! 4. token_registry_map: hash(tokenAddress) -> [fid_s, fid_p, 0, 0] +#! 3. faucet_registry_map: [0, 0, faucet_id_suffix, faucet_id_prefix] -> [1, is_native, 0, 0] +#! 4. token_registry_map: hash(tokenAddress) -> [0, 0, faucet_id_suffix, faucet_id_prefix] #! -#! Inputs: [origin_token_addr(5), fid_s, fid_p, scale, origin_network, is_native, pad(6)] +#! Inputs: [origin_token_addr(5), faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] #! Outputs: [pad(16)] #! #! Panics if: @@ -131,14 +131,14 @@ end @locals(14) pub proc register_faucet exec.assert_sender_is_bridge_admin - # => [addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native, pad(6)] + # => [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.9 loc_store.REG_IS_NATIVE_LOC movup.8 loc_store.REG_ORIGIN_NETWORK_LOC movup.7 loc_store.REG_SCALE_LOC - movup.6 loc_store.REG_FID_P_LOC - movup.5 loc_store.REG_FID_S_LOC + movup.6 loc_store.REG_FAUCET_ID_PREFIX_LOC + movup.5 loc_store.REG_FAUCET_ID_SUFFIX_LOC # => [addr0, addr1, addr2, addr3, addr4, pad(11)] # Duplicate address for hashing before it gets consumed @@ -152,27 +152,18 @@ pub proc register_faucet # => [addr0, addr1, addr2, addr3, addr4, pad(11)] # --- Step 1: Store origin address part 1 in faucet_metadata_map --- - # KEY = [0, 0, fid_s, fid_p], VALUE = [addr0, addr1, addr2, addr3] + # KEY = [0, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [addr0, addr1, addr2, addr3] - loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC - # => [fid_s, fid_p, addr0, addr1, addr2, addr3, addr4, pad(11)] - - push.0.0 - # => [0, 0, fid_s, fid_p, addr0, addr1, addr2, addr3, addr4, pad(11)] - - movup.7 movup.7 movup.7 movup.7 - # => [addr0, addr1, addr2, addr3, 0, 0, fid_s, fid_p, addr4, pad(11)] - - swapw - # => [0, 0, fid_s, fid_p, addr0, addr1, addr2, addr3, addr4, pad(11)] + 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, addr0, addr1, addr2, addr3, addr4, pad(11)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item dropw - # => [addr4, pad(11)] + # => [addr4, pad(15)] # --- Step 2: Store origin address part 2 + origin_network + scale --- - # KEY = [1, 0, fid_s, fid_p], VALUE = [addr4, origin_network, scale, 0] + # KEY = [1, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [addr4, origin_network, scale, 0] push.0 loc_load.REG_SCALE_LOC @@ -180,11 +171,11 @@ pub proc register_faucet movup.3 # => [addr4, origin_network, scale, 0, pad(11)] - loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC - # => [fid_s, fid_p, addr4, origin_network, scale, 0, pad(11)] + 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(11)] push.0.1 - # => [1, 0, fid_s, fid_p, addr4, origin_network, scale, 0, pad(11)] + # => [1, 0, faucet_id_suffix, faucet_id_prefix, addr4, origin_network, scale, 0, pad(11)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -192,32 +183,30 @@ pub proc register_faucet # => [pad(11)] # --- 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. - loc_load.REG_IS_NATIVE_LOC - loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC - # => [fid_s, fid_p, is_native, pad(11)] - - push.0.0 movup.4 push.IS_FAUCET_REGISTERED_FLAG - # => [1, is_native, 0, 0, fid_s, fid_p, pad(11)] + loc_load.REG_IS_NATIVE_LOC push.IS_FAUCET_REGISTERED_FLAG + # => [1, is_native, pad(14)] - movup.5 movup.5 push.0.0 - # => [0, 0, fid_s, fid_p, 1, is_native, 0, 0, pad(11)] + 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(14)] push.FAUCET_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item dropw - # => [pad(11)] + # => [pad(16)] - # --- Step 4: Store TOKEN_ADDR_HASH -> [fid_s, fid_p, 0, 0] in token_registry --- + # --- Step 4: Store TOKEN_ADDR_HASH -> [0, 0, faucet_id_suffix, faucet_id_prefix] in token_registry --- - loc_load.REG_FID_P_LOC loc_load.REG_FID_S_LOC - # => [fid_s, fid_p, pad(11)] + loc_load.REG_FAUCET_ID_PREFIX_LOC loc_load.REG_FAUCET_ID_SUFFIX_LOC + # => [faucet_id_suffix, faucet_id_prefix, pad(11)] push.0.0 - # => [0, 0, fid_s, fid_p, pad(11)] + # => [0, 0, faucet_id_suffix, faucet_id_prefix, pad(11)] padw loc_loadw_le.REG_TOKEN_HASH_LOC - # => [TOKEN_ADDR_HASH, 0, 0, fid_s, fid_p, pad(7)] + # => [TOKEN_ADDR_HASH, 0, 0, faucet_id_suffix, faucet_id_prefix, pad(7)] push.TOKEN_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -229,10 +218,10 @@ end #! #! 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, fid_s, fid_p] -> METADATA_HASH_LO -#! - KEY [3, 0, fid_s, fid_p] -> METADATA_HASH_HI +#! - 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: [fid_s, fid_p, METADATA_HASH_LO, METADATA_HASH_HI, pad(6)] +#! Inputs: [faucet_id_suffix, faucet_id_prefix, METADATA_HASH_LO, METADATA_HASH_HI, pad(6)] #! Outputs: [pad(16)] #! #! Panics if: @@ -241,37 +230,23 @@ end #! Invocation: call pub proc store_faucet_metadata_hash exec.assert_sender_is_bridge_admin - # => [fid_s, fid_p, mh_lo0, mh_lo1, mh_lo2, mh_lo3, mh_hi0, mh_hi1, mh_hi2, mh_hi3, pad(6)] + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] - # --- Store METADATA_HASH_LO at key [2, 0, fid_s, fid_p] --- - dup.1 dup.1 - # => [fid_s, fid_p, fid_s, fid_p, MH_LO, MH_HI, pad(6)] + # --- Store METADATA_HASH_LO at key [2, 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.2 - # => [2, 0, fid_s, fid_p, fid_s, fid_p, MH_LO, MH_HI, pad(6)] - - # Bring MH_LO to the top. The key prefix `[2, 0]` plus both the duplicated and original - # `[fid_s, fid_p]` pairs sit beneath MH_LO, so MH_LO_3 is at idx 9 (not idx 7). - movup.9 movup.9 movup.9 movup.9 - # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, 2, 0, fid_s, fid_p, fid_s, fid_p, MH_HI, pad(6)] - - swapw - # => [2, 0, fid_s, fid_p, MH_LO, fid_s, fid_p, MH_HI, pad(6)] + # => [2, 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 - # => [fid_s, fid_p, MH_HI, pad(6)] + # => [faucet_id_suffix, faucet_id_prefix, MH_HI, pad(6)] - # --- Store METADATA_HASH_HI at key [3, 0, fid_s, fid_p] --- + # --- Store METADATA_HASH_HI at key [3, 0, faucet_id_suffix, faucet_id_prefix] --- push.0.3 - # => [3, 0, fid_s, fid_p, MH_HI, pad(6)] - - movup.7 movup.7 movup.7 movup.7 - # => [mh_hi0, mh_hi1, mh_hi2, mh_hi3, 3, 0, fid_s, fid_p, pad(6)] - - swapw - # => [3, 0, fid_s, fid_p, MH_HI, pad(6)] + # => [3, 0, faucet_id_suffix, faucet_id_prefix, MH_HI, pad(6)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -282,7 +257,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 [1, is_native, 0, 0] where element 0 is the registration flag. +#! The stored value is [is_registered, is_native, 0, 0]. #! #! Inputs: [faucet_id_suffix, faucet_id_prefix] #! Outputs: [] @@ -332,40 +307,38 @@ end #! faucet from the bridge's faucet metadata map. #! #! Reads sub-keys 0 and 1 from faucet_metadata_map: -#! - Key [0, 0, fid_s, fid_p] -> [addr0, addr1, addr2, addr3] -#! - Key [1, 0, fid_s, fid_p] -> [addr4, origin_network, scale, 0] +#! - 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 - # Save faucet_id for second read - dup.1 dup.1 - # => [fid_s, fid_p, fid_s, fid_p] + # Prepare the sub-key 0 KEY underneath. + push.0.0 + # => [0, 0, faucet_id_suffix, faucet_id_prefix] - # Read sub-key 1: [addr4, origin_network, scale, 0] - push.0.1 - # => [1, 0, fid_s, fid_p, fid_s, fid_p] + # Prepare the sub-key 1 KEY on top. + dup.3 dup.3 push.0.1 + # => [1, 0, faucet_id_suffix, faucet_id_prefix, 0, 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, fid_s, fid_p] - - # Drop the trailing 0 (sits at idx 3, not at the top of the stack). - movup.3 drop - # => [addr4, origin_network, scale, fid_s, fid_p] + # => [addr4, origin_network, scale, 0, 0, 0, faucet_id_suffix, faucet_id_prefix] - # Save scale, origin_network, addr4 temporarily by moving them below fid - movdn.4 movdn.4 movdn.4 - # => [fid_s, fid_p, addr4, origin_network, scale] + # 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.0.0 - # => [0, 0, fid_s, fid_p, addr4, origin_network, scale] - 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 @@ -373,8 +346,8 @@ end #! metadata map. #! #! Reads sub-keys 2 and 3 from faucet_metadata_map: -#! - Key [2, 0, fid_s, fid_p] -> METADATA_HASH_LO -#! - Key [3, 0, fid_s, fid_p] -> METADATA_HASH_HI +#! - Key [2, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_LO +#! - Key [3, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_HI #! #! Sub-key 3 (HI) is read first so the final stack has MH_LO on top without a swapw. #! @@ -385,23 +358,23 @@ end proc get_faucet_metadata_hash # Save faucet_id for second read dup.1 dup.1 - # => [fid_s, fid_p, fid_s, fid_p] + # => [faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] # Read sub-key 3 (MH_HI) first so MH_LO ends up on top after the second read. push.0.3 - # => [3, 0, fid_s, fid_p, fid_s, fid_p] + # => [3, 0, faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item - # => [MH_HI, fid_s, fid_p] + # => [MH_HI, faucet_id_suffix, faucet_id_prefix] # Move fid below MH_HI movup.5 movup.5 - # => [fid_s, fid_p, MH_HI] + # => [faucet_id_suffix, faucet_id_prefix, MH_HI] # Read sub-key 2: METADATA_HASH_LO push.0.2 - # => [2, 0, fid_s, fid_p, MH_HI] + # => [2, 0, faucet_id_suffix, faucet_id_prefix, MH_HI] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index ff698e34c4..715ab6d7f9 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -97,8 +97,6 @@ begin movdn.8 # => [PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(16)] - debug.stack - # call the Bridge Claim procedure call.bridge::claim # => [pad(16), pad(9)] 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 7aaa469e27..5abeaa52a9 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -23,10 +23,10 @@ const STORAGE_START_PTR = 0 # [10..13] METADATA_HASH_LO_0..3 (4 felts) # [14..17] METADATA_HASH_HI_0..3 (4 felts) -const ORIGIN_TOKEN_ADDR_0 = STORAGE_START_PTR -const ORIGIN_TOKEN_ADDR_1 = STORAGE_START_PTR + 1 -const ORIGIN_TOKEN_ADDR_2 = STORAGE_START_PTR + 2 -const ORIGIN_TOKEN_ADDR_3 = STORAGE_START_PTR + 3 +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 @@ -77,7 +77,7 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! - faucet_id_prefix [6] : 1 felt #! - scale [7] : 1 felt #! - origin_network [8] : 1 felt -#! - is_native [9] : 1 felt (0 or 1) +#! - is_native [9] : 1 felt #! - metadata_hash_lo [10] : 4 felts #! - metadata_hash_hi [14] : 4 felts #! @@ -107,7 +107,7 @@ begin # => [] # --- Call 1: register_faucet --- - # Expects: [addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native, pad(6)] + # Expects: [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] # Build the stack bottom-up: load deepest elements first, then push shallower ones on top. # This avoids complex rearrangement since each mem_load pushes onto the top. @@ -117,28 +117,28 @@ begin mem_load.FAUCET_ID_PREFIX mem_load.FAUCET_ID_SUFFIX mem_load.ORIGIN_TOKEN_ADDR_4 - # => [addr4, fid_s, fid_p, scale, origin_network, is_native] + # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native] # Load addr0..addr3 on top in reverse order so addr0 ends up on top 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, fid_s, fid_p, scale, origin_network, is_native] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native] # Pad to 16 (need 6 more zeros) padw push.0.0 - # => [0, 0, 0, 0, 0, 0, addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native] + # => [0, 0, 0, 0, 0, 0, addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native] # Move 6 pad zeros to the end movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [addr0, addr1, addr2, addr3, addr4, fid_s, fid_p, scale, origin_network, is_native, pad(6)] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] call.bridge_config::register_faucet # => [pad(16)] # --- Call 2: store_faucet_metadata_hash --- - # Expects: [fid_s, fid_p, MH_LO, MH_HI, pad(6)] + # Expects: [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] # Drop all 16 pad elements and rebuild the stack dropw dropw dropw dropw @@ -147,25 +147,25 @@ begin # Load MH_HI from memory addresses 14..17 (individual loads — not word-aligned) mem_load.METADATA_HASH_HI_3 mem_load.METADATA_HASH_HI_2 mem_load.METADATA_HASH_HI_1 mem_load.METADATA_HASH_HI_0 - # => [mh_hi0, mh_hi1, mh_hi2, mh_hi3] + # => [MH_HI] # Load MH_LO from memory addresses 10..13 (individual loads — not word-aligned) mem_load.METADATA_HASH_LO_3 mem_load.METADATA_HASH_LO_2 mem_load.METADATA_HASH_LO_1 mem_load.METADATA_HASH_LO_0 - # => [mh_lo0, mh_lo1, mh_lo2, mh_lo3, mh_hi0, mh_hi1, mh_hi2, mh_hi3] + # => [MH_LO, MH_HI] - # Load fid_p, fid_s + # Load faucet_id_prefix, faucet_id_suffix mem_load.FAUCET_ID_PREFIX mem_load.FAUCET_ID_SUFFIX - # => [fid_s, fid_p, MH_LO, MH_HI] + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI] # Pad to 16 (need 6 more) padw push.0.0 - # => [0, 0, 0, 0, 0, 0, fid_s, fid_p, MH_LO, MH_HI] + # => [0, 0, 0, 0, 0, 0, faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI] # Move pad to end movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [fid_s, fid_p, MH_LO, MH_HI, pad(6)] + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] call.bridge_config::store_faucet_metadata_hash # => [pad(16)] From fe7c82202e5722a810d69409b2f02e065ea1736e Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 22 Apr 2026 12:58:05 -0400 Subject: [PATCH 11/28] refactor: pack ConfigAggBridgeNote::create args into ConversionMetadata Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/bench-transaction/src/context_setups.rs | 29 ++-- crates/miden-agglayer/src/config_note.rs | 128 +++++++++++++----- crates/miden-agglayer/src/lib.rs | 2 +- .../miden-testing/tests/agglayer/bridge_in.rs | 43 +++--- .../tests/agglayer/bridge_out.rs | 29 ++-- .../tests/agglayer/config_bridge.rs | 15 +- 6 files changed, 161 insertions(+), 85 deletions(-) diff --git a/bin/bench-transaction/src/context_setups.rs b/bin/bench-transaction/src/context_setups.rs index 9511fa501c..22dcba87f9 100644 --- a/bin/bench-transaction/src/context_setups.rs +++ b/bin/bench-transaction/src/context_setups.rs @@ -4,6 +4,7 @@ use miden_agglayer::{ B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, MetadataHash, UpdateGerNote, @@ -241,12 +242,14 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result Result { // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) let metadata_hash = MetadataHash::from_token_info("AGG", "AGG", 8); let config_note = ConfigAggBridgeNote::create( - faucet.id(), - &origin_token_address, - scale, - origin_network, - false, - &metadata_hash, + 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(), diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index 6bb8f08490..035eaf6ba9 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -42,6 +42,48 @@ 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`. + 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 // ================================================================================================ @@ -87,49 +129,21 @@ impl ConfigAggBridgeNote { /// Creates a CONFIG_AGG_BRIDGE note to register a faucet in the bridge's registry. /// - /// The note storage contains 18 felts carrying all the data needed for faucet registration: - /// - Origin token address (5 felts) - /// - Faucet account ID (2 felts) - /// - Scale factor (1 felt) - /// - Origin network (1 felt) - /// - Is-native flag (1 felt) - /// - Metadata hash (8 felts) - /// /// # Parameters - /// - `faucet_account_id`: The account ID of the faucet to register - /// - `origin_token_address`: The origin EVM token address for the token registry - /// - `scale`: The decimal scaling factor (e.g. 0 for USDC, 8 for ETH) - /// - `origin_network`: The origin network/chain ID - /// - `is_native`: Whether this is a Miden-native faucet (lock/unlock) vs bridge-owned - /// (burn/mint) - /// - `metadata_hash`: The keccak256 hash of ABI-encoded token metadata - /// - `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. - #[allow(clippy::too_many_arguments)] pub fn create( - faucet_account_id: AccountId, - origin_token_address: &EthAddress, - scale: u8, - origin_network: u32, - is_native: bool, - metadata_hash: &MetadataHash, + metadata: ConversionMetadata, sender_account_id: AccountId, target_account_id: AccountId, rng: &mut R, ) -> Result { - // Create note storage with 18 felts - let addr_elements = origin_token_address.to_elements(); - let mut storage_values: Vec = addr_elements; - storage_values.push(faucet_account_id.suffix()); - storage_values.push(faucet_account_id.prefix().as_felt()); - storage_values.push(Felt::from(scale)); - storage_values.push(Felt::from(origin_network)); - storage_values.push(Felt::from(u8::from(is_native))); - storage_values.extend(metadata_hash.to_elements()); + let storage_values = metadata.to_elements(); debug_assert_eq!( storage_values.len(), @@ -149,12 +163,54 @@ 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)); + 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/lib.rs b/crates/miden-agglayer/src/lib.rs index 6df9319f9d..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::{ diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 7d9bdc062a..b760a300e1 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -9,6 +9,7 @@ use miden_agglayer::{ B2AggNote, ClaimNoteStorage, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, EthEmbeddedAccountId, ExitRoot, @@ -252,12 +253,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, - scale, - origin_network, - false, - &metadata_hash, + 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(), @@ -519,12 +522,14 @@ async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { // CREATE CONFIG_AGG_BRIDGE NOTE let config_note = ConfigAggBridgeNote::create( - agglayer_faucet.id(), - &origin_token_address, - scale, - origin_network, - false, - &leaf_data.metadata_hash, + 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(), @@ -678,12 +683,14 @@ async fn bridge_in_unlock_native_token() -> anyhow::Result<()> { // Register the native faucet with is_native = true. let config_note = ConfigAggBridgeNote::create( - native_faucet.id(), - &origin_token_address, - scale, - origin_network, - true, // is_native - &metadata_hash, + 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(), diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 66f9c92d7d..cb906670de 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -5,6 +5,7 @@ use miden_agglayer::{ AggLayerBridge, B2AggNote, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, ExitRoot, MetadataHash, @@ -107,12 +108,14 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { // 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, - scale, - origin_network, - false, - &metadata_hash, + 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(), @@ -609,12 +612,14 @@ async fn bridge_out_lock_native_token() -> anyhow::Result<()> { let metadata_hash = MetadataHash::from_token_info("Native Token", "NATIVE", 8); let config_note = ConfigAggBridgeNote::create( - native_faucet.id(), - &origin_token_address, - scale, - origin_network, - true, // is_native - &metadata_hash, + 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(), diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index aac36cf181..d3bf1ff035 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -3,6 +3,7 @@ extern crate alloc; use miden_agglayer::{ AggLayerBridge, ConfigAggBridgeNote, + ConversionMetadata, EthAddress, MetadataHash, create_existing_bridge_account, @@ -70,12 +71,14 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { let origin_network = 0u32; let metadata_hash = MetadataHash::from_token_info("USD Coin", "USDC", 6); let config_note = ConfigAggBridgeNote::create( - faucet_to_register, - &origin_token_address, - scale, - origin_network, - false, - &metadata_hash, + 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(), From 80eb103fe5ba85f0df0d3cb25bd3660d362c915a Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 22 Apr 2026 12:58:11 -0400 Subject: [PATCH 12/28] refactor: split bridge_in output-note emission into bridge_in_output.masm Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/miden-agglayer/SPEC.md | 2 +- .../asm/agglayer/bridge/bridge_in.masm | 309 +---------------- .../asm/agglayer/bridge/bridge_in_output.masm | 314 ++++++++++++++++++ 3 files changed, 320 insertions(+), 305 deletions(-) create mode 100644 crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index d5a6eff16e..f45ebe7a81 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -1090,7 +1090,7 @@ 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::build_mint_output_note` — emits a MINT note consumed by the faucet. | `bridge_in::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. | +| 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` diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 361d216b49..fa20bca76c 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -1,4 +1,5 @@ use agglayer::bridge::bridge_config +use agglayer::bridge::bridge_in_output use agglayer::bridge::leaf_utils use agglayer::common::utils use agglayer::common::asset_conversion @@ -7,17 +8,8 @@ use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::poseidon2 use miden::core::mem use miden::core::word -use miden::protocol::asset -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::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 use miden::protocol::types::DoubleWord use miden::protocol::types::MemoryAddress @@ -58,24 +50,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 # ------------------------------------------------------------------------------------------------- @@ -102,9 +76,11 @@ 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 +# CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT is also mirrored in bridge_in_output.masm; keep values in sync. const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 # Memory addresses for stored keys (used by claim procedure) +# CLAIM_PROOF_DATA_KEY_MEM_ADDR is also mirrored in bridge_in_output.masm; keep values in sync. const CLAIM_PROOF_DATA_KEY_MEM_ADDR = 700 const CLAIM_LEAF_DATA_KEY_MEM_ADDR = 704 @@ -133,18 +109,6 @@ const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556 -# 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 # ------------------------------------------------------------------------------------------------- @@ -244,13 +208,13 @@ pub proc claim 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.unlock_and_send + 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.build_mint_output_note + exec.bridge_in_output::build_mint_output_note # => [pad(16)] end end @@ -787,269 +751,6 @@ proc load_origin_token_address # => [origin_token_addr(5)] 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] - - exec.output_note::set_attachment - # => [] -end - -#! Local memory layout inside `unlock_and_send`: -#! - [0..3]: ASSET_KEY -#! - [4..7]: ASSET_VALUE -#! - [8]: destination_id_suffix -#! - [9]: destination_id_prefix -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. -#! -#! Inputs: [destination_id_suffix, destination_id_prefix, faucet_id_suffix, faucet_id_prefix] -#! Outputs: [] -#! -#! Invocation: exec -@locals(10) -proc unlock_and_send - # Stash destination to locals (claim's CLAIM_DEST_ID_*_LOCAL is in a different frame and is - # not visible here — `exec` invocations get their own local frame). - 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 - #! 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..5df240e146 --- /dev/null +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm @@ -0,0 +1,314 @@ +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 + +# Shared memory addresses — mirrored from bridge_in.masm; must stay in sync with the +# values defined there. These are global offsets into the claim's working memory that +# both `claim` (in bridge_in.masm) and the emission procs below read from. +const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 +const CLAIM_PROOF_DATA_KEY_MEM_ADDR = 700 + +# 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 + +# 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 + +# 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 + +#! 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. +#! +#! 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 (claim's CLAIM_DEST_ID_*_LOCAL is in a different frame and is + # not visible here — `exec` invocations get their own local frame). + 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 From 50d6a8fbf64bf74a90b165ae7a1c0fb6bcac3578 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 23 Apr 2026 11:07:30 -0400 Subject: [PATCH 13/28] refactor: fix stack-comment depths in CONFIG_AGG_BRIDGE.masm The inter-call dropw sequences were removed, so the input pad(12) now persists across both call boundaries. Update every # => [...] comment to reflect the true sdepth (verified with sdepth debug.stack drop): pad(16) through the setup, pad(32) after call 1, pad(48) after call 2, pad(16) after the final repeat.8 cleanup. --- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) 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 5abeaa52a9..5544eb18b1 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -87,24 +87,20 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! - The account does not expose the register_faucet or store_faucet_metadata_hash procedures. begin dropw - # => [pad(12)] + # => [pad(16)] # Ensure note attachment targets the consuming bridge account. exec.network_account_target::active_account_matches_target_account assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH - # => [pad(12)] + # => [pad(16)] # Load note storage to memory push.STORAGE_START_PTR exec.active_note::get_storage - # => [num_storage_items, dest_ptr, pad(12)] + # => [num_storage_items, dest_ptr, pad(16)] # Validate the number of storage items push.CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS assert_eq.err=ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS drop - # => [pad(12)] - - # Drop the remaining pad to start fresh - dropw dropw dropw - # => [] + # => [pad(16)] # --- Call 1: register_faucet --- # Expects: [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] @@ -117,58 +113,58 @@ begin 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] + # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(16)] # Load addr0..addr3 on top in reverse order so addr0 ends up on top 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] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(16)] - # Pad to 16 (need 6 more zeros) + # Pad the call input region to 16 (need 6 more zeros) padw push.0.0 - # => [0, 0, 0, 0, 0, 0, addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native] + # => [0, 0, 0, 0, 0, 0, addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(16)] - # Move 6 pad zeros to the end + # Move 6 pad zeros to the end of the call input region movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] + # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(22)] call.bridge_config::register_faucet - # => [pad(16)] + # => [pad(32)] # --- Call 2: store_faucet_metadata_hash --- # Expects: [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] - # Drop all 16 pad elements and rebuild the stack - dropw dropw dropw dropw - # => [] - # Load MH_HI from memory addresses 14..17 (individual loads — not word-aligned) mem_load.METADATA_HASH_HI_3 mem_load.METADATA_HASH_HI_2 mem_load.METADATA_HASH_HI_1 mem_load.METADATA_HASH_HI_0 - # => [MH_HI] + # => [MH_HI, pad(32)] # Load MH_LO from memory addresses 10..13 (individual loads — not word-aligned) mem_load.METADATA_HASH_LO_3 mem_load.METADATA_HASH_LO_2 mem_load.METADATA_HASH_LO_1 mem_load.METADATA_HASH_LO_0 - # => [MH_LO, MH_HI] + # => [MH_LO, MH_HI, pad(32)] # Load faucet_id_prefix, faucet_id_suffix mem_load.FAUCET_ID_PREFIX mem_load.FAUCET_ID_SUFFIX - # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI] + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(32)] - # Pad to 16 (need 6 more) + # Pad the call input region to 16 (need 6 more zeros) padw push.0.0 - # => [0, 0, 0, 0, 0, 0, faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI] + # => [0, 0, 0, 0, 0, 0, faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(32)] - # Move pad to end + # Move 6 pad zeros to the end of the call input region movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(38)] call.bridge_config::store_faucet_metadata_hash - # => [pad(16)] + # => [pad(48)] - dropw dropw dropw dropw + # Drop 32 to bring sdepth back to the 16-minimum. + repeat.8 + dropw + end + # => [pad(16)] end From ba2fa99f488cb744aa2886aca42b213411fab542 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 23 Apr 2026 11:59:09 -0400 Subject: [PATCH 14/28] refactor: clarify docstrings and add native-path duplicate-claim test - bridge_config.masm: reword the register_faucet local-offset comment and move an implementation detail out of get_faucet_metadata_hash's docstring into an inline comment. - bridge_in_output.masm: document that unlock_and_send's replay safety comes from the nullifier check in bridge_in::claim, not serial-number uniqueness. - bridge_in.rs: add bridge_in_unlock_native_duplicate_rejected. Seeds the bridge vault with 2x the claim amount so the nullifier is the only thing stopping a second unlock, then asserts the replay fails with ERR_CLAIM_ALREADY_SPENT. --- .../asm/agglayer/bridge/bridge_config.masm | 6 +- .../asm/agglayer/bridge/bridge_in_output.masm | 5 + .../miden-testing/tests/agglayer/bridge_in.rs | 210 ++++++++++++++++++ 3 files changed, 217 insertions(+), 4 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 4afd3ac6fb..ef783426d0 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -31,7 +31,7 @@ const IS_FAUCET_REGISTERED_FLAG = 1 # Offset in the local memory of the `hash_token_address` procedure const TOKEN_ADDR_HASH_PTR = 0 -# Offsets in the local memory of the `register_faucet` procedure +# 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 @@ -349,8 +349,6 @@ end #! - Key [2, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_LO #! - Key [3, 0, faucet_id_suffix, faucet_id_prefix] -> METADATA_HASH_HI #! -#! Sub-key 3 (HI) is read first so the final stack has MH_LO on top without a swapw. -#! #! Inputs: [faucet_id_suffix, faucet_id_prefix] #! Outputs: [METADATA_HASH_LO, METADATA_HASH_HI] #! @@ -360,7 +358,7 @@ proc get_faucet_metadata_hash dup.1 dup.1 # => [faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] - # Read sub-key 3 (MH_HI) first so MH_LO ends up on top after the second read. + # Read sub-key 3 (MH_HI) first so MH_LO ends up on top after the second read without a swapw. push.0.3 # => [3, 0, faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm index 5df240e146..fd202797c9 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm @@ -93,6 +93,11 @@ end #! (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: [] #! diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index b760a300e1..d1aae3641b 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -862,6 +862,216 @@ async fn bridge_in_unlock_native_token() -> anyhow::Result<()> { 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::SimulatedL1ToMiden; + 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; + + 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())); + + 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(), + claim_sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(claim_note_1.clone())); + + 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(), + claim_sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(claim_note_2.clone())); + + 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 — register native 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: 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()? + .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: 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()?; + + // 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!( + error_msg.contains(&expected_err_code), + "expected error code {expected_err_code} for 'claim note has already been spent', got: {error_msg}" + ); + + Ok(()) +} + #[tokio::test] async fn solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; From e197f46bdf34eead97f2424b5ad8ba1854bb280e Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 29 Apr 2026 20:51:58 -0400 Subject: [PATCH 15/28] refactor: dedupe shared CLAIM_* consts and colocate UNLOCK_*_LOC Make CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT and CLAIM_PROOF_DATA_KEY_MEM_ADDR public in bridge_in.masm and import them from bridge_in_output.masm, removing the duplicate definitions and the "keep in sync" comments. Move UNLOCK_*_LOC consts to sit immediately above unlock_and_send so the local-memory layout lives next to the procedure that uses it. --- .../asm/agglayer/bridge/bridge_in.masm | 6 ++---- .../asm/agglayer/bridge/bridge_in_output.masm | 20 ++++++++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index fa20bca76c..3fd145cb17 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -76,12 +76,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 -# CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT is also mirrored in bridge_in_output.masm; keep values in sync. -const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 +pub const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 # Memory addresses for stored keys (used by claim procedure) -# CLAIM_PROOF_DATA_KEY_MEM_ADDR is also mirrored in bridge_in_output.masm; keep values in sync. -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 diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm index fd202797c9..0e6dcfa24f 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm @@ -1,3 +1,5 @@ +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 @@ -30,12 +32,6 @@ 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 -# Shared memory addresses — mirrored from bridge_in.masm; must stay in sync with the -# values defined there. These are global offsets into the claim's working memory that -# both `claim` (in bridge_in.masm) and the emission procs below read from. -const CLAIM_OUTPUT_NOTE_FAUCET_AMOUNT = 568 -const CLAIM_PROOF_DATA_KEY_MEM_ADDR = 700 - # Memory addresses for MINT note output construction const MINT_NOTE_STORAGE_MEM_ADDR_0 = 800 const MINT_NOTE_STORAGE_DEST_TAG = 800 @@ -48,12 +44,6 @@ const MINT_NOTE_STORAGE_OUTPUT_SERIAL_NUM = 812 const MINT_NOTE_STORAGE_OUTPUT_NOTE_SUFFIX = 816 const MINT_NOTE_STORAGE_OUTPUT_NOTE_PREFIX = 817 -# 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 - # PUBLIC INTERFACE # ================================================================================================= @@ -83,6 +73,12 @@ pub proc build_mint_output_note # => [] 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. #! From fe4d17498d6e04c3dd4f090f9bf0825f7ab912cc Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Tue, 5 May 2026 09:27:37 -0400 Subject: [PATCH 16/28] refactor: apply PR suggestions Co-authored-by: Marti --- crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm | 2 +- crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index ef783426d0..7ba42aa11f 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -276,7 +276,7 @@ proc assert_faucet_registered # => [VALUE] # the stored word is [1, is_native, 0, 0] for registered faucets - # assert element 0 (registration flag) is non-zero + # assert element 0 (registration flag) equals "1" assert.err=ERR_FAUCET_NOT_REGISTERED drop drop drop # => [] end 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 5544eb18b1..bf19e764f7 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -105,8 +105,7 @@ begin # --- Call 1: register_faucet --- # Expects: [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] - # Build the stack bottom-up: load deepest elements first, then push shallower ones on top. - # This avoids complex rearrangement since each mem_load pushes onto the top. + # Build the stack by loading deepest elements first mem_load.IS_NATIVE mem_load.ORIGIN_NETWORK mem_load.SCALE From c5c0a9e9f52b54dfc4289d6701e46802ca87937e Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:27:29 -0400 Subject: [PATCH 17/28] refactor(agglayer): drop redundant memory layout description in CONFIG_AGG_BRIDGE.masm The 18-felt note storage layout was documented in three places: the top-of-file comment block, the constant definitions immediately below, and the docblock above 'begin'. The constants are self-documenting and the docblock covers it for callers, so drop the top-of-file block. --- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 14 -------------- 1 file changed, 14 deletions(-) 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 c132fe179a..4b91abffbf 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -9,20 +9,6 @@ const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 18 const STORAGE_START_PTR = 0 -# Memory layout after get_storage (18 felts): -# [0] origin_token_addr_0 -# [1] origin_token_addr_1 -# [2] origin_token_addr_2 -# [3] origin_token_addr_3 -# [4] origin_token_addr_4 -# [5] faucet_id_suffix -# [6] faucet_id_prefix -# [7] scale -# [8] origin_network -# [9] is_native -# [10..13] METADATA_HASH_LO_0..3 (4 felts) -# [14..17] METADATA_HASH_HI_0..3 (4 felts) - const ORIGIN_TOKEN_ADDR_0 = 0 const ORIGIN_TOKEN_ADDR_1 = 1 const ORIGIN_TOKEN_ADDR_2 = 2 From 222df449457dbeda7d6b9403b44a3311d731bbc1 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:31:26 -0400 Subject: [PATCH 18/28] refactor(agglayer): add mem_load_double_word_unaligned helper Mirror mem_store_double_word_unaligned in asm/agglayer/common/utils.masm and use it in CONFIG_AGG_BRIDGE.masm to load the metadata-hash double word from offset 10..17, replacing eight individual mem_load.METADATA_HASH_* ops with a single push.METADATA_HASH_LO_0 exec.utils::mem_load_double_word_unaligned. --- .../asm/agglayer/common/utils.masm | 27 +++++++++++++++++++ .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 11 +++----- 2 files changed, 30 insertions(+), 8 deletions(-) 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/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm index 4b91abffbf..e6a65e568a 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -1,4 +1,5 @@ use agglayer::bridge::bridge_config +use agglayer::common::utils use miden::protocol::active_note use miden::standards::attachments::network_account_target @@ -122,14 +123,8 @@ begin # --- Call 2: store_faucet_metadata_hash --- # Expects: [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] - # Load MH_HI from memory addresses 14..17 (individual loads — not word-aligned) - mem_load.METADATA_HASH_HI_3 mem_load.METADATA_HASH_HI_2 - mem_load.METADATA_HASH_HI_1 mem_load.METADATA_HASH_HI_0 - # => [MH_HI, pad(32)] - - # Load MH_LO from memory addresses 10..13 (individual loads — not word-aligned) - mem_load.METADATA_HASH_LO_3 mem_load.METADATA_HASH_LO_2 - mem_load.METADATA_HASH_LO_1 mem_load.METADATA_HASH_LO_0 + # Load [MH_LO, MH_HI] from memory addresses 10..17 (unaligned). + push.METADATA_HASH_LO_0 exec.utils::mem_load_double_word_unaligned # => [MH_LO, MH_HI, pad(32)] # Load faucet_id_prefix, faucet_id_suffix From 57cf8c6b1df74feeafecbce8cb266af576222710 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:32:43 -0400 Subject: [PATCH 19/28] refactor(agglayer): pre-pad CONFIG_AGG_BRIDGE call frames to drop the movdn dance Push the call's 6 trailing pad zeros first, then build the args on top, instead of pushing args first and then doing 'movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15' to move 6 pads to the bottom. The end-state stack is identical; this just removes a 6-instruction shuffle from each of the two call sites. --- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) 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 e6a65e568a..defee7ce16 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -93,29 +93,24 @@ begin # --- Call 1: register_faucet --- # Expects: [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] - # Build the stack by loading deepest elements first + # Push the call's 6 trailing pad zeros first, then build the 10 args on top. This + # avoids the `movdn.15 ×6` shuffle that an after-the-fact pad would require. + padw push.0.0 + # => [pad(6), pad(16)] + 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(16)] + # => [addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6), pad(16)] - # Load addr0..addr3 on top in reverse order so addr0 ends up on top 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(16)] - - # Pad the call input region to 16 (need 6 more zeros) - padw push.0.0 - # => [0, 0, 0, 0, 0, 0, addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(16)] - - # Move 6 pad zeros to the end of the call input region - movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(22)] + # => [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(32)] @@ -123,22 +118,16 @@ begin # --- Call 2: store_faucet_metadata_hash --- # Expects: [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)] - # Load [MH_LO, MH_HI] from memory addresses 10..17 (unaligned). + # 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(32)] + # => [MH_LO, MH_HI, pad(6), pad(32)] - # Load faucet_id_prefix, faucet_id_suffix mem_load.FAUCET_ID_PREFIX mem_load.FAUCET_ID_SUFFIX - # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(32)] - - # Pad the call input region to 16 (need 6 more zeros) - padw push.0.0 - # => [0, 0, 0, 0, 0, 0, faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(32)] - - # Move 6 pad zeros to the end of the call input region - movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 movdn.15 - # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(38)] + # => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6), pad(32)] call.bridge_config::store_faucet_metadata_hash # => [pad(48)] From 762a909b8206733c7a6a8288ab7b1c74aa687042 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:35:27 -0400 Subject: [PATCH 20/28] refactor(agglayer): introduce sub-key constants for faucet_metadata_map Replace literal push.0.0 / push.0.1 / push.0.2 / push.0.3 sub-key prefixes with named constants FAUCET_METADATA_SUBKEY_{ADDR_LO,ADDR_HI, HASH_LO,HASH_HI}. Only the metadata-map sites are touched; push.0.0 in faucet_registry / token_registry contexts is a different map and stays literal. --- .../asm/agglayer/bridge/bridge_config.masm | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 11c861a71d..7134d866ab 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -40,6 +40,13 @@ 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 # ================================================================================================= @@ -168,8 +175,9 @@ pub proc register_faucet # --- 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] - 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, addr0, addr1, addr2, addr3, addr4, pad(11)] + 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_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -177,7 +185,7 @@ pub proc register_faucet # => [addr4, pad(15)] # --- Step 2: Store origin address part 2 + origin_network + scale --- - # KEY = [1, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [addr4, origin_network, scale, 0] + # KEY = [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix], VALUE = [addr4, origin_network, scale, 0] push.0 loc_load.REG_SCALE_LOC @@ -188,8 +196,8 @@ pub proc register_faucet 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(11)] - push.0.1 - # => [1, 0, faucet_id_suffix, faucet_id_prefix, addr4, origin_network, scale, 0, pad(11)] + push.0.FAUCET_METADATA_SUBKEY_ADDR_HI + # => [SUBKEY_ADDR_HI, 0, faucet_id_suffix, faucet_id_prefix, addr4, origin_network, scale, 0, pad(11)] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -246,21 +254,21 @@ 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 [2, 0, faucet_id_suffix, faucet_id_prefix] --- + # --- 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.2 - # => [2, 0, 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 [3, 0, faucet_id_suffix, faucet_id_prefix] --- - push.0.3 - # => [3, 0, 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 @@ -305,9 +313,9 @@ end #! #! Invocation: exec proc get_faucet_scale - # Build KEY = [1, 0, faucet_id_suffix, faucet_id_prefix] - push.0.1 - # => [1, 0, faucet_id_suffix, faucet_id_prefix] + # 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 @@ -329,13 +337,13 @@ end #! #! Invocation: exec proc get_faucet_conversion_info - # Prepare the sub-key 0 KEY underneath. - push.0.0 - # => [0, 0, faucet_id_suffix, faucet_id_prefix] + # 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 sub-key 1 KEY on top. - dup.3 dup.3 push.0.1 - # => [1, 0, faucet_id_suffix, faucet_id_prefix, 0, 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] @@ -372,9 +380,9 @@ proc get_faucet_metadata_hash dup.1 dup.1 # => [faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] - # Read sub-key 3 (MH_HI) first so MH_LO ends up on top after the second read without a swapw. - push.0.3 - # => [3, 0, faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] + # Read SUBKEY_HASH_HI first so MH_LO ends up on top after the second read without a swapw. + push.0.FAUCET_METADATA_SUBKEY_HASH_HI + # => [SUBKEY_HASH_HI, 0, faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item @@ -384,9 +392,9 @@ proc get_faucet_metadata_hash movup.5 movup.5 # => [faucet_id_suffix, faucet_id_prefix, MH_HI] - # Read sub-key 2: METADATA_HASH_LO - push.0.2 - # => [2, 0, faucet_id_suffix, faucet_id_prefix, MH_HI] + # Read SUBKEY_HASH_LO: METADATA_HASH_LO + push.0.FAUCET_METADATA_SUBKEY_HASH_LO + # => [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix, MH_HI] push.FAUCET_METADATA_MAP_SLOT[0..2] exec.active_account::get_map_item From 50ff9949cd2aa2b8ca6d5243de0a311a03a693b6 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:36:19 -0400 Subject: [PATCH 21/28] refactor(agglayer): cheaper register_faucet local stash via repeated movup.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace movup.9/.8/.7/.6/.5 with five repeated movup.5 starting from the now-deepest non-address element. Each movup always lifts from depth 5 (instead of climbing 9→5), and the post-iteration stack is identical. --- .../asm/agglayer/bridge/bridge_config.masm | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 7134d866ab..f8af85c45a 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -143,12 +143,14 @@ pub proc register_faucet exec.assert_sender_is_bridge_admin # => [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.9 loc_store.REG_IS_NATIVE_LOC - movup.8 loc_store.REG_ORIGIN_NETWORK_LOC - movup.7 loc_store.REG_SCALE_LOC - movup.6 loc_store.REG_FAUCET_ID_PREFIX_LOC + # Save non-address data to locals. Each `movup.5` lifts the now-deepest non-address + # element from depth 5 — cheaper than climbing 9→8→7→6→5 since each lift moves fewer + # elements. 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 address for hashing before it gets consumed From 6940c0a79f0127d64972182cdc45cff2c39e5bba Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:37:41 -0400 Subject: [PATCH 22/28] refactor(agglayer): use repeat.5 dup.4 to duplicate origin token address Replace 'dup.4 dup.4 dup.4 dup.4 dup.4' with 'repeat.5 dup.4 end'. The five-fold dup-from-depth-4 produces the same stack pattern (top word is the duplicate address with addr0 at the very top) which is what hash_token_address expects when it stores the 5-felt address into local memory before computing Poseidon2. --- .../miden-agglayer/asm/agglayer/bridge/bridge_config.masm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index f8af85c45a..2b9305e5fc 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -153,8 +153,10 @@ pub proc register_faucet movup.5 loc_store.REG_IS_NATIVE_LOC # => [addr0, addr1, addr2, addr3, addr4, pad(11)] - # Duplicate address for hashing before it gets consumed - dup.4 dup.4 dup.4 dup.4 dup.4 + # 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(6)] # Push origin_network to position 5 so hash_token_address sees [addr(5), origin_network]. From b499207d7c283926f406ad8f59e068b07d34ea84 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:38:50 -0400 Subject: [PATCH 23/28] refactor(agglayer): unify get_faucet_metadata_hash key-prep with sibling proc Rewrite get_faucet_metadata_hash to use the same 'prep both keys first, then swapw between reads' pattern as get_faucet_conversion_info. The previous shape used 'dup.1 dup.1 ... movup.5 movup.5' to reshuffle faucet_id around the first read, which read awkwardly next to a sibling proc with the opposite mechanism. This subsumes the swapw / movup.5 movup.5 oddity Marti called out. --- .../asm/agglayer/bridge/bridge_config.masm | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 2b9305e5fc..2d4759411e 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -380,26 +380,24 @@ end #! #! Invocation: exec proc get_faucet_metadata_hash - # Save faucet_id for second read - dup.1 dup.1 - # => [faucet_id_suffix, faucet_id_prefix, faucet_id_suffix, faucet_id_prefix] + # Prepare the SUBKEY_HASH_LO KEY underneath. + push.0.FAUCET_METADATA_SUBKEY_HASH_LO + # => [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] - # Read SUBKEY_HASH_HI first so MH_LO ends up on top after the second read without a swapw. - push.0.FAUCET_METADATA_SUBKEY_HASH_HI - # => [SUBKEY_HASH_HI, 0, faucet_id_suffix, faucet_id_prefix, 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, faucet_id_suffix, faucet_id_prefix] - - # Move fid below MH_HI - movup.5 movup.5 - # => [faucet_id_suffix, faucet_id_prefix, MH_HI] + # => [MH_HI, SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] - # Read SUBKEY_HASH_LO: METADATA_HASH_LO - push.0.FAUCET_METADATA_SUBKEY_HASH_LO + # 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] From 4b1642c1ba91a5878568e06a736acf594800e9b8 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:44:39 -0400 Subject: [PATCH 24/28] refactor(agglayer): correct stack-padding annotations in register_faucet Re-walk stack annotations from proc-entry through Steps 2-4. Pads grow on pops (via Miden's auto-padding to depth-16 floor) and stay constant on pushes; the previous comments treated them as if pad+named always equaled 16, which understated the depth after each new push. --- .../asm/agglayer/bridge/bridge_config.masm | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 2d4759411e..829ae8fa40 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -157,7 +157,7 @@ pub proc register_faucet repeat.5 dup.4 end - # => [addr0, addr1, addr2, addr3, addr4, addr0, addr1, addr2, addr3, addr4, pad(6)] + # => [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 @@ -168,10 +168,10 @@ pub proc register_faucet # 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(6)] + # => [addr0, addr1, addr2, addr3, addr4, origin_network_swapped, addr0, addr1, addr2, addr3, addr4, pad(11)] exec.hash_token_address - # => [TOKEN_ADDR_HASH, addr0, addr1, addr2, addr3, addr4, pad(6)] + # => [TOKEN_ADDR_HASH, addr0, addr1, addr2, addr3, addr4, pad(11)] loc_storew_le.REG_TOKEN_HASH_LOC dropw # => [addr0, addr1, addr2, addr3, addr4, pad(11)] @@ -195,28 +195,28 @@ pub proc register_faucet loc_load.REG_SCALE_LOC loc_load.REG_ORIGIN_NETWORK_LOC movup.3 - # => [addr4, origin_network, scale, 0, pad(11)] + # => [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(11)] + # => [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(11)] + # => [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 - # => [pad(11)] + # => [pad(16)] # --- 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. loc_load.REG_IS_NATIVE_LOC push.IS_FAUCET_REGISTERED_FLAG - # => [1, is_native, pad(14)] + # => [1, is_native, pad(16)] 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(14)] + # => [0, 0, faucet_id_suffix, faucet_id_prefix, 1, is_native, pad(16)] push.FAUCET_REGISTRY_MAP_SLOT[0..2] exec.native_account::set_map_item @@ -226,13 +226,13 @@ pub proc register_faucet # --- 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(11)] + # => [faucet_id_suffix, faucet_id_prefix, pad(16)] push.0.0 - # => [0, 0, faucet_id_suffix, faucet_id_prefix, pad(11)] + # => [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(7)] + # => [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 From 7ea675aad885f1a2593d3908eaeba6d99b169983 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:45:19 -0400 Subject: [PATCH 25/28] refactor(agglayer): clarify is_faucet_native trailing-zeros comment 'sink ... below the trailing zeros' was misleading because the stack direction it implied is the opposite of what movdn.2 does. Reword to 'move is_native past the two trailing zeros'. --- crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 829ae8fa40..c84c37ab56 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -420,7 +420,7 @@ proc is_faucet_native exec.active_account::get_map_item # => [1, is_native, 0, 0] - # Drop element 0 (registration flag), sink is_native below the trailing zeros, drop them. + # Drop element 0 (registration flag), move is_native past the two trailing zeros, drop them. drop movdn.2 drop drop # => [is_native] end From d96fcdef0b9e307ed55abe839e6e0d5c8c52475d Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:45:56 -0400 Subject: [PATCH 26/28] refactor(agglayer): clarify unlock_and_send local-stash comment The previous comment referenced 'claim's CLAIM_DEST_ID_*_LOCAL' and the 'exec invocations get their own local frame' detail, both of which are distracting. State the actual reason for the stash: we'll need the destination ID again after native_account::remove_asset clears the stack. --- .../miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm index 0e6dcfa24f..4985c866c6 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in_output.masm @@ -100,8 +100,8 @@ const UNLOCK_DEST_PREFIX_LOC = 9 #! Invocation: exec @locals(10) pub proc unlock_and_send - # Stash destination to locals (claim's CLAIM_DEST_ID_*_LOCAL is in a different frame and is - # not visible here — `exec` invocations get their own local frame). + # 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] From cfa5b4c6b96590d9950094f668e540c5d7bb0805 Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 5 May 2026 10:47:47 -0400 Subject: [PATCH 27/28] docs(agglayer): document native faucet registration and updated bridge flows - Section 2.1 (bridge-out): drop FPI references, replace with bridge-local faucet_metadata_map reads. Add the is_native dispatch (BURN vs lock_asset). - Section 2.2 (bridge-in): drop the unconditional MINT description, replace with the is_native dispatch (MINT vs remove_asset + direct P2ID). Update the token-registry lookup to mention the (address, network) pair. - Section 2.4 (faucet registration): describe the CONFIG_AGG_BRIDGE 18-felt payload and the two-call register_faucet / store_faucet_metadata_hash flow. - Section 7.1: rename to 'Registering faucets on Miden' (covers both wrapped and Miden-native faucets), and add a new sub-section explaining the wrapped-vs-native distinction at the dispatch level. --- crates/miden-agglayer/SPEC.md | 85 +++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 0a031aa846..95a74f89c4 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -40,14 +40,21 @@ asset and the destination network/address. The bridge account consumes this note 1. Validates that the asset's faucet is registered in the faucet registry, and that the destination network is not Miden's AggLayer network ID. -2. FPIs to the faucet (`agglayer_faucet::asset_to_origin_asset`) to obtain the scaled - U256 amount, origin token address, and origin network. -3. FPIs to the faucet (`agglayer_faucet::get_metadata_hash`) to obtain the metadata hash. +2. Reads conversion metadata (origin token address, origin network, scale) for the asset's + faucet from the bridge's local `faucet_metadata_map`. No FPI into the faucet is required; + metadata was written to the map at registration time. +3. Reads the precomputed metadata hash for the same faucet from `faucet_metadata_map`. 4. Constructs a leaf-data structure (leaf type, origin network, origin token address, destination network, destination address, amount, metadata hash). 5. Computes the Keccak-256 leaf value and appends it to the Local Exit Tree (LET). -6. Creates a public [`BURN`](#45-burn-generated) note targeting the faucet, which burns the asset and - decreases the faucet's token supply. +6. Dispatches on the faucet's `is_native` flag (also read from the registry): + - **Wrapped faucet (`is_native = false`):** the bridge does not hold the asset onchain; it + emits a public [`BURN`](#45-burn-generated) note targeting the faucet, which the faucet + consumes to burn the asset and decrement the faucet's token supply. + - **Miden-native faucet (`is_native = true`):** the bridge does not hold mint/burn authority + for the faucet, so it cannot emit a `BURN`. Instead it locks the asset by adding it to + the bridge's own vault (`native_account::add_asset`); a later bridge-in claim for the + same token can pay out from this locked balance. The leaf appended to the LET can later be included in a Merkle proof on any AggLayer-connected chain to claim the bridged asset. @@ -73,12 +80,18 @@ The `CLAIM` note is consumed by the bridge account: 4. Updates the claimed global index (CGI) chain hash: `NEW_CGI = Keccak256(OLD_CGI, Keccak256(GLOBAL_INDEX, LEAF_VALUE))`. 5. Checks and sets the claim nullifier to prevent double-claiming. -6. Looks up the faucet from the origin token address via the token registry. +6. Looks up the faucet from the `(origin_token_address, origin_network)` pair via the token + registry. 7. Verifies the claim amount against the leaf's U256 amount and the faucet's scale factor. -8. Creates a [`MINT`](#47-mint-generated) note targeting the faucet. - -The faucet consumes the `MINT` note, mints the specified amount, and creates a [`P2ID`](#46-p2id-generated) note -that delivers the minted assets to the recipient's Miden account. +8. Dispatches on the faucet's `is_native` flag: + - **Wrapped faucet (`is_native = false`):** the bridge emits a [`MINT`](#47-mint-generated) + note targeting the faucet. The faucet consumes the `MINT` note, mints the specified amount, + and creates a [`P2ID`](#46-p2id-generated) note delivering the minted assets to the + recipient's Miden account. + - **Miden-native faucet (`is_native = true`):** the bridge cannot mint via the faucet, so + it removes the asset from its own vault (`native_account::remove_asset`) and emits a + `P2ID` note targeted at the recipient directly. The asset must have been previously + locked into the bridge by a prior bridge-out for the same token. Inside `bridge_in::claim`, immediately after proof and leaf data are piped into memory, the bridge asserts the leaf's `destination_network` equals the global MASM constant `MIDEN_NETWORK_ID` in `asm/agglayer/common/constants.masm` (after `swap_u32_bytes` on the LE-packed memory limb). The same value is exposed to Rust as `AggLayerBridge::MIDEN_NETWORK_ID`, matching Solidity test vectors. This mirrors Solidity `claimAsset` destination-network checks. @@ -117,11 +130,22 @@ TODO: Duplicate GER insertions are silently accepted ![Faucet registration flow](diagrams/faucet-registration.png) -Each bridged token requires a dedicated AggLayer faucet on Miden. The Bridge Operator -creates [`CONFIG_AGG_BRIDGE`](#43-config_agg_bridge) notes to register faucets. The bridge consumes these notes, -asserting the sender is the bridge admin, then registers the faucet in both the faucet -registry and the token registry. For a detailed description of the faucet and token -registries, see [Section 7](#7-faucet-registry). +Each bridged token (wrapped or Miden-native) requires registration in the bridge's +registries. The Bridge Operator creates [`CONFIG_AGG_BRIDGE`](#43-config_agg_bridge) notes +carrying the faucet's account ID, the origin token address, the origin network, the scale +factor, the metadata hash, and an `is_native` flag. The bridge consumes the note (asserting +the sender is the bridge admin) and runs two calls back-to-back: + +- `bridge_config::register_faucet` writes the registration flag plus `is_native` into + `faucet_registry_map`, the conversion metadata into `faucet_metadata_map` (sub-keys 0 and + 1), and the `(origin_token_address, origin_network) → faucet_id` mapping into + `token_registry_map`. +- `bridge_config::store_faucet_metadata_hash` writes the precomputed metadata hash into + `faucet_metadata_map` (sub-keys 2 and 3). + +The split is necessary because the 16-element MASM stack cannot fit all 18 registration +felts at once. For a detailed description of the registries, see +[Section 7](#7-faucet-registry). TODO: Faucet registrations are permanent; no remapping or deregistration is supported ([#2704](https://github.com/0xMiden/protocol/issues/2704), @@ -1030,11 +1054,13 @@ consumption: is required for native-token support, where the faucet is not under the bridge's control and does not necessarily expose any AggLayer-specific procedures. -### 7.1 Bridging-in: Registering non-native faucets on Miden +### 7.1 Registering faucets on Miden -When a new ERC20 token is bridged to Miden for the first time, a corresponding AggLayer -faucet account must be created and registered. The faucet serves as the mint/burn -authority for the wrapped token on Miden. +Every faucet that participates in bridging — whether it represents a wrapped foreign token +or a Miden-native token — must be registered in the bridge's three registries before it can +be referenced by a `B2AGG` (bridge-out) or `CLAIM` (bridge-in) note. Registration is the +same flow for both kinds; the `is_native` flag in the `CONFIG_AGG_BRIDGE` note storage tells +the bridge which dispatch path to take for each future bridge operation against that faucet. The `AggLayerFaucet` Rust struct (`src/faucet.rs`) holds only token metadata — symbol, decimals, max supply, and token supply @@ -1072,6 +1098,27 @@ The bridge admin is a trusted role, and is the sole entity that can register fau 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) From f61c3ada0e1389a793dc32a99fca1182b60bcabd Mon Sep 17 00:00:00 2001 From: Marti Date: Thu, 7 May 2026 14:54:07 +0200 Subject: [PATCH 28/28] Apply suggestions from code review Co-authored-by: Marti --- crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm | 4 +--- crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index c84c37ab56..7dabbd470b 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -143,9 +143,7 @@ pub proc register_faucet exec.assert_sender_is_bridge_admin # => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] - # Save non-address data to locals. Each `movup.5` lifts the now-deepest non-address - # element from depth 5 — cheaper than climbing 9→8→7→6→5 since each lift moves fewer - # elements. + # 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 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 defee7ce16..1324b31af7 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -93,8 +93,7 @@ begin # --- Call 1: register_faucet --- # Expects: [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)] - # Push the call's 6 trailing pad zeros first, then build the 10 args on top. This - # avoids the `movdn.15 ×6` shuffle that an after-the-fact pad would require. + # Push the call's 6 trailing pad zeros first, then build the 10 args on top. padw push.0.0 # => [pad(6), pad(16)]