Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changes

- [BREAKING] Replaced the AggLayer bridge's hard-coded admin/injector/remover account-ID authorization with an RBAC access-control stack (`Ownable2Step` + `RoleBasedAccessControl` + `Authority`); `create_bridge_account` now takes `(seed, owner, Vec<BridgeRoleMember>)`, `AggLayerBridge::new()` is now stateless, and `AccessControl::Rbac` gains a `members` field for seeding initial role holders ([#3130](https://github.com/0xMiden/protocol/pull/3130)).
- Added a skeleton batch kernel ([#1122](https://github.com/0xMiden/protocol/issues/1122)) wired through `LocalBatchProver::prove` and attached to `ProvenBatch` as an `ExecutionProof`. It does not yet perform any verification.
- [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)).
- [BREAKING] Replaced the per-tree account and nullifier backend traits with shared `SmtBackend` and `SmtBackendReader` traits, split into read-only and read-write capabilities, enabling read-only `LargeSmt`-backed tree views via `reader()` ([#2755](https://github.com/0xMiden/protocol/pull/2755), [#3009](https://github.com/0xMiden/protocol/pull/3009)).
Expand Down
37 changes: 29 additions & 8 deletions bin/bench-transaction/src/context_setups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use miden_agglayer::testing::ClaimDataSource;
use miden_agglayer::{
AggLayerBridge,
B2AggNote,
BridgeRoleMember,
ClaimNote,
ClaimNoteStorage,
ConfigAggBridgeNote,
Expand All @@ -14,7 +15,7 @@ use miden_agglayer::{
create_existing_bridge_account,
};
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{Account, StorageMapKey};
use miden_protocol::account::{Account, AccountId, AccountIdVersion, AccountType, StorageMapKey};
use miden_protocol::asset::{Asset, FungibleAsset};
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::note::{NoteAssets, NoteType};
Expand All @@ -26,6 +27,30 @@ use miden_standards::note::StandardNote;
use miden_testing::{Auth, MockChain, TransactionContext};
use rand::Rng;

// BRIDGE ACCOUNT HELPER
// ================================================================================================

/// Builds an existing bridge account seeded with the three operational roles (`FAUCET_ADMIN`,
/// `GER_INJECTOR`, `GER_REMOVER`) and a fixed dummy governance owner. Benchmark accounts do not
/// exercise the owner's role-management powers.
fn bench_bridge_account(
seed: Word,
faucet_admin: AccountId,
ger_injector: AccountId,
ger_remover: AccountId,
) -> Account {
let owner = AccountId::dummy([0xee; 15], AccountIdVersion::Version1, AccountType::Public);
create_existing_bridge_account(
seed,
owner,
vec![
BridgeRoleMember::FaucetAdmin(faucet_admin),
BridgeRoleMember::GerInjector(ger_injector),
BridgeRoleMember::GerRemover(ger_remover),
],
)
}

// P2ID NOTE SETUPS
// ================================================================================================

Expand Down Expand Up @@ -194,12 +219,8 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result<Trans

// CREATE BRIDGE ACCOUNT
let bridge_seed = builder.rng_mut().draw_word();
let bridge_account = create_existing_bridge_account(
bridge_seed,
bridge_admin.id(),
ger_injector.id(),
ger_remover.id(),
);
let bridge_account =
bench_bridge_account(bridge_seed, bridge_admin.id(), ger_injector.id(), ger_remover.id());
builder.add_account(bridge_account.clone())?;

// GET CLAIM DATA FROM JSON
Expand Down Expand Up @@ -412,7 +433,7 @@ pub async fn tx_consume_b2agg_note(pre_populate_leaves: Option<u32>) -> Result<T
})?;

// CREATE BRIDGE ACCOUNT
let mut bridge_account = create_existing_bridge_account(
let mut bridge_account = bench_bridge_account(
builder.rng_mut().draw_word(),
bridge_admin.id(),
ger_injector.id(),
Expand Down
121 changes: 69 additions & 52 deletions crates/miden-agglayer/SPEC.md

Large diffs are not rendered by default.

113 changes: 15 additions & 98 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use miden::core::crypto::hashes::poseidon2
use miden::core::word
use miden::protocol::account_id
use miden::protocol::active_account
use miden::protocol::active_note
use miden::protocol::native_account
use miden::standards::access::authority
use agglayer::common::utils

# ERRORS
Expand All @@ -14,17 +14,16 @@ const ERR_GER_NOT_FOUND = "GER not found in storage"
const ERR_GER_ALREADY_REGISTERED = "GER is already registered in storage"
const ERR_FAUCET_NOT_REGISTERED = "faucet is not registered in the bridge's faucet registry"
const ERR_TOKEN_NOT_REGISTERED = "(origin token address, origin network) pair is not registered in the bridge's token registry"
const ERR_SENDER_NOT_BRIDGE_ADMIN = "note sender is not the bridge admin"
const ERR_SENDER_NOT_GER_INJECTOR = "note sender is not the global exit root injector"
const ERR_SENDER_NOT_GER_REMOVER = "note sender is not the global exit root remover"

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

# Storage slots
const BRIDGE_ADMIN_SLOT = word("agglayer::bridge::admin_account_id")
const GER_INJECTOR_SLOT = word("agglayer::bridge::ger_injector_account_id")
const GER_REMOVER_SLOT = word("agglayer::bridge::ger_remover_account_id")
# Storage slots.
#
# The privileged roles (faucet admin, GER injector, GER remover) are no longer stored as plain
# account IDs here. Authorization for the role-gated procedures below is delegated to the account's
# `Authority` component (RBAC-controlled), which maps each procedure root to the role required to
# invoke it. See `authority::assert_authorized`.
const GER_MAP_STORAGE_SLOT = word("agglayer::bridge::ger_map")
const FAUCET_REGISTRY_MAP_SLOT = word("agglayer::bridge::faucet_registry_map")
const TOKEN_REGISTRY_MAP_SLOT = word("agglayer::bridge::token_registry_map")
Expand Down Expand Up @@ -79,8 +78,8 @@ const FAUCET_METADATA_SUBKEY_HASH_HI = 3 # METADATA_HASH_HI[4]
#!
#! Invocation: call
pub proc update_ger
# assert the note sender is the global exit root injector.
exec.assert_sender_is_ger_injector
# assert the note sender is authorized for this procedure (holds the GER injector role).
exec.authority::assert_authorized
# => [GER_LOWER[4], GER_UPPER[4], pad(8)]

# compute hash(GER) = poseidon2::merge(GER_LOWER, GER_UPPER)
Expand Down Expand Up @@ -122,8 +121,8 @@ end
#!
#! Invocation: call
pub proc remove_ger
# assert the note sender is the global exit root remover.
exec.assert_sender_is_ger_remover
# assert the note sender is authorized for this procedure (holds the GER remover role).
exec.authority::assert_authorized
# => [GER_LOWER[4], GER_UPPER[4], pad(8)]

# duplicate the GER (16 felts) so we can use one copy to compute the map key
Expand Down Expand Up @@ -211,7 +210,8 @@ end
#! Invocation: call
@locals(14)
pub proc register_faucet
exec.assert_sender_is_bridge_admin
# assert the note sender is authorized for this procedure (holds the faucet admin role).
exec.authority::assert_authorized
# => [addr0, addr1, addr2, addr3, addr4, faucet_id_suffix, faucet_id_prefix, scale, origin_network, is_native, pad(6)]

# Save non-address data to locals.
Expand Down Expand Up @@ -324,7 +324,8 @@ end
#!
#! Invocation: call
pub proc store_faucet_metadata_hash
exec.assert_sender_is_bridge_admin
# assert the note sender is authorized for this procedure (holds the faucet admin role).
exec.authority::assert_authorized
# => [faucet_id_suffix, faucet_id_prefix, MH_LO, MH_HI, pad(6)]

# --- Store METADATA_HASH_LO at key [SUBKEY_HASH_LO, 0, faucet_id_suffix, faucet_id_prefix] ---
Expand Down Expand Up @@ -553,90 +554,6 @@ proc hash_token_address
# => [TOKEN_ADDR_HASH]
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.
#!
#! Inputs: [pad(16)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the note sender does not match the bridge admin account ID.
#!
#! Invocation: exec
proc assert_sender_is_bridge_admin
push.BRIDGE_ADMIN_SLOT[0..2]
exec.active_account::get_item
# => [0, 0, admin_suffix, admin_prefix, pad(16)]

drop drop
# => [admin_suffix, admin_prefix, pad(16)]

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

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

#! Asserts that the note sender matches the global exit root injector stored in account storage.
#!
#! Reads the GER injector account ID from GER_INJECTOR_SLOT and compares it against the sender of the
#! currently executing note.
#!
#! Inputs: [pad(16)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the note sender does not match the GER injector account ID.
#!
#! Invocation: exec
proc assert_sender_is_ger_injector
push.GER_INJECTOR_SLOT[0..2]
exec.active_account::get_item
# => [0, 0, mgr_suffix, mgr_prefix, pad(16)]

drop drop
# => [mgr_suffix, mgr_prefix, pad(16)]

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

exec.account_id::is_equal
assert.err=ERR_SENDER_NOT_GER_INJECTOR
# => [pad(16)]
end

#! Asserts that the note sender matches the global exit root remover stored in account storage.
#!
#! Reads the GER remover account ID from GER_REMOVER_SLOT and compares it against the sender of the
#! currently executing note.
#!
#! Inputs: [pad(16)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the note sender does not match the GER remover account ID.
#!
#! Invocation: exec
proc assert_sender_is_ger_remover
push.GER_REMOVER_SLOT[0..2]
exec.active_account::get_item
# => [0, 0, rem_suffix, rem_prefix, pad(16)]

drop drop
# => [rem_suffix, rem_prefix, pad(16)]

exec.active_note::get_sender
# => [sender_suffix, sender_prefix, rem_suffix, rem_prefix, pad(16)]

exec.account_id::is_equal
assert.err=ERR_SENDER_NOT_GER_REMOVER
# => [pad(16)]
end

#! Updates the removed-GER keccak256 hash chain by folding in the provided GER.
#!
#! Computes NEW_CHAIN = keccak256::merge(OLD_CHAIN, GER), then writes the new chain to the
Expand Down
27 changes: 19 additions & 8 deletions crates/miden-agglayer/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::{BTreeSet, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::env;
use std::fmt::Write;
use std::path::Path;
Expand All @@ -12,7 +12,7 @@ use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest};
use miden_protocol::account::{AccountCode, AccountComponent, AccountComponentMetadata};
use miden_protocol::note::NoteScriptRoot;
use miden_protocol::transaction::TransactionKernel;
use miden_standards::account::access::Authority;
use miden_standards::account::access::{AccessControl, Authority};
use miden_standards::account::auth::AuthNetworkAccount;
use miden_standards::account::policies::{
BurnPolicy,
Expand Down Expand Up @@ -328,14 +328,25 @@ fn generate_agglayer_constants(
let placeholder_allowlist = BTreeSet::from([NoteScriptRoot::from_raw(Word::default())]);
let auth_component = AuthNetworkAccount::with_allowed_notes(placeholder_allowlist)
.expect("placeholder allowlist is non-empty");
// Use a dummy owner for commitment computation - the actual owner is set at runtime. Only
// the component code (not storage) contributes to the code commitment.
let dummy_owner = miden_protocol::account::AccountId::try_from(
miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
)
.unwrap();

let mut components: Vec<AccountComponent> =
vec![AccountComponent::from(auth_component), agglayer_component];
if lib_name == "faucet" {
// Use a dummy owner for commitment computation - the actual owner is set at runtime
let dummy_owner = miden_protocol::account::AccountId::try_from(
miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
)
.unwrap();
if lib_name == "bridge" {
// The bridge installs the RBAC access-control stack (Ownable2Step + RBAC +
// Authority::RbacControlled), matching `create_bridge_account_builder` in lib.rs. Empty
// role config / members suffice here since only component code affects the commitment.
components.extend(AccessControl::Rbac {
owner: dummy_owner,
roles: BTreeMap::new(),
members: Vec::new(),
});
} else if lib_name == "faucet" {
components.push(AccountComponent::from(
miden_standards::account::access::Ownable2Step::new(dummy_owner),
));
Expand Down
Loading
Loading