Skip to content
Open
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 @@ -47,6 +47,7 @@

### Features

- Added GER removal mechanism with a dedicated `ger_remover` role, `remove_ger` MASM procedure, `REMOVE_GER` note script, `RemoveGerNote` Rust helper, and a running keccak256 removed-GER hash chain ([#2837](https://github.com/0xMiden/protocol/pull/2837)).
- Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)).
- Added `SignedBlock` struct ([#2355](https://github.com/0xMiden/miden-base/pull/2235)).
- Enabled `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)).
Expand Down
19 changes: 17 additions & 2 deletions bin/bench-transaction/src/context_setups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,19 @@ pub async fn tx_consume_claim_note(data_source: ClaimDataSource) -> Result<Trans
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;

// CREATE GER REMOVER ACCOUNT (not used in this benchmark, but distinct from admin and manager)
let ger_remover = builder.add_existing_wallet(Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;

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

// GET CLAIM DATA FROM JSON
Expand Down Expand Up @@ -316,11 +325,17 @@ pub async fn tx_consume_b2agg_note() -> Result<TransactionContext> {
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;

// CREATE GER REMOVER ACCOUNT (not used in bridge-out, but required for bridge creation)
let ger_remover = builder.add_existing_wallet(Auth::BasicAuth {
auth_scheme: AuthScheme::Falcon512Poseidon2,
})?;

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

Expand Down
19 changes: 17 additions & 2 deletions crates/miden-agglayer/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,23 @@ on Miden. The bridge consumes these notes:
Subsequent CLAIM notes reference a GER that must be present in this map for the claim
to be valid.

TODO: GERs cannot be removed once inserted
([#2702](https://github.com/0xMiden/protocol/issues/2702)).
A separate GER Remover role can revoke a previously-registered GER by sending a
`REMOVE_GER` note. The bridge consumes such a note and:

1. Asserts the note sender is the designated GER remover (a role distinct from the GER
manager so that insertion and revocation authority can be split).
2. Computes `KEY = poseidon2::merge(GER_LOWER, GER_UPPER)`.
3. Asserts that `ger_map[KEY] == [1, 0, 0, 0]`, i.e. that the GER is currently known.
4. Overwrites `ger_map[KEY]` with `[0, 0, 0, 0]`, the Miden equivalent of Solidity's
`delete globalExitRootMap[ger]`. After this, any CLAIM note referencing the removed
GER will fail `assert_valid_ger`.
5. Updates a running keccak256 hash chain over all removed GERs:
`removed_ger_hash_chain = keccak256(removed_ger_hash_chain || removed_ger)`. This
chain is stored across two Word slots (`removed_ger_hash_chain_lo` /
`removed_ger_hash_chain_hi`) and mirrors the
`removeGlobalExitRoots` chain in Solidity's
`GlobalExitRootManagerL2SovereignChain`, providing an auditable record of every
removal.

TODO: No hash chain tracks GER insertions for proof generation
([#2707](https://github.com/0xMiden/protocol/issues/2707)).
Expand Down
157 changes: 157 additions & 0 deletions crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use miden::core::crypto::hashes::keccak256
use miden::core::crypto::hashes::poseidon2
use miden::protocol::account_id
use miden::protocol::active_account
Expand All @@ -12,17 +13,25 @@ const ERR_FAUCET_NOT_REGISTERED = "faucet is not registered in the bridge's fauc
const ERR_TOKEN_NOT_REGISTERED = "token address 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_MANAGER = "note sender is not the global exit root manager"
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_MANAGER_SLOT = word("agglayer::bridge::ger_manager_account_id")
const GER_REMOVER_SLOT = word("agglayer::bridge::ger_remover_account_id")
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")

# Storage slot constants for the removed GER hash chain.
# The chain is updated as `keccak256(prev_chain || ger)` on each removal and stored in two
# separate value slots (lo/hi) since a Word holds only 4 felts but the chain is 8 felts.
const REMOVED_GER_HASH_CHAIN_LO_SLOT = word("agglayer::bridge::removed_ger_hash_chain_lo")
const REMOVED_GER_HASH_CHAIN_HI_SLOT = word("agglayer::bridge::removed_ger_hash_chain_hi")

# Flags
const GER_KNOWN_FLAG = 1
const IS_FAUCET_REGISTERED_FLAG = 1
Expand Down Expand Up @@ -71,6 +80,71 @@ pub proc update_ger
# => [pad(16)]
end

#! Removes a Global Exit Root (GER) from the bridge account storage and folds it into the running
#! removed-GER keccak256 hash chain.
#!
#! Computes hash(GER) = poseidon2::merge(GER_LOWER, GER_UPPER), asserts that the GER is currently
#! known (map value equals [GER_KNOWN_FLAG, 0, 0, 0]), overwrites the map entry with [0, 0, 0, 0]
#! (Miden equivalent of Solidity's `delete globalExitRootMap[ger]`), and updates the removed-GER
#! hash chain as NEW_CHAIN = keccak256::merge(OLD_CHAIN, GER).
#!
#! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)]
#! Outputs: [pad(16)]
#!
#! Panics if:
#! - the note sender is not the global exit root remover.
#! - the GER is not currently registered in the bridge's GER map.
#!
#! Invocation: call
pub proc remove_ger
# assert the note sender is the global exit root remover.
exec.assert_sender_is_ger_remover
# => [GER_LOWER[4], GER_UPPER[4], pad(8)]

# duplicate the GER (16 felts) so we can use one copy to compute the map key + clear the map
# and the other copy as the keccak256 preimage for the chain hash update later.
dupw.1 dupw.1
# => [GER_LOWER, GER_UPPER, GER_LOWER, GER_UPPER, pad(8)]

# compute hash(GER) = poseidon2::merge(GER_LOWER, GER_UPPER) on the top copy.
exec.poseidon2::merge
# => [GER_HASH, GER_LOWER, GER_UPPER, pad(8)]

# assert the GER is currently known: map[GER_HASH] must equal [GER_KNOWN_FLAG, 0, 0, 0].
dupw
# => [GER_HASH, GER_HASH, GER_LOWER, GER_UPPER, pad(8)]

push.GER_MAP_STORAGE_SLOT[0..2]
# => [slot_id_prefix, slot_id_suffix, GER_HASH, GER_HASH, GER_LOWER, GER_UPPER, pad(8)]

exec.active_account::get_map_item
# => [VALUE, GER_HASH, GER_LOWER, GER_UPPER, pad(8)]

push.0.0.0.GER_KNOWN_FLAG
assert_eqw.err=ERR_GER_NOT_FOUND
# => [GER_HASH, GER_LOWER, GER_UPPER, pad(8)]

# overwrite the map entry with [0, 0, 0, 0] to mark it removed.
push.0.0.0.0
# => [0, 0, 0, 0, GER_HASH, GER_LOWER, GER_UPPER, pad(8)]

swapw
# => [GER_HASH, [0, 0, 0, 0], GER_LOWER, GER_UPPER, pad(8)]

push.GER_MAP_STORAGE_SLOT[0..2]
# => [slot_id_prefix, slot_id_suffix, GER_HASH, [0, 0, 0, 0], GER_LOWER, GER_UPPER, pad(8)]

exec.native_account::set_map_item
# => [OLD_VALUE, GER_LOWER, GER_UPPER, pad(8)]

dropw
# => [GER_LOWER, GER_UPPER, pad(8)]

# update the removed-GER keccak256 hash chain: NEW_CHAIN = keccak256::merge(OLD_CHAIN, GER).
exec.update_removed_ger_hash_chain
# => [pad(16)]
end

#! Asserts that the provided GER is valid (exists in storage).
#!
#! Computes hash(GER) = poseidon2::merge(GER_LOWER, GER_UPPER) and looks up the hash in the GER
Expand Down Expand Up @@ -307,3 +381,86 @@ proc assert_sender_is_ger_manager
assert.err=ERR_SENDER_NOT_GER_MANAGER
# => [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
#! REMOVED_GER_HASH_CHAIN_LO_SLOT and REMOVED_GER_HASH_CHAIN_HI_SLOT slots.
#!
#! Inputs: [GER_LOWER[4], GER_UPPER[4]]
#! Outputs: []
#!
#! Invocation: exec
proc update_removed_ger_hash_chain
# load OLD_CHAIN above the GER preimage on the stack so that keccak256::merge produces
# keccak256(OLD_CHAIN || GER), matching Solidity's
# `removedGERHashChain = efficientKeccak256(removedGERHashChain, removedGER)`.
exec.load_removed_ger_hash_chain_data
# => [OLD_CHAIN[8], GER_LOWER, GER_UPPER]

exec.keccak256::merge
# => [NEW_CHAIN_LO, NEW_CHAIN_HI]

exec.store_removed_ger_hash_chain
# => []
end

#! Loads the old removed-GER hash chain onto the stack, below the existing GER preimage.
#!
#! Inputs: [GER_LOWER[4], GER_UPPER[4]]
#! Outputs: [OLD_CHAIN[8], GER_LOWER, GER_UPPER]
#!
#! Invocation: exec
proc load_removed_ger_hash_chain_data
push.REMOVED_GER_HASH_CHAIN_HI_SLOT[0..2]
exec.active_account::get_item
# => [OLD_CHAIN_HI, GER_LOWER, GER_UPPER]

push.REMOVED_GER_HASH_CHAIN_LO_SLOT[0..2]
exec.active_account::get_item
# => [OLD_CHAIN_LO, OLD_CHAIN_HI, GER_LOWER, GER_UPPER]
end

#! Stores the updated removed-GER hash chain into the corresponding lo/hi storage slots.
#!
#! Inputs: [NEW_CHAIN_LO, NEW_CHAIN_HI]
#! Outputs: []
#!
#! Invocation: exec
proc store_removed_ger_hash_chain
push.REMOVED_GER_HASH_CHAIN_LO_SLOT[0..2]
exec.native_account::set_item dropw
# => [NEW_CHAIN_HI]

push.REMOVED_GER_HASH_CHAIN_HI_SLOT[0..2]
exec.native_account::set_item dropw
# => []
end
2 changes: 2 additions & 0 deletions crates/miden-agglayer/asm/components/bridge.masm
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
# The bridge exposes:
# - `register_faucet` from the bridge_config module
# - `update_ger` from the bridge_config module
# - `remove_ger` from the bridge_config module
# - `claim` for bridge-in
# - `bridge_out` for bridge-out

pub use ::agglayer::bridge::bridge_config::register_faucet
pub use ::agglayer::bridge::bridge_config::update_ger
pub use ::agglayer::bridge::bridge_config::remove_ger
pub use ::agglayer::bridge::bridge_in::claim
pub use ::agglayer::bridge::bridge_out::bridge_out
69 changes: 69 additions & 0 deletions crates/miden-agglayer/asm/note_scripts/REMOVE_GER.masm
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use agglayer::bridge::bridge_config
use miden::protocol::active_note
use miden::standards::attachments::network_account_target

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

const REMOVE_GER_NOTE_NUM_STORAGE_ITEMS = 8
const STORAGE_PTR_GER_LOWER = 0
const STORAGE_PTR_GER_UPPER = 4

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

const ERR_REMOVE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "REMOVE_GER script expects exactly 8 note storage items"
const ERR_REMOVE_GER_TARGET_ACCOUNT_MISMATCH = "REMOVE_GER note attachment target account does not match consuming account"

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

#! Agglayer Bridge REMOVE_GER script: removes a GER from the bridge account by calling the
#! bridge_config::remove_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), and only if the note was sent by the global exit root
#! remover.
#!
#! Requires that the account exposes:
#! - agglayer::bridge_config::remove_ger procedure.
#!
#! Inputs: [ARGS, pad(12)]
#! Outputs: [pad(16)]
#!
#! NoteStorage layout (8 felts total):
#! - GER_LOWER [0..3] : 4 felts
#! - GER_UPPER [4..7] : 4 felts
#!
#! Panics if:
#! - account does not expose remove_ger procedure.
#! - target account ID does not match the consuming account ID.
#! - number of note storage items is not exactly 8.
#! - the GER is not currently registered in the bridge's GER map.
begin
dropw
# => [pad(16)]

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

# Load note storage to memory
push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage
# => [num_storage_items, dest_ptr, pad(16)]

# Validate the number of storage items
push.REMOVE_GER_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_REMOVE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS drop
# => [pad(16)]

# Load GER_LOWER and GER_UPPER from note storage
mem_loadw_le.STORAGE_PTR_GER_UPPER
# => [GER_UPPER[4], pad(12)]

swapw mem_loadw_le.STORAGE_PTR_GER_LOWER
# => [GER_LOWER[4], GER_UPPER[4], pad(8)]

call.bridge_config::remove_ger
# => [pad(16)]
end
Loading
Loading