Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d4ffea1
feat: put target account in attachment
mmagician Jan 23, 2026
371efae
feat: extract b2agg creation to helper
mmagician Jan 23, 2026
1b044d3
lint: regen error file
mmagician Jan 23, 2026
bfe8055
feat: use NetworkAccountTarget for attachments
mmagician Jan 24, 2026
b61a1f5
fix: still need LET slot
mmagician Jan 24, 2026
38132c8
chore: test the target mismatch logic
mmagician Jan 24, 2026
0d83087
chore: apply target checks to UPDATE_GER
mmagician Jan 24, 2026
d59c8a3
Apply suggestion from @mmagician
mmagician Jan 24, 2026
55a8792
chore: add TODOs
mmagician Jan 24, 2026
ed6c582
chore: lint
mmagician Jan 24, 2026
26476ac
docs(masm): trim dangling B2AGG note description
cursoragent Jan 28, 2026
2217b38
docs(masm): clarify B2AGG attachment layout
cursoragent Jan 28, 2026
4c742ec
feat: use network account target helper
cursoragent Jan 28, 2026
bbecf8c
feat: make B2AGG notes always public
cursoragent Jan 28, 2026
0e20ba8
feat: mark B2AGG notes always consumable
cursoragent Jan 28, 2026
b262861
style: use NoteScript import for B2AGG
cursoragent Jan 28, 2026
851ecea
docs: add changelog entry for PR 2334
cursoragent Jan 28, 2026
3750112
Apply suggestions from code review
mmagician Jan 28, 2026
8857e67
chore: move helper to network_account_target
mmagician Jan 28, 2026
3b2a537
chore: note script section headers
mmagician Jan 28, 2026
6f90bd9
chore: comment simplify and wrap
mmagician Jan 28, 2026
b0041bb
Merge branch 'agglayer-fixed-2' into mmagician-bagg-check
mmagician Jan 28, 2026
9ece869
lint
mmagician Jan 28, 2026
0282672
fix: use `with_account_target` instead of `0` for `NoteTag`
mmagician Jan 28, 2026
ec8f116
chore: add missing panic to get_id
mmagician Feb 1, 2026
3c6e32f
chore: mention active note
mmagician Feb 1, 2026
185cb51
docs: how active_account_matches_target_account panics
mmagician Feb 1, 2026
a7a3809
Revert "fix: use `with_account_target` instead of `0` for `NoteTag`"
mmagician Feb 1, 2026
ad7bc84
Merge branch 'agglayer-fixed-2' into mmagician-bagg-check
mmagician Feb 1, 2026
495dc2b
feat: extract assert_is_network_account_target helper
mmagician Feb 1, 2026
f8e4fd2
feat: change is_network_account_target to return bool, not panic
mmagician Feb 1, 2026
aad47e5
feat: swap kind/scheme in signatures
mmagician Feb 1, 2026
30d1fd0
lint: regen errors
mmagician Feb 1, 2026
1b9c98b
feat: encapsulate note logic in structs
mmagician Feb 1, 2026
5a09fda
chore: rename file to bagg_note
mmagician Feb 1, 2026
457bcd9
chore: remove redundant comments
mmagician Feb 1, 2026
f9ae94d
Merge branch 'agglayer-fixed-2' into mmagician-bagg-check
mmagician Feb 1, 2026
1a5a49f
Merge branch 'agglayer-fixed-2' into mmagician-bagg-check
mmagician Feb 2, 2026
37e8392
Merge branch 'agglayer-fixed-2' into mmagician-bagg-check
mmagician Feb 2, 2026
779a1bf
Revert "feat: swap kind/scheme in signatures"
mmagician Feb 4, 2026
3603dd8
Merge branch 'agglayer-fixed-2' into mmagician-bagg-check
mmagician Feb 4, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)).
- Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)).
- Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)).
- Added B2AGG and UPDATE_GER note attachment target checks ([#2334](https://github.com/0xMiden/miden-base/pull/2334)).
- Added double-word array data structure abstraction over storage maps ([#2299](https://github.com/0xMiden/miden-base/pull/2299)).
- Implemented verification of AggLayer deposits (claims) against GER ([#2295](https://github.com/0xMiden/miden-base/pull/2295), [#2288](https://github.com/0xMiden/miden-base/pull/2288)).
- Added `SignedBlock` struct ([#2355](https://github.com/0xMiden/miden-base/pull/2235)).
Expand Down
18 changes: 15 additions & 3 deletions crates/miden-agglayer/asm/note_scripts/B2AGG.masm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use miden::agglayer::bridge_out
use miden::protocol::account_id
use miden::protocol::active_account
use miden::protocol::active_note
use miden::protocol::note
use miden::standards::attachments::network_account_target
use miden::standards::wallets::basic->basic_wallet

# CONSTANTS
Expand All @@ -12,16 +14,18 @@ const B2AGG_NOTE_NUM_STORAGE_ITEMS=6
# ERRORS
# =================================================================================================
const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS="B2AGG script requires exactly 1 note asset"

const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly 6 note storage items"
const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account does not match consuming account"

# NOTE SCRIPT
# =================================================================================================

#! Bridge-to-AggLayer (B2AGG) note script: bridges assets from Miden to an AggLayer-connected chain.
#!
#! This note can be consumed in two ways:
#! - If the consuming account is the sender (reclaim): the note's assets are added back to the consuming account.
#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note,
#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note,
#! and the note details are hashed into a leaf and appended to the Local Exit Tree.
#! global exit root (GER) merkle tree structure.
#!
#! Inputs: []
#! Outputs: []
Expand All @@ -34,10 +38,13 @@ const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly
#! - destination_address_2: bytes 8-11
#! - destination_address_3: bytes 12-15
#! - destination_address_4: bytes 16-19
#! 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 6 storage items.
#! - The note does not contain exactly 1 asset.
#! - The note attachment does not target the consuming account.
#!
begin
dropw
Expand All @@ -58,6 +65,11 @@ begin
exec.basic_wallet::add_assets_to_account
# => [pad(16)]
else
# Ensure note attachment targets the consuming bridge account.
exec.network_account_target::active_account_matches_target_account
assert.err=ERR_B2AGG_TARGET_ACCOUNT_MISMATCH
# => [pad(16)]

# Store note storage -> mem[8..14]
push.8 exec.active_note::get_storage
# => [num_storage_items, dest_ptr, pad(16)]
Expand Down
17 changes: 16 additions & 1 deletion crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use miden::agglayer::bridge_in
use miden::protocol::active_note
use miden::protocol::active_account
use miden::protocol::account_id
use miden::protocol::note
use miden::standards::attachments::network_account_target

# CONSTANTS
# =================================================================================================
Expand All @@ -10,6 +14,10 @@ const STORAGE_PTR_GER_UPPER = 4
# ERRORS
# =================================================================================================
const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "UPDATE_GER script expects exactly 8 note storage items"
const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment target account does not match consuming account"

# NOTE SCRIPT
# =================================================================================================

#! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_in::update_ger function.
#!
Expand All @@ -33,6 +41,13 @@ begin
dropw
# => [pad(16)]

# Ensure note attachment targets the consuming bridge account.
exec.network_account_target::active_account_matches_target_account
assert.err=ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH
# => [pad(16)]

# proceed with the GER update logic

push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage
# => [num_storage_items, dest_ptr, pad(16)]

Expand All @@ -49,4 +64,4 @@ begin
call.bridge_in::update_ger
# => [pad(16)]

end
end
131 changes: 131 additions & 0 deletions crates/miden-agglayer/src/b2agg_note.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Bridge Out note creation utilities.
//!
//! This module provides helpers for creating B2AGG (Bridge to AggLayer) notes,
//! which are used to bridge assets out from Miden to the AggLayer network.
use alloc::string::ToString;
use alloc::vec::Vec;

use miden_assembly::utils::Deserializable;
use miden_core::{Felt, Program, Word};
use miden_protocol::account::AccountId;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteExecutionHint,
NoteMetadata,
NoteRecipient,
NoteScript,
NoteStorage,
NoteType,
};
use miden_standards::note::NetworkAccountTarget;
use miden_utils_sync::LazyLock;

use crate::EthAddressFormat;

// NOTE SCRIPT
// ================================================================================================

// Initialize the B2AGG note script only once
static B2AGG_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/B2AGG.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed");
NoteScript::new(program)
});

// B2AGG NOTE
// ================================================================================================

/// B2AGG (Bridge to AggLayer) note.
///
/// This note is used to bridge assets from Miden to another network via the AggLayer.
/// When consumed by a bridge account, the assets are burned and a corresponding
/// claim can be made on the destination network. B2AGG notes are always public.
pub struct B2AggNote;

impl B2AggNote {
// CONSTANTS
// --------------------------------------------------------------------------------------------

/// Expected number of storage items for a B2AGG note.
pub const NUM_STORAGE_ITEMS: usize = 6;

// PUBLIC ACCESSORS
// --------------------------------------------------------------------------------------------

/// Returns the B2AGG (Bridge to AggLayer) note script.
pub fn script() -> NoteScript {
B2AGG_SCRIPT.clone()
}

/// Returns the B2AGG note script root.
pub fn script_root() -> Word {
B2AGG_SCRIPT.root()
}

// BUILDERS
// --------------------------------------------------------------------------------------------

/// Creates a B2AGG (Bridge to AggLayer) note.
///
/// This note is used to bridge assets from Miden to another network via the AggLayer.
/// When consumed by a bridge account, the assets are burned and a corresponding
/// claim can be made on the destination network. B2AGG notes are always public.
///
/// # Parameters
/// - `destination_network`: The AggLayer-assigned network ID for the destination chain
/// - `destination_address`: The Ethereum address on the destination network
/// - `assets`: The assets to bridge (must be fungible assets from a network faucet)
/// - `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<R: FeltRng>(
destination_network: u32,
destination_address: EthAddressFormat,
assets: NoteAssets,
target_account_id: AccountId,
sender_account_id: AccountId,
rng: &mut R,
) -> Result<Note, NoteError> {
let note_storage = build_note_storage(destination_network, destination_address)?;

let attachment = NoteAttachment::from(
NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always)
.map_err(|e| NoteError::other(e.to_string()))?,
);

let metadata =
NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment);

let recipient = NoteRecipient::new(rng.draw_word(), Self::script(), note_storage);

Ok(Note::new(assets, metadata, recipient))
}
}

// HELPER FUNCTIONS
// ================================================================================================

/// Builds the note storage for a B2AGG note.
///
/// The storage layout is:
/// - 1 felt: destination_network
/// - 5 felts: destination_address (20 bytes as 5 u32 values)
fn build_note_storage(
destination_network: u32,
destination_address: EthAddressFormat,
) -> Result<NoteStorage, NoteError> {
let mut elements = Vec::with_capacity(6);

elements.push(Felt::new(destination_network as u64));
elements.extend(destination_address.to_elements());

NoteStorage::new(elements)
}
4 changes: 4 additions & 0 deletions crates/miden-agglayer/src/errors/agglayer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use miden_protocol::errors::MasmError;
/// Error Message: "most-significant 4 bytes (addr4) must be zero"
pub const ERR_ADDR4_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes (addr4) must be zero");

/// Error Message: "B2AGG note attachment target account does not match consuming account"
pub const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("B2AGG note attachment target account does not match consuming account");
/// Error Message: "B2AGG script expects exactly 6 note storage items"
pub const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("B2AGG script expects exactly 6 note storage items");
/// Error Message: "B2AGG script requires exactly 1 note asset"
Expand Down Expand Up @@ -47,5 +49,7 @@ pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_st
/// Error Message: "merkle proof verification failed: provided SMT root does not match the computed root"
pub const ERR_SMT_ROOT_VERIFICATION_FAILED: MasmError = MasmError::from_static_str("merkle proof verification failed: provided SMT root does not match the computed root");

/// Error Message: "UPDATE_GER note attachment target account does not match consuming account"
pub const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("UPDATE_GER note attachment target account does not match consuming account");
/// Error Message: "UPDATE_GER script expects exactly 8 note storage items"
pub const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("UPDATE_GER script expects exactly 8 note storage items");
46 changes: 15 additions & 31 deletions crates/miden-agglayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ use miden_standards::account::auth::NoAuth;
use miden_standards::account::faucets::NetworkFungibleFaucet;
use miden_utils_sync::LazyLock;

pub mod b2agg_note;
pub mod claim_note;
pub mod errors;
pub mod eth_types;
pub mod update_ger_note;
pub mod utils;

pub use b2agg_note::B2AggNote;
pub use claim_note::{
ClaimNoteStorage,
ExitRoot,
Expand All @@ -40,22 +42,11 @@ pub use claim_note::{
create_claim_note,
};
pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError};
pub use update_ger_note::create_update_ger_note;
pub use update_ger_note::UpdateGerNote;

// AGGLAYER NOTE SCRIPTS
// ================================================================================================

// Initialize the B2AGG note script only once
static B2AGG_SCRIPT: LazyLock<Program> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/B2AGG.masb"));
Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed")
});

/// Returns the B2AGG (Bridge to AggLayer) note script.
pub fn b2agg_script() -> Program {
B2AGG_SCRIPT.clone()
}

// Initialize the CLAIM note script only once
static CLAIM_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CLAIM.masb"));
Expand All @@ -68,19 +59,6 @@ pub fn claim_script() -> NoteScript {
CLAIM_SCRIPT.clone()
}

// Initialize the UPDATE_GER note script only once
static UPDATE_GER_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/UPDATE_GER.masb"));
let program =
Program::read_from_bytes(bytes).expect("Shipped UPDATE_GER script is well-formed");
NoteScript::new(program)
});

/// Returns the UPDATE_GER note script.
pub fn update_ger_script() -> NoteScript {
UPDATE_GER_SCRIPT.clone()
}

// AGGLAYER ACCOUNT COMPONENTS
// ================================================================================================

Expand Down Expand Up @@ -268,22 +246,28 @@ pub fn create_agglayer_faucet_component(

/// Creates a complete bridge account builder with the standard configuration.
pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder {
// Create the "bridge_in" component
let ger_upper_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_upper")
.expect("Bridge storage slot name should be valid");
let ger_lower_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_lower")
.expect("Bridge storage slot name should be valid");
let bridge_storage_slots = vec![
let bridge_in_storage_slots = vec![
StorageSlot::with_value(ger_upper_storage_slot_name, Word::empty()),
StorageSlot::with_value(ger_lower_storage_slot_name, Word::empty()),
];

let bridge_in_comp = bridge_in_component(bridge_storage_slots);
let bridge_out_comp = bridge_out_component(vec![]);
let bridge_in_component = bridge_in_component(bridge_in_storage_slots);

// Create the "bridge_out" component
let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap();
let bridge_out_storage_slots = vec![StorageSlot::with_empty_map(let_storage_slot_name)];
let bridge_out_component = bridge_out_component(bridge_out_storage_slots);

// Combine the components into a single account(builder)
Account::builder(seed.into())
.storage_mode(AccountStorageMode::Public)
.with_component(bridge_in_comp)
.with_component(bridge_out_comp)
.storage_mode(AccountStorageMode::Network)
.with_component(bridge_out_component)
.with_component(bridge_in_component)
}

/// Creates a new bridge account with the standard configuration.
Expand Down
Loading