Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 71 additions & 3 deletions crates/miden-agglayer/asm/bridge/bridge_config.masm
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
use miden::protocol::account_id
use miden::protocol::active_account
use miden::protocol::active_note
use miden::protocol::native_account

# ERRORS
# =================================================================================================

const ERR_SENDER_NOT_BRIDGE_ADMIN="note sender is not the bridge admin"
const ERR_SENDER_NOT_GER_MANAGER="note sender is not the global exit root manager"

# CONSTANTS
# =================================================================================================

const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry")
const BRIDGE_ADMIN_SLOT=word("miden::agglayer::bridge::admin")
const GER_MANAGER_SLOT=word("miden::agglayer::bridge::ger_manager")
const IS_REGISTERED_FLAG=1

# PUBLIC INTERFACE
Expand All @@ -17,9 +28,6 @@ const IS_REGISTERED_FLAG=1
#! The sentinel value `[1, 0, 0, 0]` distinguishes registered faucets from
#! non-existent entries (SMTs return EMPTY_WORD for missing keys).
#!
#! TODO: Currently, no sender validation is performed — anyone can register a faucet.
#! Tracked in https://github.com/0xMiden/miden-base/issues/2450
#!
#! Inputs: [faucet_id_prefix, faucet_id_suffix, pad(14)]
#! Outputs: [pad(16)]
#!
Expand All @@ -43,3 +51,63 @@ pub proc register_faucet

dropw
end

#! Asserts that the note sender matches the bridge admin stored in account storage.
#!
#! Reads the bridge admin account ID from BRIDGE_ADMIN_SLOT and compares it against
#! the sender of the currently executing note. Panics if they do not match.
#!
#! Inputs: [pad(16)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the note sender does not match the bridge admin account ID.
#!
#! Invocation: call
pub proc assert_sender_is_bridge_admin
# => [pad(16)]

push.BRIDGE_ADMIN_SLOT[0..2]
exec.active_account::get_item
# => [admin_prefix, admin_suffix, 0, 0, pad(16)]

exec.active_note::get_sender
# => [sender_prefix, sender_suffix, admin_prefix, admin_suffix, pad(18)]

exec.account_id::is_equal
assert.err=ERR_SENDER_NOT_BRIDGE_ADMIN
# => [pad(18)]

drop drop
# => [pad(16)]
end

#! Asserts that the note sender matches the global exit root manager stored in account storage.
#!
#! Reads the GER manager account ID from GER_MANAGER_SLOT and compares it against
#! the sender of the currently executing note. Panics if they do not match.
#!
#! Inputs: [pad(16)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the note sender does not match the GER manager account ID.
#!
#! Invocation: call
pub proc assert_sender_is_ger_manager
# => [pad(16)]

push.GER_MANAGER_SLOT[0..2]
exec.active_account::get_item
# => [mgr_prefix, mgr_suffix, 0, 0, pad(16)]

exec.active_note::get_sender
# => [sender_prefix, sender_suffix, mgr_prefix, mgr_suffix, pad(18)]

exec.account_id::is_equal
assert.err=ERR_SENDER_NOT_GER_MANAGER
# => [pad(18)]

drop drop
# => [pad(16)]
end
11 changes: 8 additions & 3 deletions crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at
#! Registers a faucet in the bridge's faucet registry.
#!
#! This note can only be consumed by the Agglayer Bridge account that is targeted by the note
#! attachment. Upon consumption, it registers the faucet ID from note storage in the bridge's
#! attachment, and only if the note was sent by the bridge admin.
#! Upon consumption, it registers the faucet ID from note storage in the bridge's
#! faucet registry.

#! Note: Currently, there are no sender validation checks, so anyone can register a faucet.
#!
#! Requires that the account exposes:
#! - agglayer::bridge_config::register_faucet procedure.
#! - agglayer::bridge_config::assert_sender_is_bridge_admin procedure.
#!
#! Inputs: [ARGS, pad(12)]
#! Outputs: [pad(16)]
Expand All @@ -40,6 +40,7 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at
#!
#! Panics if:
#! - The note attachment target account does not match the consuming bridge account.
#! - The note sender is not the bridge admin.
#! - The note does not contain exactly 2 storage items.
#! - The account does not expose the register_faucet procedure.
#!
Expand All @@ -52,6 +53,10 @@ begin
assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH
# => [pad(16)]

# Ensure the note sender is the bridge admin.
call.bridge_config::assert_sender_is_bridge_admin
# => [pad(16)]

# Load note storage to memory
push.STORAGE_START_PTR exec.active_note::get_storage
# => [num_storage_items, dest_ptr, pad(16)]
Expand Down
12 changes: 10 additions & 2 deletions crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use miden::agglayer::bridge_config
use miden::agglayer::bridge_in
use miden::protocol::active_note
use miden::protocol::active_account
Expand All @@ -22,10 +23,12 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe
#! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_in::update_ger function.
#!
#! This note can only be consumed by the specific agglayer bridge account whose ID is provided
#! in the note attachment (target_account_id).
#! in the note attachment (target_account_id), and only if the note was sent by the
#! global exit root manager.
#!
#! Requires that the account exposes:
#! - agglayer::bridge_in::update_ger procedure.
#! - agglayer::bridge_config::update_ger procedure.
#! - agglayer::bridge_config::assert_sender_is_ger_manager procedure.
#!
#! Inputs: [ARGS, pad(12)]
#! Outputs: [pad(16)]
Expand All @@ -36,6 +39,7 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe
#! Panics if:
#! - account does not expose update_ger procedure.
#! - target account ID does not match the consuming account ID.
#! - note sender is not the global exit root manager.
#! - number of note storage items is not exactly 8.
begin
dropw
Expand All @@ -46,6 +50,10 @@ begin
assert.err=ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH
# => [pad(16)]

# Ensure the note sender is the global exit root manager.
call.bridge_config::assert_sender_is_ger_manager
# => [pad(16)]

# proceed with the GER update logic

push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage
Expand Down
5 changes: 5 additions & 0 deletions crates/miden-agglayer/src/errors/agglayer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ pub const ERR_ROLLUP_INDEX_NON_ZERO: MasmError = MasmError::from_static_str("rol
/// Error Message: "maximum scaling factor is 18"
pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("maximum scaling factor is 18");

/// Error Message: "note sender is not the bridge admin"
pub const ERR_SENDER_NOT_BRIDGE_ADMIN: MasmError = MasmError::from_static_str("note sender is not the bridge admin");
/// Error Message: "note sender is not the global exit root manager"
pub const ERR_SENDER_NOT_GER_MANAGER: MasmError = MasmError::from_static_str("note sender is not the global exit root manager");

/// 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");

Expand Down
56 changes: 51 additions & 5 deletions crates/miden-agglayer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ pub fn local_exit_tree_library() -> Library {
agglayer_library()
}

/// Storage slot name for the bridge admin account ID.
pub const BRIDGE_ADMIN_SLOT_NAME: &str = "miden::agglayer::bridge::admin";

/// Storage slot name for the global exit root manager account ID.
pub const GER_MANAGER_SLOT_NAME: &str = "miden::agglayer::bridge::ger_manager";

/// Creates a Local Exit Tree component with the specified storage slots.
///
/// This component uses the local_exit_tree library and can be added to accounts
Expand Down Expand Up @@ -337,7 +343,18 @@ pub fn create_agglayer_faucet_component(
///
/// The bridge starts with an empty faucet registry. Faucets are registered at runtime
/// via CONFIG_AGG_BRIDGE notes that call `bridge_config::register_faucet`.
pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder {
///
/// # Parameters
/// - `seed`: The seed used to derive the account ID.
/// - `bridge_admin_id`: The account ID of the bridge admin. Only notes sent by this account are
/// allowed to update bridge configuration (e.g. register faucets).
/// - `ger_manager_id`: The account ID of the global exit root manager. Only notes sent by this
/// account are allowed to update the GER.
pub fn create_bridge_account_builder(
seed: Word,
bridge_admin_id: AccountId,
ger_manager_id: AccountId,
) -> AccountBuilder {
let ger_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger")
.expect("Bridge storage slot name should be valid");
let bridge_in_storage_slots = vec![StorageSlot::with_empty_map(ger_storage_slot_name)];
Expand All @@ -359,12 +376,33 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder {
let faucet_registry_slot_name =
StorageSlotName::new("miden::agglayer::bridge::faucet_registry")
.expect("Faucet registry storage slot name should be valid");

let bridge_admin_slot_name = StorageSlotName::new(BRIDGE_ADMIN_SLOT_NAME)
.expect("Bridge admin storage slot name should be valid");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
.expect("Bridge admin storage slot name should be valid");
.expect("bridge admin storage slot name should be valid");

This matches the style of other .expect("") error messages in miden-base. I know @PhilippGackstatter would approve this message 😉


This applies to other usages of expect()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done here #2491

let bridge_admin_word = Word::new([
Felt::ZERO,
Felt::ZERO,
bridge_admin_id.suffix(),
bridge_admin_id.prefix().as_felt(),
]);

let ger_manager_slot_name = StorageSlotName::new(GER_MANAGER_SLOT_NAME)
.expect("GER manager storage slot name should be valid");
let ger_manager_word = Word::new([
Felt::ZERO,
Felt::ZERO,
ger_manager_id.suffix(),
ger_manager_id.prefix().as_felt(),
]);

let bridge_out_storage_slots = vec![
StorageSlot::with_empty_map(let_storage_slot_name),
StorageSlot::with_value(let_root_lo_slot_name, Word::empty()),
StorageSlot::with_value(let_root_hi_slot_name, Word::empty()),
StorageSlot::with_value(let_num_leaves_slot_name, Word::empty()),
StorageSlot::with_empty_map(faucet_registry_slot_name),
StorageSlot::with_value(bridge_admin_slot_name, bridge_admin_word),
StorageSlot::with_value(ger_manager_slot_name, ger_manager_word),
];
let bridge_out_component = bridge_out_component(bridge_out_storage_slots);

Expand All @@ -380,8 +418,12 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder {
/// Creates a new bridge account with the standard configuration.
///
/// This creates a new account suitable for production use.
pub fn create_bridge_account(seed: Word) -> Account {
create_bridge_account_builder(seed)
pub fn create_bridge_account(
seed: Word,
bridge_admin_id: AccountId,
ger_manager_id: AccountId,
) -> Account {
create_bridge_account_builder(seed, bridge_admin_id, ger_manager_id)
.with_auth_component(AccountComponent::from(NoAuth))
.build()
.expect("Bridge account should be valid")
Expand All @@ -391,8 +433,12 @@ pub fn create_bridge_account(seed: Word) -> Account {
///
/// This creates an existing account suitable for testing scenarios.
#[cfg(any(feature = "testing", test))]
pub fn create_existing_bridge_account(seed: Word) -> Account {
create_bridge_account_builder(seed)
pub fn create_existing_bridge_account(
seed: Word,
bridge_admin_id: AccountId,
ger_manager_id: AccountId,
) -> Account {
create_bridge_account_builder(seed, bridge_admin_id, ger_manager_id)
.with_auth_component(AccountComponent::from(NoAuth))
.build_existing()
.expect("Bridge account should be valid")
Expand Down
13 changes: 11 additions & 2 deletions crates/miden-testing/tests/agglayer/bridge_in.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,19 @@ enum ClaimDataSource {
async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> {
let mut builder = MockChain::builder();

// CREATE BRIDGE ADMIN ACCOUNT (not used in this test, but distinct from GER manager)
// --------------------------------------------------------------------------------------------
let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?;

// CREATE GER MANAGER ACCOUNT (sends the UPDATE_GER note)
// --------------------------------------------------------------------------------------------
let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?;

// CREATE BRIDGE ACCOUNT (with bridge_out component for MMR validation)
// --------------------------------------------------------------------------------------------
let bridge_seed = builder.rng_mut().draw_word();
let bridge_account = create_existing_bridge_account(bridge_seed);
let bridge_account =
create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id());
builder.add_account(bridge_account.clone())?;

// GET CLAIM DATA FROM JSON (source depends on the test case)
Expand Down Expand Up @@ -127,7 +136,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a
// CREATE UPDATE_GER NOTE WITH GLOBAL EXIT ROOT
// --------------------------------------------------------------------------------------------
let update_ger_note =
UpdateGerNote::create(ger, sender_account.id(), bridge_account.id(), builder.rng_mut())?;
UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?;
builder.add_output_note(OutputNote::Full(update_ger_note.clone()));

// BUILD MOCK CHAIN WITH ALL ACCOUNTS
Expand Down
Loading