Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
03eca4c
feat(agglayer): add from_account_id to eth_address.masm
Dominik1999 May 31, 2026
e7f445a
Add bridge_message procedure to bridge_out.masm
Dominik1999 May 31, 2026
563be90
feat(agglayer): re-export bridge_message from bridge component
Dominik1999 May 31, 2026
58f99e4
Add bridge_message.masm note script for outbound message bridging
Dominik1999 May 31, 2026
51f6d77
Add MessageNote Rust type for cross-chain message bridging
Dominik1999 May 31, 2026
d3ec4fc
feat(agglayer): register BRIDGE_MESSAGE in bridge allowed notes
Dominik1999 May 31, 2026
84ada79
Add integration test for bridge_message outbound path
Dominik1999 May 31, 2026
dbe0814
fix(agglayer): byte-swap MIDEN_NETWORK_ID in bridge_message origin_ne…
Dominik1999 May 31, 2026
8263f6f
fix(agglayer): remove incorrect swap before from_account_id, add recl…
Dominik1999 Jun 1, 2026
9f23be1
Add bridge_message integration tests for consecutive, error, and meta…
Dominik1999 Jun 1, 2026
c5d9c6c
fix(agglayer): correct u32split limb order in from_account_id, enable…
Dominik1999 Jun 1, 2026
e6593f2
chore: add changelog entry, fix rustfmt and clippy issues
Dominik1999 Jun 1, 2026
39c3b93
chore: fix nightly rustfmt formatting (vertical imports, hex lowercase)
Dominik1999 Jun 1, 2026
2089cb5
refactor(agglayer): use named constants for bridge_message local memo…
Dominik1999 Jun 2, 2026
bb5e7c1
refactor(agglayer): add type annotations to from_account_id proc sign…
Dominik1999 Jun 2, 2026
0c9dd6c
fix(agglayer): use bridge_message-specific errors, fix stack comment,…
Dominik1999 Jun 2, 2026
a97c5ce
refactor: use word loads in note script, add byte-swap doc, relocate …
Dominik1999 Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
156 changes: 156 additions & 0 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ 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
# =================================================================================================

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
# =================================================================================================
Expand Down Expand Up @@ -73,6 +76,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
Expand All @@ -82,6 +101,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

Expand Down Expand Up @@ -243,6 +263,142 @@ 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
@locals(14)
pub proc bridge_message
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.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,
# 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.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.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.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.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
# => [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 (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)]

# --- 6. Store origin_token_address from note sender ---
exec.active_note::get_sender
# => [sender_suffix, sender_prefix, pad(16)]

# 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)]

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 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
exec.utils::mem_store_double_word_unaligned
# => [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
# =================================================================================================

Expand Down
60 changes: 60 additions & 0 deletions crates/miden-agglayer/asm/agglayer/common/eth_address.masm
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,66 @@ 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(suffix: felt, prefix: felt) -> EthereumAddressFormat
# split prefix into BE limbs
swap
# => [prefix, suffix]

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
# => [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 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
# => [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
# =================================================================================================

Expand Down
1 change: 1 addition & 0 deletions crates/miden-agglayer/asm/components/bridge.masm
Original file line number Diff line number Diff line change
Expand Up @@ -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
119 changes: 119 additions & 0 deletions crates/miden-agglayer/asm/note_scripts/bridge_message.masm
Original file line number Diff line number Diff line change
@@ -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_ASSETS_PTR = 0
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"
const ERR_BRIDGE_MSG_NOTE_HAS_ASSETS="BRIDGE_MESSAGE note must not contain any assets"

# 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 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_message`).
#! - 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_suffix, sender_id_prefix, 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)]

# 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)]

# 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 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, 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 `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(16)]

# Clean up the 16 extra elements below the call's return frame
dropw dropw dropw dropw
# => [pad(16)]
end
# => [pad(16)]
end
Loading
Loading