From 03eca4cfb9a0351019815dc997b912088353dd34 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 13:51:09 +0200 Subject: [PATCH 01/17] feat(agglayer): add from_account_id to eth_address.masm Add the inverse of to_account_id for converting AccountId [suffix, prefix] into Ethereum address format [limb0, limb1, limb2, limb3, limb4]. This is needed by the bridge_message procedure to derive the origin Ethereum address from the note sender's AccountId. --- .../asm/agglayer/common/eth_address.masm | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm index b9cc4a9c33..ed11a8b9cb 100644 --- a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm +++ b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm @@ -69,6 +69,64 @@ pub proc to_account_id # => [suffix, prefix] end +#! Converts an AccountId [suffix, prefix] into the Ethereum address format (address[5] type). +#! +#! This is the inverse of `to_account_id`. Each felt encodes a conceptual u64 = (hi << 32) | lo +#! in big-endian limb order. The procedure splits each felt back into two u32 limbs via u32split, +#! then byte-swaps each limb from big-endian back to little-endian (matching the bridge convention). +#! +#! The most-significant 4 bytes (limb0) are always zero since an AccountId only occupies 16 bytes. +#! +#! Inputs: [suffix, prefix] +#! Outputs: [limb0, limb1, limb2, limb3, limb4] +#! +#! Invocation: exec +pub proc from_account_id + # split prefix into BE limbs + swap + # => [prefix, suffix] + + u32split + # => [hi_be, lo_be, suffix] + # prefix = hi_be * 2^32 + lo_be + + # byte-swap hi_be to get limb1 (LE) + exec.utils::swap_u32_bytes + # => [limb1, lo_be, suffix] + + swap + # => [lo_be, limb1, suffix] + + # byte-swap lo_be to get limb2 (LE) + exec.utils::swap_u32_bytes + # => [limb2, limb1, suffix] + + # split suffix into BE limbs + movup.2 + # => [suffix, limb2, limb1] + + u32split + # => [hi_be, lo_be, limb2, limb1] + + # byte-swap hi_be to get limb3 (LE) + exec.utils::swap_u32_bytes + # => [limb3, lo_be, limb2, limb1] + + swap + # => [lo_be, limb3, limb2, limb1] + + # byte-swap lo_be to get limb4 (LE) + exec.utils::swap_u32_bytes + # => [limb4, limb3, limb2, limb1] + + # reorder from [limb4, limb3, limb2, limb1] to [limb1, limb2, limb3, limb4] + movdn.3 movdn.2 swap + # => [limb1, limb2, limb3, limb4] + + push.0 + # => [limb0=0, limb1, limb2, limb3, limb4] +end + # HELPER PROCEDURES # ================================================================================================= From e7f445a327ee31f807862272cb6292fba2fa40a3 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 17:58:12 +0200 Subject: [PATCH 02/17] Add bridge_message procedure to bridge_out.masm Adds a new public procedure that creates a leafType=1 leaf in the Local Exit Tree for outbound message bridging. Unlike bridge_out (asset transfers), this procedure does not involve faucet lookup, burn, or lock operations. The origin address is derived from the note sender via eth_address::from_account_id and the amount is always zero. --- .../asm/agglayer/bridge/bridge_out.masm | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 7db98dc04f..5ef3b498c4 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -16,6 +16,7 @@ use agglayer::common::asset_conversion use agglayer::bridge::bridge_config use agglayer::bridge::leaf_utils use agglayer::bridge::merkle_tree_frontier +use agglayer::common::eth_address use agglayer::common::eth_address::EthereumAddressFormat # ERRORS @@ -82,6 +83,7 @@ const ATTACHMENT_SCHEME_LOC=12 # ------------------------------------------------------------------------------------------------- const LEAF_TYPE_ASSET=0 +const LEAF_TYPE_MESSAGE=1 use miden::protocol::note::NOTE_TYPE_PUBLIC const BURN_NOTE_NUM_STORAGE_ITEMS=0 @@ -243,6 +245,116 @@ pub proc bridge_out end end +#! Bridges a message out via the AggLayer. +#! +#! This procedure handles the complete bridge-message operation: +#! 1. Validates the note is public and the destination is not Miden +#! 2. Builds the leaf data with leafType=1, the caller-provided metadata hash, +#! and the note sender's address as the origin token address +#! 3. Computes Keccak hash of the leaf data and appends it to the Local Exit Tree +#! +#! Unlike bridge_out, this procedure does NOT involve any faucet lookup, burn, or lock operations. +#! The amount field is always zero. +#! +#! Inputs: [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - dest_network_id is the u32 destination network/chain ID. +#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. +#! - metadata_hash(8) is the Keccak256 hash of the message metadata (8 u32 limbs). +#! +#! Panics if: +#! - the note calling `bridge_message` is not public. +#! - destination network ID is Miden's AggLayer network ID. +#! +#! Invocation: call +pub proc bridge_message + exec.active_note::is_public assert.err=ERR_B2AGG_NOTE_MUST_BE_PUBLIC + # => [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] + + dup exec.assert_destination_id_not_miden_id + # => [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] + + # --- 1. Store destination_network --- + push.LEAF_DATA_START_PTR push.DESTINATION_NETWORK_OFFSET add + mem_store + # => [dest_address(5), metadata_hash(8), pad(2)] + + # --- 2. Store destination_address (5 felts) --- + push.LEAF_DATA_START_PTR push.DESTINATION_ADDRESS_OFFSET add + exec.write_address_to_memory + # => [metadata_hash(8), pad(2)] + + # --- 3. Store metadata_hash (8 felts) --- + push.LEAF_DATA_START_PTR push.METADATA_HASH_OFFSET add + movdn.8 + exec.utils::mem_store_double_word_unaligned + # => [pad(2)] + + # Pad stack back to 16 elements for subsequent operations + padw padw push.0.0.0.0.0.0 + # => [pad(16)] + + # --- 4. Store leafType=1 (byte-swapped) --- + push.LEAF_TYPE_MESSAGE + exec.utils::swap_u32_bytes + push.LEAF_DATA_START_PTR push.LEAF_TYPE_OFFSET add + mem_store + # => [pad(16)] + + # --- 5. Store originNetwork = MIDEN_NETWORK_ID --- + push.MIDEN_NETWORK_ID + push.LEAF_DATA_START_PTR push.ORIGIN_NETWORK_OFFSET add + mem_store + # => [pad(16)] + + # --- 6. Store origin_token_address from note sender --- + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, pad(16)] + + # from_account_id expects [suffix, prefix] + swap + exec.eth_address::from_account_id + # => [limb0, limb1, limb2, limb3, limb4, pad(16)] + + push.LEAF_DATA_START_PTR push.ORIGIN_TOKEN_ADDRESS_OFFSET add + exec.write_address_to_memory + # => [pad(16)] + + # --- 7. Store amount = 0 (8 felts, all zero) --- + push.LEAF_DATA_START_PTR push.AMOUNT_OFFSET add + movdn.8 + padw padw + # => [0, 0, 0, 0, 0, 0, 0, 0, amount_ptr, pad(8)] + + exec.utils::mem_store_double_word_unaligned + # => [pad(8)] + + # Restore stack to 16 elements + padw padw + # => [pad(16)] + + # --- 8. Zero the 3 padding felts --- + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add + mem_store + + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.1 add + mem_store + + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.2 add + mem_store + # => [pad(16)] + + # --- 9. Compute leaf value and append to LET --- + push.LEAF_DATA_START_PTR + exec.add_leaf_bridge + # => [pad(16)] +end + # HELPER PROCEDURES # ================================================================================================= From 563be90afa05e62377f36af0fa5f5439f570de6e Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 17:59:10 +0200 Subject: [PATCH 03/17] feat(agglayer): re-export bridge_message from bridge component --- crates/miden-agglayer/asm/components/bridge.masm | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/miden-agglayer/asm/components/bridge.masm b/crates/miden-agglayer/asm/components/bridge.masm index 14c5169f93..6c7ff4bb15 100644 --- a/crates/miden-agglayer/asm/components/bridge.masm +++ b/crates/miden-agglayer/asm/components/bridge.masm @@ -14,3 +14,4 @@ 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 +pub use ::agglayer::bridge::bridge_out::bridge_message From 58f99e4d9edbdd89fb608619d406bad4e3c72d04 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 18:04:42 +0200 Subject: [PATCH 04/17] Add bridge_message.masm note script for outbound message bridging --- .../asm/note_scripts/bridge_message.masm | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 crates/miden-agglayer/asm/note_scripts/bridge_message.masm diff --git a/crates/miden-agglayer/asm/note_scripts/bridge_message.masm b/crates/miden-agglayer/asm/note_scripts/bridge_message.masm new file mode 100644 index 0000000000..781d452a3b --- /dev/null +++ b/crates/miden-agglayer/asm/note_scripts/bridge_message.masm @@ -0,0 +1,119 @@ +use agglayer::bridge::bridge_out +use miden::protocol::account_id +use miden::protocol::active_account +use miden::protocol::active_note +use miden::standards::attachments::network_account_target + +# CONSTANTS +# ================================================================================================= + +const BRIDGE_MSG_NOTE_STORAGE_PTR = 8 +const BRIDGE_MSG_NOTE_NUM_STORAGE_ITEMS = 14 + +# ERRORS +# ================================================================================================= +const ERR_BRIDGE_MSG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="BRIDGE_MESSAGE script expects exactly 14 note storage items" +const ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH="BRIDGE_MESSAGE note attachment target account does not match consuming account" + +# NOTE SCRIPT +# ================================================================================================= + +#! Bridge Message note script: bridges a message (no assets) from Miden to an AggLayer-connected +#! chain. +#! +#! This note can be consumed in two ways: +#! - If the consuming account is the sender (reclaim case): no-op, since there are no assets. +#! - If the consuming account is the Agglayer Bridge: the note details are hashed into a leaf and +#! appended to the Local Exit Tree. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Note storage layout (14 felts total): +#! - destination_network [0] : 1 felt +#! - destination_address [1..5] : 5 felts +#! - metadata_hash [6..13] : 8 felts +#! +#! Where: +#! - destination_network: Destination network identifier (uint32) +#! - destination_address: 20-byte address as 5 u32 felts +#! - metadata_hash: 32-byte hash as 8 u32 felts +#! +#! Note attachment is constructed from a NetworkAccountTarget standard: +#! - [0, exec_hint_tag, target_id_prefix, target_id_suffix] +#! +#! Panics if: +#! - The note does not contain exactly 14 storage items. +#! - The note attachment does not target the consuming account. +#! - The note is not public (enforced by `bridge_out`). +#! - The destination network ID equals Miden's AggLayer network ID. +@note_script +pub proc main + dropw + # => [pad(16)] + + # Check if reclaim + exec.active_account::get_id + # => [account_id_prefix, account_id_suffix, pad(16)] + + exec.active_note::get_sender + # => [sender_id_prefix, sender_id_suffix, account_id_prefix, account_id_suffix, pad(16)] + + exec.account_id::is_equal + # => [reclaim, pad(16)] + + # BRIDGE_MESSAGE note is being reclaimed; no-op since there are no assets + if.true + # Nothing to do on reclaim — message notes carry no assets. + # => [pad(16)] + else + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + + # Store note storage -> mem[8..22] + push.BRIDGE_MSG_NOTE_STORAGE_PTR exec.active_note::get_storage + # => [num_storage_items, pad(16)] + + # Validate the number of storage items + push.BRIDGE_MSG_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_BRIDGE_MSG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS + # => [pad(16)] + + # Load the 14 felts from memory in reverse order so the stack top matches the + # expected call layout: [dest_network, da(5), mh(8), pad(2)] + # + # Memory layout (BRIDGE_MSG_NOTE_STORAGE_PTR = 8): + # mem[8] = dest_network + # mem[9] = da0 + # mem[10] = da1 + # mem[11] = da2 + # mem[12] = da3 + # mem[13] = da4 + # mem[14] = mh0 + # mem[15] = mh1 + # mem[16] = mh2 + # mem[17] = mh3 + # mem[18] = mh4 + # mem[19] = mh5 + # mem[20] = mh6 + # mem[21] = mh7 + # + # Each mem_load.X pushes the value at address X onto the stack. + # Loading in reverse order so that dest_network ends up on top. + mem_load.21 mem_load.20 mem_load.19 mem_load.18 + mem_load.17 mem_load.16 mem_load.15 mem_load.14 + mem_load.13 mem_load.12 mem_load.11 mem_load.10 + mem_load.9 mem_load.8 + # => [dest_network, da(5), mh(8), pad(16)] + # Top 16 elements: [dest_network, da(5), mh(8), 0, 0] + + call.bridge_out::bridge_message + # => [pad(16), pad(14)] + + # Clean up the 14 extra elements below the call's return frame + dropw dropw dropw drop drop + # => [pad(16)] + end + # => [pad(16)] +end From 51f6d773884076d0072820374c1a40d8b1071af5 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 18:07:10 +0200 Subject: [PATCH 05/17] Add MessageNote Rust type for cross-chain message bridging Create message_note.rs following the B2AggNote pattern but for asset-less message bridging. The note carries destination_network, destination_address, and metadata_hash in its storage (14 felts total) with no assets attached. --- crates/miden-agglayer/src/lib.rs | 2 + crates/miden-agglayer/src/message_note.rs | 146 ++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 crates/miden-agglayer/src/message_note.rs diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 0826a1e777..80d0c73c7b 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -24,6 +24,7 @@ pub mod bridge; pub mod claim_note; pub mod config_note; pub mod errors; +pub mod message_note; pub mod eth_types; pub mod faucet; #[cfg(feature = "testing")] @@ -44,6 +45,7 @@ pub use claim_note::{ SmtNode, }; pub use config_note::{ConfigAggBridgeNote, ConversionMetadata}; +pub use message_note::MessageNote; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{ diff --git a/crates/miden-agglayer/src/message_note.rs b/crates/miden-agglayer/src/message_note.rs new file mode 100644 index 0000000000..57cdaf5384 --- /dev/null +++ b/crates/miden-agglayer/src/message_note.rs @@ -0,0 +1,146 @@ +//! Bridge Message note creation utilities. +//! +//! This module provides helpers for creating Bridge Message notes, +//! which are used to send cross-chain messages (without assets) via the AggLayer network. + +use alloc::vec::Vec; + +use miden_assembly::Library; +use miden_assembly::serde::Deserializable; +use miden_core::Felt; +use miden_protocol::account::AccountId; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteAttachments, + NoteRecipient, + NoteScript, + NoteScriptRoot, + NoteStorage, + NoteType, + PartialNoteMetadata, +}; +use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; +use miden_utils_sync::LazyLock; + +use crate::{EthAddress, MetadataHash}; + +// NOTE SCRIPT +// ================================================================================================ + +// Initialize the Bridge Message note script only once +static BRIDGE_MESSAGE_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/bridge_message.masl")); + let library = Library::read_from_bytes(bytes) + .expect("shipped Bridge Message script library is well-formed"); + NoteScript::from_library(&library).expect("shipped Bridge Message script is well-formed") +}); + +// MESSAGE NOTE +// ================================================================================================ + +/// Bridge Message note. +/// +/// This note is used to send a cross-chain message from Miden to another network via the AggLayer. +/// Unlike B2AGG notes, message notes carry no assets — they only convey routing information +/// (destination network, destination address) and an arbitrary metadata hash. +/// Message notes are always public. +pub struct MessageNote; + +impl MessageNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for a Bridge Message note. + /// Layout: 1 (destination_network) + 5 (destination_address) + 8 (metadata_hash) = 14 + pub const NUM_STORAGE_ITEMS: usize = 14; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the Bridge Message note script. + pub fn script() -> NoteScript { + BRIDGE_MESSAGE_SCRIPT.clone() + } + + /// Returns the Bridge Message note script root. + pub fn script_root() -> NoteScriptRoot { + BRIDGE_MESSAGE_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Creates a Bridge Message note. + /// + /// This note is used to send a cross-chain message from Miden to another network via the + /// AggLayer. When consumed by a bridge account, the message metadata is forwarded to the + /// destination network. Message notes carry no assets and are always public. + /// + /// # Parameters + /// - `destination_network`: The AggLayer-assigned network ID for the destination chain + /// - `destination_address`: The Ethereum address on the destination network + /// - `metadata_hash`: The hash of the message metadata + /// - `target_account_id`: The account ID that will consume this note (bridge account) + /// - `sender_account_id`: The account ID of the note creator + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + destination_network: u32, + destination_address: EthAddress, + metadata_hash: MetadataHash, + target_account_id: AccountId, + sender_account_id: AccountId, + rng: &mut R, + ) -> Result { + let note_storage = + build_note_storage(destination_network, destination_address, metadata_hash)?; + + let attachment = NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|error| { + NoteError::other_with_source( + "failed to create bridge message network account target", + error, + ) + })?; + let attachments = NoteAttachments::from(NoteAttachment::from(attachment)); + + let metadata = PartialNoteMetadata::new(sender_account_id, NoteType::Public); + + let recipient = NoteRecipient::new(rng.draw_word(), Self::script(), note_storage); + + let assets = NoteAssets::default(); + + Ok(Note::with_attachments(assets, metadata, recipient, attachments)) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Builds the note storage for a Bridge Message note. +/// +/// The storage layout is: +/// - 1 felt: destination_network +/// - 5 felts: destination_address (20 bytes as 5 u32 values) +/// - 8 felts: metadata_hash (32 bytes as 8 u32 values) +fn build_note_storage( + destination_network: u32, + destination_address: EthAddress, + metadata_hash: MetadataHash, +) -> Result { + let mut elements = Vec::with_capacity(14); + + let destination_network = u32::from_le_bytes(destination_network.to_be_bytes()); + elements.push(Felt::from(destination_network)); + elements.extend(destination_address.to_elements()); + elements.extend(metadata_hash.to_elements()); + + NoteStorage::new(elements) +} From d3ec4fcee258a2f54e6dfcbe6b84d933d572b8e5 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 18:08:46 +0200 Subject: [PATCH 06/17] feat(agglayer): register BRIDGE_MESSAGE in bridge allowed notes --- crates/miden-agglayer/src/bridge.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 06d1825c33..8c4bc0ce35 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -28,6 +28,7 @@ pub use crate::{ GlobalIndex, GlobalIndexError, LeafData, + MessageNote, MetadataHash, ProofData, SmtNode, @@ -114,6 +115,7 @@ static LET_NUM_LEAVES_SLOT_NAME: LazyLock = LazyLock::new(|| { /// - `register_faucet`, which registers a faucet in the bridge. /// - `update_ger`, which injects a new GER into the storage map. /// - `bridge_out`, which bridges an asset out of Miden to the destination network. +/// - `bridge_message`, which sends an arbitrary cross-chain message to the destination network. /// - `claim`, which validates a claim against the AggLayer bridge and creates a MINT note for the /// AggLayer Faucet. /// @@ -260,6 +262,7 @@ impl AggLayerBridge { B2AggNote::script_root(), ConfigAggBridgeNote::script_root(), UpdateGerNote::script_root(), + MessageNote::script_root(), ]) } From 84ada7924f40ff347b555108b471eb68a4fd6f8b Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 21:21:26 +0200 Subject: [PATCH 07/17] Add integration test for bridge_message outbound path Fix stack depth bug in bridge_message procedure: within a `call` frame, the stack floor is 16 elements, so direct consumption of input values via mem_store cannot reduce depth below 16. Restructured to save inputs to locals first, then reload above the floor for memory operations. The test verifies: - LET num_leaves increments by 1 after consuming a MessageNote - Local exit root becomes non-zero - No output notes are produced (messages don't create BURN notes) - Bridge vault remains empty (no assets involved) --- .../asm/agglayer/bridge/bridge_out.masm | 41 ++++--- .../tests/agglayer/bridge_message.rs | 104 ++++++++++++++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + 3 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 crates/miden-testing/tests/agglayer/bridge_message.rs diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 5ef3b498c4..267ce39f72 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -269,6 +269,7 @@ end #! - destination network ID is Miden's AggLayer network ID. #! #! Invocation: call +@locals(14) pub proc bridge_message exec.active_note::is_public assert.err=ERR_B2AGG_NOTE_MUST_BE_PUBLIC # => [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] @@ -276,24 +277,43 @@ pub proc bridge_message dup exec.assert_destination_id_not_miden_id # => [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] + # Save all 14 input values to locals. Within a `call` frame, the stack floor is 16, + # so `loc_store` at depth 16 pops the value into local memory while keeping depth at 16 + # (zeros fill from below). This drains the meaningful input values into locals. + loc_store.0 # dest_network + loc_store.1 # da0 + loc_store.2 # da1 + loc_store.3 # da2 + loc_store.4 # da3 + loc_store.5 # da4 + loc_store.6 # mh0 + loc_store.7 # mh1 + loc_store.8 # mh2 + loc_store.9 # mh3 + loc_store.10 # mh4 + loc_store.11 # mh5 + loc_store.12 # mh6 + loc_store.13 # mh7 + # => [pad(16)] + # --- 1. Store destination_network --- + loc_load.0 push.LEAF_DATA_START_PTR push.DESTINATION_NETWORK_OFFSET add mem_store - # => [dest_address(5), metadata_hash(8), pad(2)] + # => [pad(16)] # --- 2. Store destination_address (5 felts) --- + loc_load.5 loc_load.4 loc_load.3 loc_load.2 loc_load.1 push.LEAF_DATA_START_PTR push.DESTINATION_ADDRESS_OFFSET add exec.write_address_to_memory - # => [metadata_hash(8), pad(2)] + # => [pad(16)] # --- 3. Store metadata_hash (8 felts) --- + loc_load.13 loc_load.12 loc_load.11 loc_load.10 + loc_load.9 loc_load.8 loc_load.7 loc_load.6 push.LEAF_DATA_START_PTR push.METADATA_HASH_OFFSET add movdn.8 exec.utils::mem_store_double_word_unaligned - # => [pad(2)] - - # Pad stack back to 16 elements for subsequent operations - padw padw push.0.0.0.0.0.0 # => [pad(16)] # --- 4. Store leafType=1 (byte-swapped) --- @@ -323,16 +343,11 @@ pub proc bridge_message # => [pad(16)] # --- 7. Store amount = 0 (8 felts, all zero) --- + # Push 8 zeros and the address above the call frame floor, then store. + padw padw push.LEAF_DATA_START_PTR push.AMOUNT_OFFSET add movdn.8 - padw padw - # => [0, 0, 0, 0, 0, 0, 0, 0, amount_ptr, pad(8)] - exec.utils::mem_store_double_word_unaligned - # => [pad(8)] - - # Restore stack to 16 elements - padw padw # => [pad(16)] # --- 8. Zero the 3 padding felts --- diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs new file mode 100644 index 0000000000..447e82b936 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -0,0 +1,104 @@ +extern crate alloc; + +use miden_agglayer::{ + AggLayerBridge, EthAddress, MessageNote, MetadataHash, create_existing_bridge_account, +}; +use miden_crypto::rand::FeltRng; +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::transaction::RawOutputNote; +use miden_testing::{Auth, MockChain}; + +/// Tests that consuming a Bridge Message note against the bridge account updates the LET. +/// +/// This test exercises the bridge_message outbound path: +/// 1. Creates a bridge account (with bridge admin and GER manager) +/// 2. Creates a MessageNote with known parameters (no assets) +/// 3. Consumes the note against the bridge account +/// 4. Verifies: +/// - LET num_leaves incremented by 1 +/// - LET root is non-zero (changed from initial empty state) +/// - No output notes were created (messages don't produce BURN notes) +/// - Bridge vault is empty (no assets locked) +#[tokio::test] +async fn bridge_message_updates_let() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ADMIN ACCOUNT + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE GER MANAGER ACCOUNT + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE BRIDGE ACCOUNT + let 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 SENDER ACCOUNT (the user sending the message) + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE THE MESSAGE NOTE + let destination_network = 1u32; + let destination_address = + EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"); + let metadata_hash = MetadataHash::new([0x42u8; 32]); + + let note = MessageNote::create( + destination_network, + destination_address, + metadata_hash, + bridge_account.id(), + sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // CONSUME THE MESSAGE NOTE AGAINST THE BRIDGE ACCOUNT + let executed_tx = mock_chain + .build_tx_context(bridge_account.clone(), &[note.id()], &[])? + .build()? + .execute() + .await?; + + // VERIFY LET UPDATED + bridge_account.apply_delta(executed_tx.account_delta())?; + assert_eq!( + AggLayerBridge::read_let_num_leaves(&bridge_account), + 1, + "LET num_leaves should be 1 after consuming one message note" + ); + + let root = AggLayerBridge::read_local_exit_root(&bridge_account)?; + assert!( + root.iter().any(|f| f.as_canonical_u64() != 0), + "LET root should be non-zero after message" + ); + + // VERIFY NO OUTPUT NOTES (no BURN note for messages) + assert_eq!( + executed_tx.output_notes().num_notes(), + 0, + "Message notes should not produce any output notes" + ); + + // VERIFY BRIDGE VAULT IS EMPTY (no assets locked) + assert!( + bridge_account.vault().assets().next().is_none(), + "Bridge vault should be empty after message (no assets involved)" + ); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index d6f44ff14a..2c7b8c1e69 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,5 +1,6 @@ pub mod asset_conversion; mod bridge_in; +mod bridge_message; mod bridge_out; mod config_bridge; mod faucet_helpers; From dbe08149f0cfa2c78bc2dcc96bfd6fba340e509e Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 31 May 2026 21:34:35 +0200 Subject: [PATCH 08/17] fix(agglayer): byte-swap MIDEN_NETWORK_ID in bridge_message origin_network The origin_network field must be stored in LE byte order in the leaf data memory layout (matching how bridge_out stores it via convert_asset). Without the swap, the Keccak leaf hash would not match Solidity's computation. --- crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 267ce39f72..f36b74c30d 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -323,8 +323,9 @@ pub proc bridge_message mem_store # => [pad(16)] - # --- 5. Store originNetwork = MIDEN_NETWORK_ID --- + # --- 5. Store originNetwork = MIDEN_NETWORK_ID (byte-swapped to LE) --- push.MIDEN_NETWORK_ID + exec.utils::swap_u32_bytes push.LEAF_DATA_START_PTR push.ORIGIN_NETWORK_OFFSET add mem_store # => [pad(16)] From 8263f6f0e0f244ae73bcf4e30e642996cb61fb64 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 1 Jun 2026 12:31:55 +0200 Subject: [PATCH 09/17] fix(agglayer): remove incorrect swap before from_account_id, add reclaim + leaf hash tests - get_sender already returns [suffix, prefix], matching from_account_id's expected input - Add bridge_message_reclaim_is_noop test (passing) - Add bridge_message_leaf_hash_matches_independent_computation test (ignored pending MASM instrumentation to diagnose keccak input byte mismatch) --- .../asm/agglayer/bridge/bridge_out.masm | 5 +- .../tests/agglayer/bridge_message.rs | 207 ++++++++++++++++++ 2 files changed, 209 insertions(+), 3 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index f36b74c30d..1ca467ad8d 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -332,10 +332,9 @@ pub proc bridge_message # --- 6. Store origin_token_address from note sender --- exec.active_note::get_sender - # => [sender_prefix, sender_suffix, pad(16)] + # => [sender_suffix, sender_prefix, pad(16)] - # from_account_id expects [suffix, prefix] - swap + # from_account_id expects [suffix, prefix] — get_sender already returns this order exec.eth_address::from_account_id # => [limb0, limb1, limb2, limb3, limb4, pad(16)] diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index 447e82b936..305472fa6d 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -102,3 +102,210 @@ async fn bridge_message_updates_let() -> anyhow::Result<()> { Ok(()) } + +/// Tests that the on-chain LET root matches an independently computed Keccak256 leaf hash. +/// +/// This test: +/// 1. Creates a bridge + sender, creates and consumes a MessageNote +/// 2. Reads the on-chain leaf hash from the LET frontier storage +/// 3. Independently computes the expected Keccak256 leaf hash in Rust using the Solidity +/// `abi.encodePacked` layout (113 bytes) +/// 4. Verifies the on-chain frontier[0] feeds into the MTF to produce the on-chain root +/// 5. Compares the on-chain leaf hash against the independently computed one +#[tokio::test] +#[ignore = "leaf hash comparison deferred: requires MASM-level instrumentation to diagnose the \ + keccak input byte mismatch between Rust and MASM pack_leaf_data. The MTF consistency \ + check (frontier[0] → root) passes, confirming the MASM pipeline is internally \ + consistent. See the assertion at line ~201 for the passing MTF check."] +async fn bridge_message_leaf_hash_matches_independent_computation() -> anyhow::Result<()> { + use miden_agglayer::{EthEmbeddedAccountId, ExitRoot}; + use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; + use miden_processor::utils::packed_u32_elements_to_bytes; + use miden_protocol::{Felt, Word}; + + use super::merkle_tree_frontier::MerkleTreeFrontier32; + + 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 mut bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let destination_network = 1u32; + let destination_address = + EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"); + let metadata_hash = MetadataHash::new([0x42u8; 32]); + + let note = MessageNote::create( + destination_network, + destination_address, + metadata_hash, + bridge_account.id(), + sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // Consume the message note against the bridge account + let executed_tx = mock_chain + .build_tx_context(bridge_account.clone(), &[note.id()], &[])? + .build()? + .execute() + .await?; + + bridge_account.apply_delta(executed_tx.account_delta())?; + + // Verify LET was updated + assert_eq!( + AggLayerBridge::read_let_num_leaves(&bridge_account), + 1, + "LET num_leaves should be 1 after consuming one message note" + ); + + // Read on-chain frontier[0] (the leaf hash) from bridge storage. + // The frontier is stored as a double_word_array in a map slot. + // For index 0, keys are [0,0,0,0] (word 0) and [0,0,1,0] (word 1). + let frontier_slot = AggLayerBridge::let_frontier_slot_name(); + let key_lo = Word::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + let key_hi = Word::new([Felt::ZERO, Felt::ZERO, Felt::from(1u32), Felt::ZERO]); + + let frontier0_lo = bridge_account.storage().get_map_item(frontier_slot, key_lo)?; + let frontier0_hi = bridge_account.storage().get_map_item(frontier_slot, key_hi)?; + + let on_chain_leaf_felts: Vec = + frontier0_lo.iter().chain(frontier0_hi.iter()).copied().collect(); + let on_chain_leaf_bytes = packed_u32_elements_to_bytes(&on_chain_leaf_felts); + + // Verify the MTF is consistent: MTF(frontier[0]) should equal the on-chain root. + let on_chain_leaf_digest = + Keccak256Digest::from(<[u8; 32]>::try_from(&on_chain_leaf_bytes[..]).unwrap()); + let mut mtf = MerkleTreeFrontier32::<32>::new(); + let root_from_frontier = mtf.append_and_update_frontier(on_chain_leaf_digest); + let expected_root = ExitRoot::new(root_from_frontier.into()); + let on_chain_root = AggLayerBridge::read_local_exit_root(&bridge_account)?; + + assert_eq!( + on_chain_root, + expected_root.to_elements(), + "MTF(frontier[0]) should match on-chain root" + ); + + // Independently compute the expected leaf hash. + // + // The leaf is the Keccak256 hash of 113 bytes packed as abi.encodePacked: + // byte 0: leafType (1 byte) = 1 for messages + // bytes 1-4: originNetwork (4 bytes, BE) = 77 (MIDEN_NETWORK_ID) + // bytes 5-24: originTokenAddress (20 bytes) = sender's AccountId as Ethereum address + // bytes 25-28: destinationNetwork (4 bytes, BE) + // bytes 29-48: destinationAddress (20 bytes) + // bytes 49-80: amount (32 bytes) = all zeros for messages + // bytes 81-112: metadataHash (32 bytes) + let origin_address: EthAddress = + EthEmbeddedAccountId::from_account_id(sender.id()).to_eth_address(); + + let mut packed = Vec::with_capacity(113); + packed.push(1u8); // leafType = 1 (message) + packed.extend_from_slice(&77u32.to_be_bytes()); // originNetwork = MIDEN_NETWORK_ID + packed.extend_from_slice(origin_address.as_bytes()); // 20 bytes + packed.extend_from_slice(&destination_network.to_be_bytes()); // 4 bytes + packed.extend_from_slice(destination_address.as_bytes()); // 20 bytes + packed.extend_from_slice(&[0u8; 32]); // amount = 0 + packed.extend_from_slice(metadata_hash.as_bytes()); // 32 bytes + assert_eq!(packed.len(), 113); + + let leaf_hash: Keccak256Digest = Keccak256::hash(&packed); + + // Compare the on-chain leaf hash against the independently computed one + let expected_leaf_felts: Vec = + ExitRoot::new((*leaf_hash.as_bytes()).into()).to_elements(); + + assert_eq!( + on_chain_leaf_felts + .iter() + .map(|f| f.as_canonical_u64() as u32) + .collect::>(), + expected_leaf_felts + .iter() + .map(|f| f.as_canonical_u64() as u32) + .collect::>(), + "On-chain leaf hash should match independently computed Keccak256 leaf hash" + ); + + Ok(()) +} + +/// Tests that the reclaim path (sender consuming their own MessageNote) is a no-op. +/// +/// When the sender consumes a MessageNote themselves (instead of the bridge consuming it), +/// the note script should detect the reclaim condition and skip all bridge logic: +/// - No output notes should be created +/// - The bridge's LET should remain unchanged (num_leaves = 0) +/// - The transaction should succeed +#[tokio::test] +async fn bridge_message_reclaim_is_noop() -> anyhow::Result<()> { + 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_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let note = MessageNote::create( + 1u32, + EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"), + MetadataHash::new([0x42u8; 32]), + bridge_account.id(), + sender.id(), // sender == consuming account in reclaim + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // Sender consumes their own note (reclaim path) + let executed_tx = mock_chain + .build_tx_context(sender.id(), &[note.id()], &[])? + .build()? + .execute() + .await?; + + // No output notes + assert_eq!(executed_tx.output_notes().num_notes(), 0); + + // LET unchanged on bridge (note wasn't consumed by bridge) + assert_eq!(AggLayerBridge::read_let_num_leaves(&bridge_account), 0); + + Ok(()) +} From 9f23be1af20780d63918bb05cf3cceea07f1db3a Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 1 Jun 2026 12:40:21 +0200 Subject: [PATCH 10/17] Add bridge_message integration tests for consecutive, error, and metadata cases --- .../tests/agglayer/bridge_message.rs | 288 +++++++++++++++++- 1 file changed, 287 insertions(+), 1 deletion(-) diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index 305472fa6d..c1d57d364a 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -1,12 +1,15 @@ extern crate alloc; +use miden_agglayer::errors::{ + ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN, ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH, +}; use miden_agglayer::{ AggLayerBridge, EthAddress, MessageNote, MetadataHash, create_existing_bridge_account, }; use miden_crypto::rand::FeltRng; use miden_protocol::account::auth::AuthScheme; use miden_protocol::transaction::RawOutputNote; -use miden_testing::{Auth, MockChain}; +use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; /// Tests that consuming a Bridge Message note against the bridge account updates the LET. /// @@ -309,3 +312,286 @@ async fn bridge_message_reclaim_is_noop() -> anyhow::Result<()> { Ok(()) } + +/// Tests that consuming multiple MessageNotes consecutively updates the LET correctly. +/// +/// Sends 3 messages with different parameters and verifies: +/// - LET num_leaves increments to 1, 2, 3 +/// - LET root changes after each message (is different from the previous) +/// - No output notes after any consumption +#[tokio::test] +async fn bridge_message_consecutive() -> anyhow::Result<()> { + 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 mut bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // Create 3 MessageNotes with different destinations and metadata hashes + let destinations = [ + ( + 1u32, + "0x1111111111111111111111111111111111111111", + [0x01u8; 32], + ), + ( + 2u32, + "0x2222222222222222222222222222222222222222", + [0x02u8; 32], + ), + ( + 3u32, + "0x3333333333333333333333333333333333333333", + [0x03u8; 32], + ), + ]; + + let mut notes = Vec::with_capacity(3); + for (dest_network, dest_addr, mh_bytes) in &destinations { + let note = MessageNote::create( + *dest_network, + EthAddress::from_hex(dest_addr).expect("valid Ethereum address"), + MetadataHash::new(*mh_bytes), + bridge_account.id(), + sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + notes.push(note); + } + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let mut prev_root = AggLayerBridge::read_local_exit_root(&bridge_account)?; + + for (i, note) in notes.iter().enumerate() { + let executed_tx = mock_chain + .build_tx_context(bridge_account.clone(), &[note.id()], &[])? + .build()? + .execute() + .await?; + + // No output notes for messages + assert_eq!( + executed_tx.output_notes().num_notes(), + 0, + "Message note #{} should not produce any output notes", + i + 1 + ); + + bridge_account.apply_delta(executed_tx.account_delta())?; + + // LET num_leaves increments + assert_eq!( + AggLayerBridge::read_let_num_leaves(&bridge_account), + (i + 1) as u64, + "LET num_leaves should be {} after consuming message note #{}", + i + 1, + i + 1 + ); + + // LET root changed from previous + let current_root = AggLayerBridge::read_local_exit_root(&bridge_account)?; + assert_ne!( + current_root, prev_root, + "LET root should change after consuming message note #{}", + i + 1 + ); + prev_root = current_root; + + mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.prove_next_block()?; + } + + Ok(()) +} + +/// Tests that a MessageNote with destination_network == MIDEN_NETWORK_ID is rejected. +/// +/// The bridge_message procedure in bridge_out.masm checks that the destination network +/// is not Miden's own network ID (77) and panics with ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN. +#[tokio::test] +async fn bridge_message_rejects_destination_miden() -> anyhow::Result<()> { + 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_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // Create a MessageNote with destination_network = MIDEN_NETWORK_ID (77) + let note = MessageNote::create( + AggLayerBridge::MIDEN_NETWORK_ID, + EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"), + MetadataHash::new([0x42u8; 32]), + bridge_account.id(), + sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + let mock_chain = builder.build()?; + + let result = mock_chain + .build_tx_context(bridge_account.id(), &[note.id()], &[])? + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN); + + Ok(()) +} + +/// Tests that a non-target account cannot consume a MessageNote. +/// +/// The bridge_message note script checks that the consuming account matches the target +/// specified in the note attachment. If neither the bridge (target) nor the sender (reclaim), +/// the transaction fails with ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH. +#[tokio::test] +async fn bridge_message_non_target_cannot_consume() -> anyhow::Result<()> { + 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_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // Create a "other" account (neither bridge nor sender) + let other_account = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let note = MessageNote::create( + 1u32, + EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"), + MetadataHash::new([0x42u8; 32]), + bridge_account.id(), + sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + let mock_chain = builder.build()?; + + // Try to consume with other_account (not bridge, not sender) + let result = mock_chain + .build_tx_context(other_account.id(), &[note.id()], &[])? + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH); + + Ok(()) +} + +/// Tests that two messages with identical destinations but different metadata_hashes produce +/// different LET roots. +/// +/// This verifies that the metadata_hash is included in the leaf hash computation, so +/// distinct messages produce distinct tree states. +#[tokio::test] +async fn bridge_message_distinct_metadata_produces_distinct_roots() -> anyhow::Result<()> { + // Helper: build a fresh bridge, consume one MessageNote, return the final LET root. + async fn consume_single_message( + metadata_hash: MetadataHash, + ) -> anyhow::Result> { + 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 mut bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); + builder.add_account(bridge_account.clone())?; + + let sender = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + let note = MessageNote::create( + 1u32, + EthAddress::from_hex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .expect("valid Ethereum address"), + metadata_hash, + bridge_account.id(), + sender.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(note.clone())); + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let executed_tx = mock_chain + .build_tx_context(bridge_account.clone(), &[note.id()], &[])? + .build()? + .execute() + .await?; + + bridge_account.apply_delta(executed_tx.account_delta())?; + let root = AggLayerBridge::read_local_exit_root(&bridge_account)?; + Ok(root) + } + + let root_a = consume_single_message(MetadataHash::new([0xAAu8; 32])).await?; + let root_b = consume_single_message(MetadataHash::new([0xBBu8; 32])).await?; + + assert_ne!( + root_a, root_b, + "Different metadata_hashes should produce different LET roots" + ); + + Ok(()) +} From c5d9c6ce0e38354285cfd7c85e0457580ccdd48d Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 1 Jun 2026 13:17:42 +0200 Subject: [PATCH 11/17] fix(agglayer): correct u32split limb order in from_account_id, enable leaf hash test u32split returns [lo, hi] (lo on top), not [hi, lo]. Added swap after each u32split to get hi on top before byte-swapping. This caused the origin address limbs to be swapped within each 8-byte word, producing incorrect leaf hashes. Also adds: - test_from_account_id_matches_rust: verifies MASM matches Rust conversion - bridge_message_leaf_hash_masm_unit_test: verifies hash pipeline in isolation - Un-ignores leaf hash comparison test (now passes) --- .../asm/agglayer/common/eth_address.masm | 6 +- .../tests/agglayer/bridge_message.rs | 316 +++++++++++++++++- 2 files changed, 316 insertions(+), 6 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm index ed11a8b9cb..2be76cd22b 100644 --- a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm +++ b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm @@ -86,9 +86,10 @@ pub proc from_account_id swap # => [prefix, suffix] - u32split + u32split swap # => [hi_be, lo_be, suffix] # prefix = hi_be * 2^32 + lo_be + # (u32split returns [lo, hi]; swap corrects to [hi, lo]) # byte-swap hi_be to get limb1 (LE) exec.utils::swap_u32_bytes @@ -105,8 +106,9 @@ pub proc from_account_id movup.2 # => [suffix, limb2, limb1] - u32split + u32split swap # => [hi_be, lo_be, limb2, limb1] + # (u32split returns [lo, hi]; swap corrects to [hi, lo]) # byte-swap hi_be to get limb3 (LE) exec.utils::swap_u32_bytes diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index c1d57d364a..c5486dd0b4 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -116,10 +116,6 @@ async fn bridge_message_updates_let() -> anyhow::Result<()> { /// 4. Verifies the on-chain frontier[0] feeds into the MTF to produce the on-chain root /// 5. Compares the on-chain leaf hash against the independently computed one #[tokio::test] -#[ignore = "leaf hash comparison deferred: requires MASM-level instrumentation to diagnose the \ - keccak input byte mismatch between Rust and MASM pack_leaf_data. The MTF consistency \ - check (frontier[0] → root) passes, confirming the MASM pipeline is internally \ - consistent. See the assertion at line ~201 for the passing MTF check."] async fn bridge_message_leaf_hash_matches_independent_computation() -> anyhow::Result<()> { use miden_agglayer::{EthEmbeddedAccountId, ExitRoot}; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; @@ -529,6 +525,318 @@ async fn bridge_message_non_target_cannot_consume() -> anyhow::Result<()> { Ok(()) } +/// Standalone MASM unit test that mimics how `bridge_message` stores leaf data to memory, +/// calls `compute_leaf_value`, and compares the hash with an independently computed Keccak256. +/// +/// This bypasses the full transaction pipeline and directly exercises the MASM memory layout +/// and hash computation to diagnose leaf hash mismatches. +#[tokio::test] +async fn bridge_message_leaf_hash_masm_unit_test() -> anyhow::Result<()> { + use alloc::sync::Arc; + use alloc::vec::Vec; + + use miden_agglayer::agglayer_library; + use miden_assembly::{Assembler, DefaultSourceManager}; + use miden_core_lib::CoreLibrary; + use miden_crypto::hash::keccak::Keccak256; + use miden_processor::utils::{bytes_to_packed_u32_elements, packed_u32_elements_to_bytes}; + use miden_protocol::Felt; + + use super::test_utils::execute_program_with_default_host; + + // --- Rust: build the 113-byte packed representation --- + let origin_address_bytes = [0u8; 20]; + let destination_address_bytes = [0u8; 20]; + let metadata_hash_bytes = [0x42u8; 32]; + + let mut packed = Vec::with_capacity(113); + packed.push(1u8); // leafType = 1 (message) + packed.extend_from_slice(&77u32.to_be_bytes()); // originNetwork = MIDEN_NETWORK_ID + packed.extend_from_slice(&origin_address_bytes); // 20 bytes + packed.extend_from_slice(&1u32.to_be_bytes()); // destinationNetwork = 1 + packed.extend_from_slice(&destination_address_bytes); // 20 bytes + packed.extend_from_slice(&[0u8; 32]); // amount = 0 + packed.extend_from_slice(&metadata_hash_bytes); // 32 bytes + assert_eq!(packed.len(), 113); + + let expected_hash = Keccak256::hash(&packed); + let expected_felts: Vec = bytes_to_packed_u32_elements(expected_hash.as_bytes()); + + // --- Compute the u32 felt values for each field as bridge_message would store them --- + + // leafType = 1, byte-swapped: swap_u32_bytes(1) = 0x01000000 = 16777216 + let leaf_type_swapped: u32 = 16777216; + + // originNetwork = 77, byte-swapped: swap_u32_bytes(77) = 0x4D000000 = 1291845632 + let origin_network_swapped: u32 = 1291845632; + + // originAddress = 5 zeros (all-zero address) + // Each limb is bytes_to_packed_u32_elements([0,0,0,0]) = 0 + let origin_addr_felts = [0u32; 5]; + + // destinationNetwork = 1, byte-swapped: swap_u32_bytes(1) = 0x01000000 = 16777216 + let dest_network_swapped: u32 = 16777216; + + // destinationAddress = 5 zeros + let dest_addr_felts = [0u32; 5]; + + // amount = 8 zeros + // metadataHash: [0x42; 32] as packed u32 LE felts + // [0x42, 0x42, 0x42, 0x42] -> u32::from_le_bytes = 0x42424242 = 1111638594 + let metadata_felts: Vec = bytes_to_packed_u32_elements(&metadata_hash_bytes); + assert_eq!(metadata_felts.len(), 8); + + // --- Build MASM program --- + // Memory layout (start_ptr = 0): + // addr 0: leafType (byte-swapped) + // addr 1: originNetwork (byte-swapped) + // addr 2-6: originAddress (5 felts) + // addr 7: destinationNetwork (byte-swapped) + // addr 8-12: destinationAddress (5 felts) + // addr 13-20: amount (8 felts, all zero) + // addr 21-28: metadataHash (8 felts) + // addr 29-31: padding (3 felts, all zero) + + let mh: Vec = metadata_felts.iter().map(|f| f.as_canonical_u64()).collect(); + + let source = format!( + r#" + use miden::core::sys + use agglayer::bridge::leaf_utils + + begin + # Store leafType = 1 (byte-swapped) + push.{leaf_type_swapped} push.0 mem_store + + # Store originNetwork = 77 (byte-swapped) + push.{origin_network_swapped} push.1 mem_store + + # Store originAddress = 5 zeros + push.{oa0} push.2 mem_store + push.{oa1} push.3 mem_store + push.{oa2} push.4 mem_store + push.{oa3} push.5 mem_store + push.{oa4} push.6 mem_store + + # Store destinationNetwork = 1 (byte-swapped) + push.{dest_network_swapped} push.7 mem_store + + # Store destinationAddress = 5 zeros + push.{da0} push.8 mem_store + push.{da1} push.9 mem_store + push.{da2} push.10 mem_store + push.{da3} push.11 mem_store + push.{da4} push.12 mem_store + + # Store amount = 8 zeros + push.0 push.13 mem_store + push.0 push.14 mem_store + push.0 push.15 mem_store + push.0 push.16 mem_store + push.0 push.17 mem_store + push.0 push.18 mem_store + push.0 push.19 mem_store + push.0 push.20 mem_store + + # Store metadataHash = 8 felts + push.{mh0} push.21 mem_store + push.{mh1} push.22 mem_store + push.{mh2} push.23 mem_store + push.{mh3} push.24 mem_store + push.{mh4} push.25 mem_store + push.{mh5} push.26 mem_store + push.{mh6} push.27 mem_store + push.{mh7} push.28 mem_store + + # Store padding = 3 zeros + push.0 push.29 mem_store + push.0 push.30 mem_store + push.0 push.31 mem_store + + # Compute leaf value (start_ptr = 0) + push.0 + exec.leaf_utils::compute_leaf_value + + exec.sys::truncate_stack + end + "#, + leaf_type_swapped = leaf_type_swapped, + origin_network_swapped = origin_network_swapped, + oa0 = origin_addr_felts[0], + oa1 = origin_addr_felts[1], + oa2 = origin_addr_felts[2], + oa3 = origin_addr_felts[3], + oa4 = origin_addr_felts[4], + dest_network_swapped = dest_network_swapped, + da0 = dest_addr_felts[0], + da1 = dest_addr_felts[1], + da2 = dest_addr_felts[2], + da3 = dest_addr_felts[3], + da4 = dest_addr_felts[4], + mh0 = mh[0], + mh1 = mh[1], + mh2 = mh[2], + mh3 = mh[3], + mh4 = mh[4], + mh5 = mh[5], + mh6 = mh[6], + mh7 = mh[7], + ); + + let agglayer_lib = agglayer_library(); + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, None).await?; + + // Note: pack_leaf_data modifies memory in-place, so post-execution memory contains the + // packed result, not the original stored values. We focus on the hash comparison. + let ctx = miden_processor::ContextId::root(); + + // --- Compare hash output --- + let computed_felts: Vec = exec_output.stack[0..8].to_vec(); + let computed_u32s: Vec = computed_felts + .iter() + .map(|f| f.as_canonical_u64() as u32) + .collect(); + let expected_u32s: Vec = expected_felts + .iter() + .map(|f| f.as_canonical_u64() as u32) + .collect(); + + if computed_u32s != expected_u32s { + // Diagnostic: show both as bytes + let computed_bytes = packed_u32_elements_to_bytes(&computed_felts); + let expected_bytes = packed_u32_elements_to_bytes(&expected_felts); + eprintln!("MASM hash (felts): {:?}", computed_u32s); + eprintln!("Expected hash (felts): {:?}", expected_u32s); + eprintln!("MASM hash (bytes): {:02x?}", computed_bytes); + eprintln!("Expected hash (bytes): {:02x?}", expected_bytes); + + // Also dump the packed memory for diagnosis (after pack_leaf_data ran) + let packed_mem: Vec = (0..29u32) + .map(|addr| { + exec_output + .memory + .read_element(ctx, Felt::from(addr)) + .map(|f| f.as_canonical_u64() as u32) + .unwrap_or(0) + }) + .collect(); + eprintln!("Packed memory (29 u32 felts): {:08x?}", packed_mem); + + // Convert packed memory to bytes for comparison with expected packed bytes + let packed_mem_felts: Vec = (0..29u32) + .map(|addr| { + exec_output + .memory + .read_element(ctx, Felt::from(addr)) + .unwrap_or(Felt::ZERO) + }) + .collect(); + let packed_mem_bytes = packed_u32_elements_to_bytes(&packed_mem_felts); + eprintln!( + "Packed memory bytes (first 113): {:02x?}", + &packed_mem_bytes[..113.min(packed_mem_bytes.len())] + ); + eprintln!("Expected packed bytes: {:02x?}", &packed); + } + + assert_eq!( + computed_u32s, expected_u32s, + "MASM compute_leaf_value output should match Rust Keccak256 of packed leaf data" + ); + + Ok(()) +} + +/// MASM unit test: verifies that `eth_address::from_account_id` produces the same 5 felts +/// as the Rust `EthEmbeddedAccountId::from_account_id().to_elements()`. +/// +/// This isolates the origin-address conversion from the full bridge pipeline to diagnose +/// leaf hash mismatches. +#[tokio::test] +async fn test_from_account_id_matches_rust() -> anyhow::Result<()> { + use miden_agglayer::EthEmbeddedAccountId; + use miden_protocol::account::{AccountId, AccountIdVersion, AccountType}; + + use super::test_utils::execute_masm_script; + + // Pick a deterministic AccountId + let account_id = AccountId::dummy([42; 15], AccountIdVersion::Version1, AccountType::Public); + + // Rust: compute expected 5 felts + let expected_elements = EthEmbeddedAccountId::from_account_id(account_id).to_elements(); + assert_eq!(expected_elements.len(), 5, "EthAddress should have 5 elements"); + + // Get the raw felt values for MASM + let prefix_val = account_id.prefix().as_u64(); + let suffix_val = account_id.suffix().as_canonical_u64(); + + eprintln!("AccountId: prefix={prefix_val}, suffix={suffix_val}"); + eprintln!( + "Expected elements (u32): {:?}", + expected_elements + .iter() + .map(|f| f.as_canonical_u64() as u32) + .collect::>() + ); + + // MASM program: push prefix then suffix so stack is [suffix, prefix] + // (push.A push.B gives [B, A] on stack, B on top) + // from_account_id expects [suffix, prefix] with suffix on top + let source = format!( + r#" + use miden::core::sys + use agglayer::common::eth_address + + begin + push.{prefix_val} push.{suffix_val} + # => [suffix, prefix] (suffix on top) + exec.eth_address::from_account_id + # => [limb0, limb1, limb2, limb3, limb4] + exec.sys::truncate_stack + end + "#, + prefix_val = prefix_val, + suffix_val = suffix_val, + ); + + let exec_output = execute_masm_script(&source).await?; + + // Read the 5 felts from the stack (limb0 on top = stack[0]) + let actual: Vec = exec_output.stack[0..5] + .iter() + .map(|f| f.as_canonical_u64()) + .collect(); + + let expected: Vec = expected_elements + .iter() + .map(|f| f.as_canonical_u64()) + .collect(); + + eprintln!("MASM actual (u64): {:?}", actual); + eprintln!("Rust expected (u64): {:?}", expected); + + // Also show as u32 for easier reading + let actual_u32: Vec = actual.iter().map(|v| *v as u32).collect(); + let expected_u32: Vec = expected.iter().map(|v| *v as u32).collect(); + eprintln!("MASM actual (u32): {:?}", actual_u32); + eprintln!("Rust expected (u32): {:?}", expected_u32); + + assert_eq!( + actual, expected, + "MASM from_account_id output should match Rust EthEmbeddedAccountId::to_elements()" + ); + + Ok(()) +} + /// Tests that two messages with identical destinations but different metadata_hashes produce /// different LET roots. /// From e6593f2a50a447ef3335fc47fc19f6a9a525f4b3 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 1 Jun 2026 15:12:58 +0200 Subject: [PATCH 12/17] chore: add changelog entry, fix rustfmt and clippy issues --- CHANGELOG.md | 4 ++ crates/miden-agglayer/src/bridge.rs | 20 +----- crates/miden-agglayer/src/lib.rs | 26 ++------ crates/miden-agglayer/src/message_note.rs | 12 +--- .../tests/agglayer/bridge_message.rs | 65 +++++-------------- 5 files changed, 32 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 860d5cf257..2e181751a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v0.16.0 (TBD) +### Features + +- Added outbound message bridging (`bridge_message`) to the AggLayer bridge, enabling arbitrary cross-chain messages from Miden to Ethereum/AggLayer chains via leafType=1 leaves in the Local Exit Tree. Includes `BRIDGE_MESSAGE` note script, `MessageNote` Rust type, and `from_account_id` address conversion ([#3012](https://github.com/0xMiden/protocol/pull/3012)). + ### Changes - [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)). diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 8c4bc0ce35..a353679325 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -16,23 +16,9 @@ use thiserror::Error; use super::agglayer_bridge_component_library; use crate::claim_note::CgiChainHash; pub use crate::{ - B2AggNote, - ClaimNote, - ClaimNoteStorage, - ConfigAggBridgeNote, - EthAddress, - EthAmount, - EthAmountError, - EthEmbeddedAccountId, - ExitRoot, - GlobalIndex, - GlobalIndexError, - LeafData, - MessageNote, - MetadataHash, - ProofData, - SmtNode, - UpdateGerNote, + B2AggNote, ClaimNote, ClaimNoteStorage, ConfigAggBridgeNote, EthAddress, EthAmount, + EthAmountError, EthEmbeddedAccountId, ExitRoot, GlobalIndex, GlobalIndexError, LeafData, + MessageNote, MetadataHash, ProofData, SmtNode, UpdateGerNote, }; // CONSTANTS diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 80d0c73c7b..dc2a69e32d 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -10,11 +10,7 @@ use miden_protocol::asset::TokenSymbol; use miden_standards::account::access::{Authority, Ownable2Step}; use miden_standards::account::auth::AuthNetworkAccount; use miden_standards::account::policies::{ - BurnAllowAll, - BurnPolicyConfig, - MintPolicyConfig, - PolicyRegistration, - TokenPolicyManager, + BurnAllowAll, BurnPolicyConfig, MintPolicyConfig, PolicyRegistration, TokenPolicyManager, TransferPolicy, }; use miden_utils_sync::LazyLock; @@ -24,9 +20,9 @@ pub mod bridge; pub mod claim_note; pub mod config_note; pub mod errors; -pub mod message_note; pub mod eth_types; pub mod faucet; +pub mod message_note; #[cfg(feature = "testing")] pub mod testing; pub mod update_ger_note; @@ -35,29 +31,17 @@ pub mod utils; pub use b2agg_note::B2AggNote; pub use bridge::{AggLayerBridge, AgglayerBridgeError}; pub use claim_note::{ - CgiChainHash, - ClaimNote, - ClaimNoteStorage, - ExitRoot, - LeafData, - LeafValue, - ProofData, - SmtNode, + CgiChainHash, ClaimNote, ClaimNoteStorage, ExitRoot, LeafData, LeafValue, ProofData, SmtNode, }; pub use config_note::{ConfigAggBridgeNote, ConversionMetadata}; -pub use message_note::MessageNote; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{ - EthAddress, - EthAmount, - EthAmountError, - EthEmbeddedAccountId, - GlobalIndex, - GlobalIndexError, + EthAddress, EthAmount, EthAmountError, EthEmbeddedAccountId, GlobalIndex, GlobalIndexError, MetadataHash, }; pub use faucet::{AggLayerFaucet, AgglayerFaucetError}; +pub use message_note::MessageNote; pub use update_ger_note::UpdateGerNote; pub use utils::Keccak256Output; diff --git a/crates/miden-agglayer/src/message_note.rs b/crates/miden-agglayer/src/message_note.rs index 57cdaf5384..c06cbb4358 100644 --- a/crates/miden-agglayer/src/message_note.rs +++ b/crates/miden-agglayer/src/message_note.rs @@ -12,16 +12,8 @@ use miden_protocol::account::AccountId; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; use miden_protocol::note::{ - Note, - NoteAssets, - NoteAttachment, - NoteAttachments, - NoteRecipient, - NoteScript, - NoteScriptRoot, - NoteStorage, - NoteType, - PartialNoteMetadata, + Note, NoteAssets, NoteAttachment, NoteAttachments, NoteRecipient, NoteScript, NoteScriptRoot, + NoteStorage, NoteType, PartialNoteMetadata, }; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use miden_utils_sync::LazyLock; diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index c5486dd0b4..3167a970db 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -51,9 +51,8 @@ async fn bridge_message_updates_let() -> anyhow::Result<()> { // CREATE THE MESSAGE NOTE let destination_network = 1u32; - let destination_address = - EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") - .expect("valid Ethereum address"); + let destination_address = EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"); let metadata_hash = MetadataHash::new([0x42u8; 32]); let note = MessageNote::create( @@ -145,9 +144,8 @@ async fn bridge_message_leaf_hash_matches_independent_computation() -> anyhow::R })?; let destination_network = 1u32; - let destination_address = - EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") - .expect("valid Ethereum address"); + let destination_address = EthAddress::from_hex("0x1234567890abcdef1234567890abcdef12345678") + .expect("valid Ethereum address"); let metadata_hash = MetadataHash::new([0x42u8; 32]); let note = MessageNote::create( @@ -234,7 +232,7 @@ async fn bridge_message_leaf_hash_matches_independent_computation() -> anyhow::R // Compare the on-chain leaf hash against the independently computed one let expected_leaf_felts: Vec = - ExitRoot::new((*leaf_hash.as_bytes()).into()).to_elements(); + ExitRoot::new(*leaf_hash.as_bytes()).to_elements(); assert_eq!( on_chain_leaf_felts @@ -339,21 +337,9 @@ async fn bridge_message_consecutive() -> anyhow::Result<()> { // Create 3 MessageNotes with different destinations and metadata hashes let destinations = [ - ( - 1u32, - "0x1111111111111111111111111111111111111111", - [0x01u8; 32], - ), - ( - 2u32, - "0x2222222222222222222222222222222222222222", - [0x02u8; 32], - ), - ( - 3u32, - "0x3333333333333333333333333333333333333333", - [0x03u8; 32], - ), + (1u32, "0x1111111111111111111111111111111111111111", [0x01u8; 32]), + (2u32, "0x2222222222222222222222222222222222222222", [0x02u8; 32]), + (3u32, "0x3333333333333333333333333333333333333333", [0x03u8; 32]), ]; let mut notes = Vec::with_capacity(3); @@ -404,7 +390,8 @@ async fn bridge_message_consecutive() -> anyhow::Result<()> { // LET root changed from previous let current_root = AggLayerBridge::read_local_exit_root(&bridge_account)?; assert_ne!( - current_root, prev_root, + current_root, + prev_root, "LET root should change after consuming message note #{}", i + 1 ); @@ -700,14 +687,10 @@ async fn bridge_message_leaf_hash_masm_unit_test() -> anyhow::Result<()> { // --- Compare hash output --- let computed_felts: Vec = exec_output.stack[0..8].to_vec(); - let computed_u32s: Vec = computed_felts - .iter() - .map(|f| f.as_canonical_u64() as u32) - .collect(); - let expected_u32s: Vec = expected_felts - .iter() - .map(|f| f.as_canonical_u64() as u32) - .collect(); + let computed_u32s: Vec = + computed_felts.iter().map(|f| f.as_canonical_u64() as u32).collect(); + let expected_u32s: Vec = + expected_felts.iter().map(|f| f.as_canonical_u64() as u32).collect(); if computed_u32s != expected_u32s { // Diagnostic: show both as bytes @@ -733,10 +716,7 @@ async fn bridge_message_leaf_hash_masm_unit_test() -> anyhow::Result<()> { // Convert packed memory to bytes for comparison with expected packed bytes let packed_mem_felts: Vec = (0..29u32) .map(|addr| { - exec_output - .memory - .read_element(ctx, Felt::from(addr)) - .unwrap_or(Felt::ZERO) + exec_output.memory.read_element(ctx, Felt::from(addr)).unwrap_or(Felt::ZERO) }) .collect(); let packed_mem_bytes = packed_u32_elements_to_bytes(&packed_mem_felts); @@ -810,15 +790,9 @@ async fn test_from_account_id_matches_rust() -> anyhow::Result<()> { let exec_output = execute_masm_script(&source).await?; // Read the 5 felts from the stack (limb0 on top = stack[0]) - let actual: Vec = exec_output.stack[0..5] - .iter() - .map(|f| f.as_canonical_u64()) - .collect(); + let actual: Vec = exec_output.stack[0..5].iter().map(|f| f.as_canonical_u64()).collect(); - let expected: Vec = expected_elements - .iter() - .map(|f| f.as_canonical_u64()) - .collect(); + let expected: Vec = expected_elements.iter().map(|f| f.as_canonical_u64()).collect(); eprintln!("MASM actual (u64): {:?}", actual); eprintln!("Rust expected (u64): {:?}", expected); @@ -896,10 +870,7 @@ async fn bridge_message_distinct_metadata_produces_distinct_roots() -> anyhow::R let root_a = consume_single_message(MetadataHash::new([0xAAu8; 32])).await?; let root_b = consume_single_message(MetadataHash::new([0xBBu8; 32])).await?; - assert_ne!( - root_a, root_b, - "Different metadata_hashes should produce different LET roots" - ); + assert_ne!(root_a, root_b, "Different metadata_hashes should produce different LET roots"); Ok(()) } From 39c3b93f23cadebdb064db82f55950cc054c13c2 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 1 Jun 2026 15:21:05 +0200 Subject: [PATCH 13/17] chore: fix nightly rustfmt formatting (vertical imports, hex lowercase) --- crates/miden-agglayer/src/bridge.rs | 20 ++++++++++++++--- crates/miden-agglayer/src/lib.rs | 22 ++++++++++++++++--- crates/miden-agglayer/src/message_note.rs | 12 ++++++++-- .../tests/agglayer/bridge_message.rs | 16 +++++++++----- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index a353679325..8c4bc0ce35 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -16,9 +16,23 @@ use thiserror::Error; use super::agglayer_bridge_component_library; use crate::claim_note::CgiChainHash; pub use crate::{ - B2AggNote, ClaimNote, ClaimNoteStorage, ConfigAggBridgeNote, EthAddress, EthAmount, - EthAmountError, EthEmbeddedAccountId, ExitRoot, GlobalIndex, GlobalIndexError, LeafData, - MessageNote, MetadataHash, ProofData, SmtNode, UpdateGerNote, + B2AggNote, + ClaimNote, + ClaimNoteStorage, + ConfigAggBridgeNote, + EthAddress, + EthAmount, + EthAmountError, + EthEmbeddedAccountId, + ExitRoot, + GlobalIndex, + GlobalIndexError, + LeafData, + MessageNote, + MetadataHash, + ProofData, + SmtNode, + UpdateGerNote, }; // CONSTANTS diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index dc2a69e32d..d44a86ecec 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -10,7 +10,11 @@ use miden_protocol::asset::TokenSymbol; use miden_standards::account::access::{Authority, Ownable2Step}; use miden_standards::account::auth::AuthNetworkAccount; use miden_standards::account::policies::{ - BurnAllowAll, BurnPolicyConfig, MintPolicyConfig, PolicyRegistration, TokenPolicyManager, + BurnAllowAll, + BurnPolicyConfig, + MintPolicyConfig, + PolicyRegistration, + TokenPolicyManager, TransferPolicy, }; use miden_utils_sync::LazyLock; @@ -31,13 +35,25 @@ pub mod utils; pub use b2agg_note::B2AggNote; pub use bridge::{AggLayerBridge, AgglayerBridgeError}; pub use claim_note::{ - CgiChainHash, ClaimNote, ClaimNoteStorage, ExitRoot, LeafData, LeafValue, ProofData, SmtNode, + CgiChainHash, + ClaimNote, + ClaimNoteStorage, + ExitRoot, + LeafData, + LeafValue, + ProofData, + SmtNode, }; pub use config_note::{ConfigAggBridgeNote, ConversionMetadata}; #[cfg(any(test, feature = "testing"))] pub use eth_types::GlobalIndexExt; pub use eth_types::{ - EthAddress, EthAmount, EthAmountError, EthEmbeddedAccountId, GlobalIndex, GlobalIndexError, + EthAddress, + EthAmount, + EthAmountError, + EthEmbeddedAccountId, + GlobalIndex, + GlobalIndexError, MetadataHash, }; pub use faucet::{AggLayerFaucet, AgglayerFaucetError}; diff --git a/crates/miden-agglayer/src/message_note.rs b/crates/miden-agglayer/src/message_note.rs index c06cbb4358..57cdaf5384 100644 --- a/crates/miden-agglayer/src/message_note.rs +++ b/crates/miden-agglayer/src/message_note.rs @@ -12,8 +12,16 @@ use miden_protocol::account::AccountId; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; use miden_protocol::note::{ - Note, NoteAssets, NoteAttachment, NoteAttachments, NoteRecipient, NoteScript, NoteScriptRoot, - NoteStorage, NoteType, PartialNoteMetadata, + Note, + NoteAssets, + NoteAttachment, + NoteAttachments, + NoteRecipient, + NoteScript, + NoteScriptRoot, + NoteStorage, + NoteType, + PartialNoteMetadata, }; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use miden_utils_sync::LazyLock; diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index 3167a970db..9dd7ed5c8f 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -1,10 +1,15 @@ extern crate alloc; use miden_agglayer::errors::{ - ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN, ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH, + ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN, + ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH, }; use miden_agglayer::{ - AggLayerBridge, EthAddress, MessageNote, MetadataHash, create_existing_bridge_account, + AggLayerBridge, + EthAddress, + MessageNote, + MetadataHash, + create_existing_bridge_account, }; use miden_crypto::rand::FeltRng; use miden_protocol::account::auth::AuthScheme; @@ -231,8 +236,7 @@ async fn bridge_message_leaf_hash_matches_independent_computation() -> anyhow::R let leaf_hash: Keccak256Digest = Keccak256::hash(&packed); // Compare the on-chain leaf hash against the independently computed one - let expected_leaf_felts: Vec = - ExitRoot::new(*leaf_hash.as_bytes()).to_elements(); + let expected_leaf_felts: Vec = ExitRoot::new(*leaf_hash.as_bytes()).to_elements(); assert_eq!( on_chain_leaf_felts @@ -867,8 +871,8 @@ async fn bridge_message_distinct_metadata_produces_distinct_roots() -> anyhow::R Ok(root) } - let root_a = consume_single_message(MetadataHash::new([0xAAu8; 32])).await?; - let root_b = consume_single_message(MetadataHash::new([0xBBu8; 32])).await?; + let root_a = consume_single_message(MetadataHash::new([0xaau8; 32])).await?; + let root_b = consume_single_message(MetadataHash::new([0xbbu8; 32])).await?; assert_ne!(root_a, root_b, "Different metadata_hashes should produce different LET roots"); From 2089cb51b40c7cf11d72b382cf0d19f18480fe29 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 2 Jun 2026 14:37:10 +0200 Subject: [PATCH 14/17] refactor(agglayer): use named constants for bridge_message local memory offsets Replace raw numeric indices (loc_store.0, loc_load.13, etc.) with named BRIDGE_MSG_*_LOC constants, matching the established convention used by bridge_out, create_burn_note, and unlock_and_send procedures. --- .../asm/agglayer/bridge/bridge_out.masm | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 1ca467ad8d..aaf30bbdb9 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -74,6 +74,22 @@ const DESTINATION_ADDRESS_4_LOC=12 const DESTINATION_NETWORK_LOC=13 const BRIDGE_OUT_IS_NATIVE_LOC=14 +# bridge_message memory locals +const BRIDGE_MSG_DEST_NETWORK_LOC=0 +const BRIDGE_MSG_DEST_ADDRESS_0_LOC=1 +const BRIDGE_MSG_DEST_ADDRESS_1_LOC=2 +const BRIDGE_MSG_DEST_ADDRESS_2_LOC=3 +const BRIDGE_MSG_DEST_ADDRESS_3_LOC=4 +const BRIDGE_MSG_DEST_ADDRESS_4_LOC=5 +const BRIDGE_MSG_METADATA_HASH_0_LOC=6 +const BRIDGE_MSG_METADATA_HASH_1_LOC=7 +const BRIDGE_MSG_METADATA_HASH_2_LOC=8 +const BRIDGE_MSG_METADATA_HASH_3_LOC=9 +const BRIDGE_MSG_METADATA_HASH_4_LOC=10 +const BRIDGE_MSG_METADATA_HASH_5_LOC=11 +const BRIDGE_MSG_METADATA_HASH_6_LOC=12 +const BRIDGE_MSG_METADATA_HASH_7_LOC=13 + # create_burn_note memory locals const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 const ATTACHMENT_LOC=8 @@ -280,37 +296,47 @@ pub proc bridge_message # Save all 14 input values to locals. Within a `call` frame, the stack floor is 16, # so `loc_store` at depth 16 pops the value into local memory while keeping depth at 16 # (zeros fill from below). This drains the meaningful input values into locals. - loc_store.0 # dest_network - loc_store.1 # da0 - loc_store.2 # da1 - loc_store.3 # da2 - loc_store.4 # da3 - loc_store.5 # da4 - loc_store.6 # mh0 - loc_store.7 # mh1 - loc_store.8 # mh2 - loc_store.9 # mh3 - loc_store.10 # mh4 - loc_store.11 # mh5 - loc_store.12 # mh6 - loc_store.13 # mh7 + loc_store.BRIDGE_MSG_DEST_NETWORK_LOC + loc_store.BRIDGE_MSG_DEST_ADDRESS_0_LOC + loc_store.BRIDGE_MSG_DEST_ADDRESS_1_LOC + loc_store.BRIDGE_MSG_DEST_ADDRESS_2_LOC + loc_store.BRIDGE_MSG_DEST_ADDRESS_3_LOC + loc_store.BRIDGE_MSG_DEST_ADDRESS_4_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_0_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_1_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_2_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_3_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_4_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_5_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_6_LOC + loc_store.BRIDGE_MSG_METADATA_HASH_7_LOC # => [pad(16)] # --- 1. Store destination_network --- - loc_load.0 + loc_load.BRIDGE_MSG_DEST_NETWORK_LOC push.LEAF_DATA_START_PTR push.DESTINATION_NETWORK_OFFSET add mem_store # => [pad(16)] # --- 2. Store destination_address (5 felts) --- - loc_load.5 loc_load.4 loc_load.3 loc_load.2 loc_load.1 + loc_load.BRIDGE_MSG_DEST_ADDRESS_4_LOC + loc_load.BRIDGE_MSG_DEST_ADDRESS_3_LOC + loc_load.BRIDGE_MSG_DEST_ADDRESS_2_LOC + loc_load.BRIDGE_MSG_DEST_ADDRESS_1_LOC + loc_load.BRIDGE_MSG_DEST_ADDRESS_0_LOC push.LEAF_DATA_START_PTR push.DESTINATION_ADDRESS_OFFSET add exec.write_address_to_memory # => [pad(16)] # --- 3. Store metadata_hash (8 felts) --- - loc_load.13 loc_load.12 loc_load.11 loc_load.10 - loc_load.9 loc_load.8 loc_load.7 loc_load.6 + loc_load.BRIDGE_MSG_METADATA_HASH_7_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_6_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_5_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_4_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_3_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_2_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_1_LOC + loc_load.BRIDGE_MSG_METADATA_HASH_0_LOC push.LEAF_DATA_START_PTR push.METADATA_HASH_OFFSET add movdn.8 exec.utils::mem_store_double_word_unaligned From bb5e7c1f341904fe4810096fa96091f7739bcd76 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 2 Jun 2026 14:44:56 +0200 Subject: [PATCH 15/17] refactor(agglayer): add type annotations to from_account_id proc signature Add input (suffix: felt, prefix: felt) and return type (EthereumAddressFormat) to match the convention used by compute_leaf_value, verify_merkle_proof, and other typed public procedures in the codebase. --- crates/miden-agglayer/asm/agglayer/common/eth_address.masm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm index 2be76cd22b..f61f4e0bca 100644 --- a/crates/miden-agglayer/asm/agglayer/common/eth_address.masm +++ b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm @@ -81,7 +81,7 @@ end #! Outputs: [limb0, limb1, limb2, limb3, limb4] #! #! Invocation: exec -pub proc from_account_id +pub proc from_account_id(suffix: felt, prefix: felt) -> EthereumAddressFormat # split prefix into BE limbs swap # => [prefix, suffix] From 0c9dd6cfc74212f0d71d20832038230848441d05 Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 2 Jun 2026 15:40:11 +0200 Subject: [PATCH 16/17] fix(agglayer): use bridge_message-specific errors, fix stack comment, validate no assets - Replace reused ERR_B2AGG_* errors with ERR_BRIDGE_MSG_NOTE_MUST_BE_PUBLIC and ERR_BRIDGE_MSG_DESTINATION_NETWORK_IS_MIDEN so error messages correctly identify the BRIDGE_MESSAGE note, not B2AGG - Fix get_sender stack comment in note script: returns [suffix, prefix] not [prefix, suffix] - Add assertz on asset count to prevent silent asset loss if a BRIDGE_MESSAGE note is manually constructed with assets --- .../asm/agglayer/bridge/bridge_out.masm | 7 +++++-- .../asm/note_scripts/bridge_message.masm | 14 ++++++++++++-- .../miden-testing/tests/agglayer/bridge_message.rs | 6 +++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index aaf30bbdb9..84e768eb05 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -24,6 +24,8 @@ use agglayer::common::eth_address::EthereumAddressFormat const ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN = "B2AGG note destination network ID must not be Miden's AggLayer network ID" const ERR_B2AGG_NOTE_MUST_BE_PUBLIC = "B2AGG note must be public" +const ERR_BRIDGE_MSG_DESTINATION_NETWORK_IS_MIDEN = "BRIDGE_MESSAGE note destination network ID must not be Miden's AggLayer network ID" +const ERR_BRIDGE_MSG_NOTE_MUST_BE_PUBLIC = "BRIDGE_MESSAGE note must be public" # CONSTANTS # ================================================================================================= @@ -287,10 +289,11 @@ end #! Invocation: call @locals(14) pub proc bridge_message - exec.active_note::is_public assert.err=ERR_B2AGG_NOTE_MUST_BE_PUBLIC + exec.active_note::is_public assert.err=ERR_BRIDGE_MSG_NOTE_MUST_BE_PUBLIC # => [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] - dup exec.assert_destination_id_not_miden_id + dup exec.utils::swap_u32_bytes push.MIDEN_NETWORK_ID neq + assert.err=ERR_BRIDGE_MSG_DESTINATION_NETWORK_IS_MIDEN # => [dest_network_id, dest_address(5), metadata_hash(8), pad(2)] # Save all 14 input values to locals. Within a `call` frame, the stack floor is 16, diff --git a/crates/miden-agglayer/asm/note_scripts/bridge_message.masm b/crates/miden-agglayer/asm/note_scripts/bridge_message.masm index 781d452a3b..aa76c11927 100644 --- a/crates/miden-agglayer/asm/note_scripts/bridge_message.masm +++ b/crates/miden-agglayer/asm/note_scripts/bridge_message.masm @@ -7,6 +7,7 @@ use miden::standards::attachments::network_account_target # CONSTANTS # ================================================================================================= +const BRIDGE_MSG_NOTE_ASSETS_PTR = 0 const BRIDGE_MSG_NOTE_STORAGE_PTR = 8 const BRIDGE_MSG_NOTE_NUM_STORAGE_ITEMS = 14 @@ -14,6 +15,7 @@ const BRIDGE_MSG_NOTE_NUM_STORAGE_ITEMS = 14 # ================================================================================================= const ERR_BRIDGE_MSG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="BRIDGE_MESSAGE script expects exactly 14 note storage items" const ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH="BRIDGE_MESSAGE note attachment target account does not match consuming account" +const ERR_BRIDGE_MSG_NOTE_HAS_ASSETS="BRIDGE_MESSAGE note must not contain any assets" # NOTE SCRIPT # ================================================================================================= @@ -43,9 +45,10 @@ const ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH="BRIDGE_MESSAGE note attachment tar #! - [0, exec_hint_tag, target_id_prefix, target_id_suffix] #! #! Panics if: +#! - The note contains any assets (message notes must be asset-free). #! - The note does not contain exactly 14 storage items. #! - The note attachment does not target the consuming account. -#! - The note is not public (enforced by `bridge_out`). +#! - The note is not public (enforced by `bridge_message`). #! - The destination network ID equals Miden's AggLayer network ID. @note_script pub proc main @@ -57,7 +60,7 @@ pub proc main # => [account_id_prefix, account_id_suffix, pad(16)] exec.active_note::get_sender - # => [sender_id_prefix, sender_id_suffix, account_id_prefix, account_id_suffix, pad(16)] + # => [sender_id_suffix, sender_id_prefix, account_id_prefix, account_id_suffix, pad(16)] exec.account_id::is_equal # => [reclaim, pad(16)] @@ -72,6 +75,13 @@ pub proc main assert.err=ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH # => [pad(16)] + # Assert the note carries no assets (message notes are asset-free). + push.BRIDGE_MSG_NOTE_ASSETS_PTR exec.active_note::get_assets + # => [num_assets, pad(16)] + + assertz.err=ERR_BRIDGE_MSG_NOTE_HAS_ASSETS + # => [pad(16)] + # Store note storage -> mem[8..22] push.BRIDGE_MSG_NOTE_STORAGE_PTR exec.active_note::get_storage # => [num_storage_items, pad(16)] diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index 9dd7ed5c8f..717123e414 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -1,7 +1,7 @@ extern crate alloc; use miden_agglayer::errors::{ - ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN, + ERR_BRIDGE_MSG_DESTINATION_NETWORK_IS_MIDEN, ERR_BRIDGE_MSG_TARGET_ACCOUNT_MISMATCH, }; use miden_agglayer::{ @@ -411,7 +411,7 @@ async fn bridge_message_consecutive() -> anyhow::Result<()> { /// Tests that a MessageNote with destination_network == MIDEN_NETWORK_ID is rejected. /// /// The bridge_message procedure in bridge_out.masm checks that the destination network -/// is not Miden's own network ID (77) and panics with ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN. +/// is not Miden's own network ID (77) and panics with ERR_BRIDGE_MSG_DESTINATION_NETWORK_IS_MIDEN. #[tokio::test] async fn bridge_message_rejects_destination_miden() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -454,7 +454,7 @@ async fn bridge_message_rejects_destination_miden() -> anyhow::Result<()> { .execute() .await; - assert_transaction_executor_error!(result, ERR_B2AGG_DESTINATION_NETWORK_IS_MIDEN); + assert_transaction_executor_error!(result, ERR_BRIDGE_MSG_DESTINATION_NETWORK_IS_MIDEN); Ok(()) } From a97c5ce91347c321f99208d3c3c345c81afc127c Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 2 Jun 2026 16:04:24 +0200 Subject: [PATCH 17/17] refactor: use word loads in note script, add byte-swap doc, relocate MASM unit tests - Note script: replace 14 individual mem_load instructions with 4 idiomatic padw + mem_loadw_le word loads (matching b2agg.masm pattern) - message_note.rs: document LE byte-swap convention in build_note_storage - Move test_from_account_id_matches_rust to solidity_miden_address_conversion.rs - Move compute_leaf_value message-type test to leaf_utils.rs --- .../asm/note_scripts/bridge_message.masm | 44 +-- crates/miden-agglayer/src/message_note.rs | 6 +- .../tests/agglayer/bridge_message.rs | 299 ------------------ .../tests/agglayer/leaf_utils.rs | 129 +++++++- .../solidity_miden_address_conversion.rs | 48 +++ 5 files changed, 195 insertions(+), 331 deletions(-) diff --git a/crates/miden-agglayer/asm/note_scripts/bridge_message.masm b/crates/miden-agglayer/asm/note_scripts/bridge_message.masm index aa76c11927..bf44a76222 100644 --- a/crates/miden-agglayer/asm/note_scripts/bridge_message.masm +++ b/crates/miden-agglayer/asm/note_scripts/bridge_message.masm @@ -90,39 +90,29 @@ pub proc main push.BRIDGE_MSG_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_BRIDGE_MSG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS # => [pad(16)] - # Load the 14 felts from memory in reverse order so the stack top matches the - # expected call layout: [dest_network, da(5), mh(8), pad(2)] + # Load the 14 storage felts as 4 words in reverse order so the stack top matches + # the expected call layout: [dest_network, da(5), mh(8), pad(2)] # - # Memory layout (BRIDGE_MSG_NOTE_STORAGE_PTR = 8): - # mem[8] = dest_network - # mem[9] = da0 - # mem[10] = da1 - # mem[11] = da2 - # mem[12] = da3 - # mem[13] = da4 - # mem[14] = mh0 - # mem[15] = mh1 - # mem[16] = mh2 - # mem[17] = mh3 - # mem[18] = mh4 - # mem[19] = mh5 - # mem[20] = mh6 - # mem[21] = mh7 + # Memory layout (BRIDGE_MSG_NOTE_STORAGE_PTR = 8, 4 words): + # word at addr 8: [dest_network, da0, da1, da2] + # word at addr 12: [da3, da4, mh0, mh1] + # word at addr 16: [mh2, mh3, mh4, mh5] + # word at addr 20: [mh6, mh7, 0, 0] # - # Each mem_load.X pushes the value at address X onto the stack. - # Loading in reverse order so that dest_network ends up on top. - mem_load.21 mem_load.20 mem_load.19 mem_load.18 - mem_load.17 mem_load.16 mem_load.15 mem_load.14 - mem_load.13 mem_load.12 mem_load.11 mem_load.10 - mem_load.9 mem_load.8 - # => [dest_network, da(5), mh(8), pad(16)] + # Each `padw` + `mem_loadw_le.X` pushes 4 zeros then overwrites them with the word at X. + # Loading in reverse address order so dest_network ends up on top. + padw push.BRIDGE_MSG_NOTE_STORAGE_PTR add.12 mem_loadw_le + padw push.BRIDGE_MSG_NOTE_STORAGE_PTR add.8 mem_loadw_le + padw push.BRIDGE_MSG_NOTE_STORAGE_PTR add.4 mem_loadw_le + padw mem_loadw_le.BRIDGE_MSG_NOTE_STORAGE_PTR + # => [dest_network, da(5), mh(8), 0, 0, pad(16)] # Top 16 elements: [dest_network, da(5), mh(8), 0, 0] call.bridge_out::bridge_message - # => [pad(16), pad(14)] + # => [pad(16), pad(16)] - # Clean up the 14 extra elements below the call's return frame - dropw dropw dropw drop drop + # Clean up the 16 extra elements below the call's return frame + dropw dropw dropw dropw # => [pad(16)] end # => [pad(16)] diff --git a/crates/miden-agglayer/src/message_note.rs b/crates/miden-agglayer/src/message_note.rs index 57cdaf5384..4e367647e9 100644 --- a/crates/miden-agglayer/src/message_note.rs +++ b/crates/miden-agglayer/src/message_note.rs @@ -127,9 +127,9 @@ impl MessageNote { /// Builds the note storage for a Bridge Message note. /// /// The storage layout is: -/// - 1 felt: destination_network -/// - 5 felts: destination_address (20 bytes as 5 u32 values) -/// - 8 felts: metadata_hash (32 bytes as 8 u32 values) +/// - 1 felt: destination_network (byte-swapped to LE u32, matching the leaf data convention) +/// - 5 felts: destination_address (20 bytes as 5 LE-packed u32 values) +/// - 8 felts: metadata_hash (32 bytes as 8 LE-packed u32 values) fn build_note_storage( destination_network: u32, destination_address: EthAddress, diff --git a/crates/miden-testing/tests/agglayer/bridge_message.rs b/crates/miden-testing/tests/agglayer/bridge_message.rs index 717123e414..be6dc182c5 100644 --- a/crates/miden-testing/tests/agglayer/bridge_message.rs +++ b/crates/miden-testing/tests/agglayer/bridge_message.rs @@ -516,305 +516,6 @@ async fn bridge_message_non_target_cannot_consume() -> anyhow::Result<()> { Ok(()) } -/// Standalone MASM unit test that mimics how `bridge_message` stores leaf data to memory, -/// calls `compute_leaf_value`, and compares the hash with an independently computed Keccak256. -/// -/// This bypasses the full transaction pipeline and directly exercises the MASM memory layout -/// and hash computation to diagnose leaf hash mismatches. -#[tokio::test] -async fn bridge_message_leaf_hash_masm_unit_test() -> anyhow::Result<()> { - use alloc::sync::Arc; - use alloc::vec::Vec; - - use miden_agglayer::agglayer_library; - use miden_assembly::{Assembler, DefaultSourceManager}; - use miden_core_lib::CoreLibrary; - use miden_crypto::hash::keccak::Keccak256; - use miden_processor::utils::{bytes_to_packed_u32_elements, packed_u32_elements_to_bytes}; - use miden_protocol::Felt; - - use super::test_utils::execute_program_with_default_host; - - // --- Rust: build the 113-byte packed representation --- - let origin_address_bytes = [0u8; 20]; - let destination_address_bytes = [0u8; 20]; - let metadata_hash_bytes = [0x42u8; 32]; - - let mut packed = Vec::with_capacity(113); - packed.push(1u8); // leafType = 1 (message) - packed.extend_from_slice(&77u32.to_be_bytes()); // originNetwork = MIDEN_NETWORK_ID - packed.extend_from_slice(&origin_address_bytes); // 20 bytes - packed.extend_from_slice(&1u32.to_be_bytes()); // destinationNetwork = 1 - packed.extend_from_slice(&destination_address_bytes); // 20 bytes - packed.extend_from_slice(&[0u8; 32]); // amount = 0 - packed.extend_from_slice(&metadata_hash_bytes); // 32 bytes - assert_eq!(packed.len(), 113); - - let expected_hash = Keccak256::hash(&packed); - let expected_felts: Vec = bytes_to_packed_u32_elements(expected_hash.as_bytes()); - - // --- Compute the u32 felt values for each field as bridge_message would store them --- - - // leafType = 1, byte-swapped: swap_u32_bytes(1) = 0x01000000 = 16777216 - let leaf_type_swapped: u32 = 16777216; - - // originNetwork = 77, byte-swapped: swap_u32_bytes(77) = 0x4D000000 = 1291845632 - let origin_network_swapped: u32 = 1291845632; - - // originAddress = 5 zeros (all-zero address) - // Each limb is bytes_to_packed_u32_elements([0,0,0,0]) = 0 - let origin_addr_felts = [0u32; 5]; - - // destinationNetwork = 1, byte-swapped: swap_u32_bytes(1) = 0x01000000 = 16777216 - let dest_network_swapped: u32 = 16777216; - - // destinationAddress = 5 zeros - let dest_addr_felts = [0u32; 5]; - - // amount = 8 zeros - // metadataHash: [0x42; 32] as packed u32 LE felts - // [0x42, 0x42, 0x42, 0x42] -> u32::from_le_bytes = 0x42424242 = 1111638594 - let metadata_felts: Vec = bytes_to_packed_u32_elements(&metadata_hash_bytes); - assert_eq!(metadata_felts.len(), 8); - - // --- Build MASM program --- - // Memory layout (start_ptr = 0): - // addr 0: leafType (byte-swapped) - // addr 1: originNetwork (byte-swapped) - // addr 2-6: originAddress (5 felts) - // addr 7: destinationNetwork (byte-swapped) - // addr 8-12: destinationAddress (5 felts) - // addr 13-20: amount (8 felts, all zero) - // addr 21-28: metadataHash (8 felts) - // addr 29-31: padding (3 felts, all zero) - - let mh: Vec = metadata_felts.iter().map(|f| f.as_canonical_u64()).collect(); - - let source = format!( - r#" - use miden::core::sys - use agglayer::bridge::leaf_utils - - begin - # Store leafType = 1 (byte-swapped) - push.{leaf_type_swapped} push.0 mem_store - - # Store originNetwork = 77 (byte-swapped) - push.{origin_network_swapped} push.1 mem_store - - # Store originAddress = 5 zeros - push.{oa0} push.2 mem_store - push.{oa1} push.3 mem_store - push.{oa2} push.4 mem_store - push.{oa3} push.5 mem_store - push.{oa4} push.6 mem_store - - # Store destinationNetwork = 1 (byte-swapped) - push.{dest_network_swapped} push.7 mem_store - - # Store destinationAddress = 5 zeros - push.{da0} push.8 mem_store - push.{da1} push.9 mem_store - push.{da2} push.10 mem_store - push.{da3} push.11 mem_store - push.{da4} push.12 mem_store - - # Store amount = 8 zeros - push.0 push.13 mem_store - push.0 push.14 mem_store - push.0 push.15 mem_store - push.0 push.16 mem_store - push.0 push.17 mem_store - push.0 push.18 mem_store - push.0 push.19 mem_store - push.0 push.20 mem_store - - # Store metadataHash = 8 felts - push.{mh0} push.21 mem_store - push.{mh1} push.22 mem_store - push.{mh2} push.23 mem_store - push.{mh3} push.24 mem_store - push.{mh4} push.25 mem_store - push.{mh5} push.26 mem_store - push.{mh6} push.27 mem_store - push.{mh7} push.28 mem_store - - # Store padding = 3 zeros - push.0 push.29 mem_store - push.0 push.30 mem_store - push.0 push.31 mem_store - - # Compute leaf value (start_ptr = 0) - push.0 - exec.leaf_utils::compute_leaf_value - - exec.sys::truncate_stack - end - "#, - leaf_type_swapped = leaf_type_swapped, - origin_network_swapped = origin_network_swapped, - oa0 = origin_addr_felts[0], - oa1 = origin_addr_felts[1], - oa2 = origin_addr_felts[2], - oa3 = origin_addr_felts[3], - oa4 = origin_addr_felts[4], - dest_network_swapped = dest_network_swapped, - da0 = dest_addr_felts[0], - da1 = dest_addr_felts[1], - da2 = dest_addr_felts[2], - da3 = dest_addr_felts[3], - da4 = dest_addr_felts[4], - mh0 = mh[0], - mh1 = mh[1], - mh2 = mh[2], - mh3 = mh[3], - mh4 = mh[4], - mh5 = mh[5], - mh6 = mh[6], - mh7 = mh[7], - ); - - let agglayer_lib = agglayer_library(); - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(agglayer_lib) - .unwrap() - .assemble_program(&source) - .unwrap(); - - let exec_output = execute_program_with_default_host(program, None).await?; - - // Note: pack_leaf_data modifies memory in-place, so post-execution memory contains the - // packed result, not the original stored values. We focus on the hash comparison. - let ctx = miden_processor::ContextId::root(); - - // --- Compare hash output --- - let computed_felts: Vec = exec_output.stack[0..8].to_vec(); - let computed_u32s: Vec = - computed_felts.iter().map(|f| f.as_canonical_u64() as u32).collect(); - let expected_u32s: Vec = - expected_felts.iter().map(|f| f.as_canonical_u64() as u32).collect(); - - if computed_u32s != expected_u32s { - // Diagnostic: show both as bytes - let computed_bytes = packed_u32_elements_to_bytes(&computed_felts); - let expected_bytes = packed_u32_elements_to_bytes(&expected_felts); - eprintln!("MASM hash (felts): {:?}", computed_u32s); - eprintln!("Expected hash (felts): {:?}", expected_u32s); - eprintln!("MASM hash (bytes): {:02x?}", computed_bytes); - eprintln!("Expected hash (bytes): {:02x?}", expected_bytes); - - // Also dump the packed memory for diagnosis (after pack_leaf_data ran) - let packed_mem: Vec = (0..29u32) - .map(|addr| { - exec_output - .memory - .read_element(ctx, Felt::from(addr)) - .map(|f| f.as_canonical_u64() as u32) - .unwrap_or(0) - }) - .collect(); - eprintln!("Packed memory (29 u32 felts): {:08x?}", packed_mem); - - // Convert packed memory to bytes for comparison with expected packed bytes - let packed_mem_felts: Vec = (0..29u32) - .map(|addr| { - exec_output.memory.read_element(ctx, Felt::from(addr)).unwrap_or(Felt::ZERO) - }) - .collect(); - let packed_mem_bytes = packed_u32_elements_to_bytes(&packed_mem_felts); - eprintln!( - "Packed memory bytes (first 113): {:02x?}", - &packed_mem_bytes[..113.min(packed_mem_bytes.len())] - ); - eprintln!("Expected packed bytes: {:02x?}", &packed); - } - - assert_eq!( - computed_u32s, expected_u32s, - "MASM compute_leaf_value output should match Rust Keccak256 of packed leaf data" - ); - - Ok(()) -} - -/// MASM unit test: verifies that `eth_address::from_account_id` produces the same 5 felts -/// as the Rust `EthEmbeddedAccountId::from_account_id().to_elements()`. -/// -/// This isolates the origin-address conversion from the full bridge pipeline to diagnose -/// leaf hash mismatches. -#[tokio::test] -async fn test_from_account_id_matches_rust() -> anyhow::Result<()> { - use miden_agglayer::EthEmbeddedAccountId; - use miden_protocol::account::{AccountId, AccountIdVersion, AccountType}; - - use super::test_utils::execute_masm_script; - - // Pick a deterministic AccountId - let account_id = AccountId::dummy([42; 15], AccountIdVersion::Version1, AccountType::Public); - - // Rust: compute expected 5 felts - let expected_elements = EthEmbeddedAccountId::from_account_id(account_id).to_elements(); - assert_eq!(expected_elements.len(), 5, "EthAddress should have 5 elements"); - - // Get the raw felt values for MASM - let prefix_val = account_id.prefix().as_u64(); - let suffix_val = account_id.suffix().as_canonical_u64(); - - eprintln!("AccountId: prefix={prefix_val}, suffix={suffix_val}"); - eprintln!( - "Expected elements (u32): {:?}", - expected_elements - .iter() - .map(|f| f.as_canonical_u64() as u32) - .collect::>() - ); - - // MASM program: push prefix then suffix so stack is [suffix, prefix] - // (push.A push.B gives [B, A] on stack, B on top) - // from_account_id expects [suffix, prefix] with suffix on top - let source = format!( - r#" - use miden::core::sys - use agglayer::common::eth_address - - begin - push.{prefix_val} push.{suffix_val} - # => [suffix, prefix] (suffix on top) - exec.eth_address::from_account_id - # => [limb0, limb1, limb2, limb3, limb4] - exec.sys::truncate_stack - end - "#, - prefix_val = prefix_val, - suffix_val = suffix_val, - ); - - let exec_output = execute_masm_script(&source).await?; - - // Read the 5 felts from the stack (limb0 on top = stack[0]) - let actual: Vec = exec_output.stack[0..5].iter().map(|f| f.as_canonical_u64()).collect(); - - let expected: Vec = expected_elements.iter().map(|f| f.as_canonical_u64()).collect(); - - eprintln!("MASM actual (u64): {:?}", actual); - eprintln!("Rust expected (u64): {:?}", expected); - - // Also show as u32 for easier reading - let actual_u32: Vec = actual.iter().map(|v| *v as u32).collect(); - let expected_u32: Vec = expected.iter().map(|v| *v as u32).collect(); - eprintln!("MASM actual (u32): {:?}", actual_u32); - eprintln!("Rust expected (u32): {:?}", expected_u32); - - assert_eq!( - actual, expected, - "MASM from_account_id output should match Rust EthEmbeddedAccountId::to_elements()" - ); - - Ok(()) -} - /// Tests that two messages with identical destinations but different metadata_hashes produce /// different LET roots. /// diff --git a/crates/miden-testing/tests/agglayer/leaf_utils.rs b/crates/miden-testing/tests/agglayer/leaf_utils.rs index 87013815d8..87670c3a33 100644 --- a/crates/miden-testing/tests/agglayer/leaf_utils.rs +++ b/crates/miden-testing/tests/agglayer/leaf_utils.rs @@ -3,12 +3,13 @@ extern crate alloc; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_agglayer::{LeafValue, agglayer_library}; +use miden_agglayer::{EthAddress, LeafValue, MetadataHash, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_crypto::SequentialCommit; +use miden_crypto::hash::keccak::Keccak256; use miden_processor::advice::AdviceInputs; -use miden_processor::utils::packed_u32_elements_to_bytes; +use miden_processor::utils::{bytes_to_packed_u32_elements, packed_u32_elements_to_bytes}; use miden_protocol::{Felt, Word}; use miden_tx::utils::hex_to_bytes; @@ -195,3 +196,127 @@ async fn get_leaf_value() -> anyhow::Result<()> { assert_eq!(computed_leaf_value, expected_leaf_value); Ok(()) } + +/// Verifies that manually storing leaf data with leafType=1 (message) and calling +/// `compute_leaf_value` produces the same Keccak256 hash as the Rust-side computation. +/// +/// This isolates the MASM hash pipeline from the full bridge_message flow. +#[tokio::test] +async fn compute_leaf_value_message_type() -> anyhow::Result<()> { + let origin_network = 77u32; + let destination_network = 1u32; + let origin_address = EthAddress::new([0u8; 20]); + let destination_address = EthAddress::new([0u8; 20]); + let metadata_hash = MetadataHash::new([0x42u8; 32]); + + // Byte-swap network IDs to LE (matching MASM convention) + let leaf_type_swapped = u32::from_le_bytes(1u32.to_be_bytes()); + let origin_network_swapped = u32::from_le_bytes(origin_network.to_be_bytes()); + let dest_network_swapped = u32::from_le_bytes(destination_network.to_be_bytes()); + let origin_addr_felts = origin_address.to_elements(); + let dest_addr_felts = destination_address.to_elements(); + let mh: Vec = metadata_hash.to_elements().iter().map(|f| f.as_canonical_u64()).collect(); + + // Build expected packed bytes (113 bytes, abi.encodePacked) + let mut packed = Vec::with_capacity(113); + packed.push(1u8); + packed.extend_from_slice(&origin_network.to_be_bytes()); + packed.extend_from_slice(origin_address.as_bytes()); + packed.extend_from_slice(&destination_network.to_be_bytes()); + packed.extend_from_slice(destination_address.as_bytes()); + packed.extend_from_slice(&[0u8; 32]); + packed.extend_from_slice(metadata_hash.as_bytes()); + assert_eq!(packed.len(), 113); + + let expected_hash = Keccak256::hash(&packed); + let expected_felts: Vec = bytes_to_packed_u32_elements(expected_hash.as_bytes()); + + // Build MASM program that stores leaf data manually, then calls compute_leaf_value + let source = format!( + r#" + use miden::core::sys + use agglayer::bridge::leaf_utils + + begin + push.{leaf_type_swapped} push.0 mem_store + push.{origin_network_swapped} push.1 mem_store + push.{oa0} push.2 mem_store + push.{oa1} push.3 mem_store + push.{oa2} push.4 mem_store + push.{oa3} push.5 mem_store + push.{oa4} push.6 mem_store + push.{dest_network_swapped} push.7 mem_store + push.{da0} push.8 mem_store + push.{da1} push.9 mem_store + push.{da2} push.10 mem_store + push.{da3} push.11 mem_store + push.{da4} push.12 mem_store + push.0 push.13 mem_store + push.0 push.14 mem_store + push.0 push.15 mem_store + push.0 push.16 mem_store + push.0 push.17 mem_store + push.0 push.18 mem_store + push.0 push.19 mem_store + push.0 push.20 mem_store + push.{mh0} push.21 mem_store + push.{mh1} push.22 mem_store + push.{mh2} push.23 mem_store + push.{mh3} push.24 mem_store + push.{mh4} push.25 mem_store + push.{mh5} push.26 mem_store + push.{mh6} push.27 mem_store + push.{mh7} push.28 mem_store + push.0 push.29 mem_store + push.0 push.30 mem_store + push.0 push.31 mem_store + push.0 + exec.leaf_utils::compute_leaf_value + exec.sys::truncate_stack + end + "#, + leaf_type_swapped = leaf_type_swapped, + origin_network_swapped = origin_network_swapped, + oa0 = origin_addr_felts[0], + oa1 = origin_addr_felts[1], + oa2 = origin_addr_felts[2], + oa3 = origin_addr_felts[3], + oa4 = origin_addr_felts[4], + dest_network_swapped = dest_network_swapped, + da0 = dest_addr_felts[0], + da1 = dest_addr_felts[1], + da2 = dest_addr_felts[2], + da3 = dest_addr_felts[3], + da4 = dest_addr_felts[4], + mh0 = mh[0], + mh1 = mh[1], + mh2 = mh[2], + mh3 = mh[3], + mh4 = mh[4], + mh5 = mh[5], + mh6 = mh[6], + mh7 = mh[7], + ); + + let agglayer_lib = agglayer_library(); + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, None).await?; + let computed: Vec = + exec_output.stack[0..8].iter().map(|f| f.as_canonical_u64() as u32).collect(); + let expected: Vec = + expected_felts.iter().map(|f: &Felt| f.as_canonical_u64() as u32).collect(); + + assert_eq!( + computed, expected, + "MASM compute_leaf_value with leafType=1 should match Rust Keccak256" + ); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 5107fcbf54..e943a40466 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -176,3 +176,51 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { Ok(()) } + +/// MASM unit test: verifies that `eth_address::from_account_id` produces the same 5 felts +/// as the Rust `EthEmbeddedAccountId::from_account_id().to_elements()`. +#[tokio::test] +async fn test_from_account_id_matches_rust() -> anyhow::Result<()> { + use miden_protocol::account::{AccountIdVersion, AccountType}; + + let account_id = AccountId::dummy([42; 15], AccountIdVersion::Version1, AccountType::Public); + + let expected_elements: Vec = + EthEmbeddedAccountId::from_account_id(account_id).to_elements(); + assert_eq!(expected_elements.len(), 5); + + let prefix_val = account_id.prefix().as_u64(); + let suffix_val = account_id.suffix().as_canonical_u64(); + + let source = format!( + r#" + use miden::core::sys + use agglayer::common::eth_address + + begin + push.{prefix_val} push.{suffix_val} + exec.eth_address::from_account_id + exec.sys::truncate_stack + end + "#, + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_library()) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + let actual: Vec = exec_output.stack[0..5].iter().map(|f| f.as_canonical_u64()).collect(); + let expected: Vec = expected_elements.iter().map(|f| f.as_canonical_u64()).collect(); + + assert_eq!( + actual, expected, + "MASM from_account_id output should match Rust EthEmbeddedAccountId::to_elements()" + ); + + Ok(()) +}