diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 5876aea0ae..326e9800e7 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -38,7 +38,8 @@ The crate `miden-agglayer` implements the AggLayer bridging protocol on the Mide A user initiates a bridge-out by creating a [`B2AGG`](#41-b2agg) note containing a single fungible 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. +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. diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index db367d415f..2e3b091001 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -11,6 +11,7 @@ use miden::standards::note::execution_hint::ALWAYS use miden::protocol::types::MemoryAddress use miden::protocol::output_note use miden::core::crypto::hashes::poseidon2 +use agglayer::common::constants::MIDEN_NETWORK_ID use agglayer::common::utils use agglayer::faucet -> agglayer_faucet use agglayer::bridge::bridge_config @@ -18,6 +19,11 @@ use agglayer::bridge::leaf_utils use agglayer::bridge::merkle_tree_frontier use agglayer::common::eth_address::EthereumAddressFormat +# ERRORS +# ================================================================================================= + +const ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN = "B2AGG note destination network ID must not be Miden's AggLayer network ID" + # CONSTANTS # ================================================================================================= @@ -100,6 +106,9 @@ const BURN_NOTE_NUM_STORAGE_ITEMS=0 #! - dest_network_id is the u32 destination network/chain ID. #! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. #! +#! Panics if: +#! - destination network ID is Miden's AggLayer network ID. +#! #! Invocation: call @locals(14) pub proc bridge_out @@ -110,6 +119,9 @@ pub proc bridge_out exec.asset::store # => [dest_network_id, dest_address(5), pad(10)] + dup exec.assert_destination_id_not_miden_id + # => [dest_network_id, dest_address(5), pad(10)] + loc_store.DESTINATION_NETWORK_LOC loc_store.DESTINATION_ADDRESS_0_LOC loc_store.DESTINATION_ADDRESS_1_LOC @@ -222,6 +234,26 @@ end # HELPER PROCEDURES # ================================================================================================= +#! Asserts that the bridge-out destination network ID is not equal to the Miden's AggLayer network +#! ID. +#! +#! Inputs: [dest_network_id] +#! Outputs: [] +#! +#! Panics if: +#! - the destination network ID equals `MIDEN_NETWORK_ID`. +#! +#! Invocation: exec +proc assert_destination_id_not_miden_id + # change the endianness to BE to compare it with the Miden network ID + exec.utils::swap_u32_bytes + # => [destination_network_id_be] + + # assert that the destination network ID is not equal to the Miden network ID + push.MIDEN_NETWORK_ID neq assert.err=ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN + # => [] +end + #! Validates that a faucet is registered in the bridge's faucet registry, then performs an FPI call #! to the faucet's `asset_to_origin_asset` procedure to obtain the scaled amount, origin token #! address, and origin network. diff --git a/crates/miden-agglayer/asm/note_scripts/b2agg.masm b/crates/miden-agglayer/asm/note_scripts/b2agg.masm index 0ae42e52e8..e4605dc74e 100644 --- a/crates/miden-agglayer/asm/note_scripts/b2agg.masm +++ b/crates/miden-agglayer/asm/note_scripts/b2agg.masm @@ -50,6 +50,7 @@ const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account do #! - The note does not contain exactly 6 storage items. #! - The note does not contain exactly 1 asset. #! - The note attachment does not target the consuming account. +#! - The destination network ID equals Miden's AggLayer network ID. begin dropw # => [pad(16)] diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index e0a61d3e47..c4b23781a7 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,6 +1,10 @@ extern crate alloc; -use miden_agglayer::errors::{ERR_B2AGG_TARGET_ACCOUNT_MISMATCH, ERR_FAUCET_NOT_REGISTERED}; +use miden_agglayer::errors::{ + ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN, + ERR_B2AGG_TARGET_ACCOUNT_MISMATCH, + ERR_FAUCET_NOT_REGISTERED, +}; use miden_agglayer::{ AggLayerBridge, B2AggNote, @@ -352,6 +356,122 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> Ok(()) } +/// B2AGG / bridge-out must reject a note whose `destination_network` equals the Miden network ID, +/// even when the faucet is registered and the rest of the bridge-out path would otherwise succeed. +#[tokio::test] +async fn test_bridge_out_fails_when_destination_is_miden_network() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) + // -------------------------------------------------------------------------------------------- + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE GER MANAGER ACCOUNT (not used for GER in this test, but distinct from admin) + // -------------------------------------------------------------------------------------------- + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE BRIDGE ACCOUNT + // -------------------------------------------------------------------------------------------- + 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())?; + + // CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI) + // Use MTF vector token metadata and a fixed origin network compatible with the vectors. + // -------------------------------------------------------------------------------------------- + let vectors = &*SOLIDITY_MTF_VECTORS; + let origin_token_address = + EthAddress::from_hex(&vectors.origin_token_address).expect("valid origin token address"); + 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, + vectors.token_decimals, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(100), + bridge_account.id(), + &origin_token_address, + 64u32, + 0u8, + metadata_hash, + ); + builder.add_account(faucet.clone())?; + + // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) + // -------------------------------------------------------------------------------------------- + let config_note = ConfigAggBridgeNote::create( + faucet.id(), + &origin_token_address, + bridge_admin.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(config_note.clone())); + + // CREATE B2AGG NOTE (targets the bridge) + // Set destination_network to exactly `AggLayerBridge::MIDEN_NETWORK_ID` so `bridge_out` + // fails immediately. + // -------------------------------------------------------------------------------------------- + let amount = Felt::new(100); + let bridge_asset: Asset = + FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap().into(); + let eth_address = + EthAddress::from_hex(&vectors.destination_addresses[0]).expect("valid destination address"); + + let b2agg_note = B2AggNote::create( + AggLayerBridge::MIDEN_NETWORK_ID, + eth_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + faucet.id(), + builder.rng_mut(), + )?; + + builder.add_output_note(RawOutputNote::Full(b2agg_note.clone())); + + // BUILD MOCK CHAIN WITH ALL ACCOUNTS AND PENDING OUTPUT NOTES + // -------------------------------------------------------------------------------------------- + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // TX0: EXECUTE CONFIG_AGG_BRIDGE NOTE TO REGISTER FAUCET IN BRIDGE + // -------------------------------------------------------------------------------------------- + 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: EXECUTE B2AGG NOTE AGAINST BRIDGE (must fail: destination_network is Miden's ID) + // -------------------------------------------------------------------------------------------- + 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; + + assert_transaction_executor_error!(result, ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN); + + Ok(()) +} + /// Tests the B2AGG (Bridge to AggLayer) note script reclaim functionality. /// /// This test covers the "reclaim" branch where the note creator consumes their own B2AGG note.