extern crate alloc;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec;
use alloc::vec::Vec;
use miden_core::{Felt, Word};
use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
use miden_protocol::account::{
Account,
AccountComponent,
AccountId,
AccountProcedureRoot,
RoleSymbol,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::crypto::hash::poseidon2::Poseidon2;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{Note, NoteScriptRoot};
use miden_standards::account::access::{PausableStorage, RoleConfig};
use miden_standards::account::auth::AuthNetworkAccount;
use miden_standards::note::{
ConstantFeePolicyConfigNote,
NetworkAccountTarget,
NetworkAccountTargetError,
NoteExecutionHint,
PauseConfig,
PauseConfigNote,
RbacConfigNote,
};
use miden_standards::procedure_root;
use miden_utils_sync::LazyLock;
use thiserror::Error;
use super::agglayer_bridge_component_package;
use crate::utils::Keccak256Output;
pub type RemovedGerHashChain = Keccak256Output;
pub use miden_standards::interop::eth::{
EthAddress,
EthAmount,
EthAmountError,
EthEmbeddedAccountId,
};
pub use crate::{
B2AggNote,
ClaimNote,
ClaimNoteStorage,
ConfigAggBridgeNote,
DeregisterAggFaucetNote,
ExitRoot,
GlobalIndex,
GlobalIndexError,
LeafData,
MetadataHash,
ProofData,
RemoveGerNote,
SmtNode,
UpdateGerNote,
};
include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs"));
static GER_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::ger_map")
.expect("GER map storage slot name should be valid")
});
static REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::removed_ger_hash_chain_lo")
.expect("removed GER hash chain lo storage slot name should be valid")
});
static REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::removed_ger_hash_chain_hi")
.expect("removed GER hash chain hi storage slot name should be valid")
});
static FAUCET_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::faucet_registry_map")
.expect("faucet registry map storage slot name should be valid")
});
static TOKEN_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::token_registry_map")
.expect("token registry map storage slot name should be valid")
});
static FAUCET_METADATA_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::faucet_metadata_map")
.expect("faucet metadata map storage slot name should be valid")
});
static NETWORK_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::network_id")
.expect("network ID storage slot name should be valid")
});
static CLAIM_NULLIFIERS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::claim_nullifiers")
.expect("claim nullifiers storage slot name should be valid")
});
static CGI_CHAIN_HASH_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::cgi_chain_hash_lo")
.expect("CGI chain hash_lo storage slot name should be valid")
});
static CGI_CHAIN_HASH_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::cgi_chain_hash_hi")
.expect("CGI chain hash_hi storage slot name should be valid")
});
static LET_FRONTIER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::let_frontier")
.expect("LET frontier storage slot name should be valid")
});
static LET_ROOT_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::let_root_lo")
.expect("LET root_lo storage slot name should be valid")
});
static LET_ROOT_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::let_root_hi")
.expect("LET root_hi storage slot name should be valid")
});
static LET_NUM_LEAVES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("agglayer::bridge::let_num_leaves")
.expect("LET num_leaves storage slot name should be valid")
});
static FAUCET_MANAGER_ROLE: LazyLock<RoleSymbol> = LazyLock::new(|| {
RoleSymbol::new("FAUCET_MNGR").expect("FAUCET_MNGR role symbol should be valid")
});
static GER_INJECTOR_ROLE: LazyLock<RoleSymbol> = LazyLock::new(|| {
RoleSymbol::new("GER_INJECTOR").expect("GER_INJECTOR role symbol should be valid")
});
static GER_REMOVER_ROLE: LazyLock<RoleSymbol> = LazyLock::new(|| {
RoleSymbol::new("GER_REMOVER").expect("GER_REMOVER role symbol should be valid")
});
static BRIDGE_COMPONENT_CODE: LazyLock<AccountComponentCode> =
LazyLock::new(|| AccountComponentCode::from(agglayer_bridge_component_package()));
procedure_root!(
REGISTER_FAUCET_ROOT,
AggLayerBridge::COMPONENT_NAMESPACE,
"register_faucet",
AggLayerBridge::code()
);
procedure_root!(
STORE_FAUCET_METADATA_HASH_ROOT,
AggLayerBridge::COMPONENT_NAMESPACE,
"store_faucet_metadata_hash",
AggLayerBridge::code()
);
procedure_root!(
UPDATE_GER_ROOT,
AggLayerBridge::COMPONENT_NAMESPACE,
"update_ger",
AggLayerBridge::code()
);
procedure_root!(
REMOVE_GER_ROOT,
AggLayerBridge::COMPONENT_NAMESPACE,
"remove_ger",
AggLayerBridge::code()
);
procedure_root!(
DEREGISTER_FAUCET_ROOT,
AggLayerBridge::COMPONENT_NAMESPACE,
"deregister_faucet",
AggLayerBridge::code()
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BridgeRoles {
roles: Vec<RoleConfig>,
}
impl BridgeRoles {
pub fn new(
faucet_managers: BTreeSet<AccountId>,
ger_injectors: BTreeSet<AccountId>,
ger_removers: BTreeSet<AccountId>,
) -> Result<Self, AgglayerBridgeError> {
let mut roles = Vec::new();
for (role, members) in [
(AggLayerBridge::faucet_manager_role(), &faucet_managers),
(AggLayerBridge::ger_injector_role(), &ger_injectors),
(AggLayerBridge::ger_remover_role(), &ger_removers),
] {
if members.is_empty() {
return Err(AgglayerBridgeError::EmptyBridgeRole(role));
}
roles.push(RoleConfig::new(role).with_members(members.iter().copied()));
}
Ok(Self { roles })
}
}
impl IntoIterator for BridgeRoles {
type Item = RoleConfig;
type IntoIter = alloc::vec::IntoIter<RoleConfig>;
fn into_iter(self) -> Self::IntoIter {
self.roles.into_iter()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AggLayerBridge {
network_id: u32,
}
impl AggLayerBridge {
const COMPONENT_NAMESPACE: &'static str = "agglayer::components::bridge";
pub fn new(network_id: u32) -> Self {
Self { network_id }
}
pub fn code() -> &'static AccountComponentCode {
&BRIDGE_COMPONENT_CODE
}
pub fn faucet_manager_role() -> RoleSymbol {
FAUCET_MANAGER_ROLE.clone()
}
pub fn ger_injector_role() -> RoleSymbol {
GER_INJECTOR_ROLE.clone()
}
pub fn ger_remover_role() -> RoleSymbol {
GER_REMOVER_ROLE.clone()
}
pub fn register_faucet_root() -> AccountProcedureRoot {
*REGISTER_FAUCET_ROOT
}
pub fn store_faucet_metadata_hash_root() -> AccountProcedureRoot {
*STORE_FAUCET_METADATA_HASH_ROOT
}
pub fn update_ger_root() -> AccountProcedureRoot {
*UPDATE_GER_ROOT
}
pub fn remove_ger_root() -> AccountProcedureRoot {
*REMOVE_GER_ROOT
}
pub fn deregister_faucet_root() -> AccountProcedureRoot {
*DEREGISTER_FAUCET_ROOT
}
pub fn procedure_roles() -> BTreeMap<AccountProcedureRoot, RoleSymbol> {
BTreeMap::from([
(Self::register_faucet_root(), Self::faucet_manager_role()),
(Self::store_faucet_metadata_hash_root(), Self::faucet_manager_role()),
(Self::deregister_faucet_root(), Self::faucet_manager_role()),
(Self::update_ger_root(), Self::ger_injector_role()),
(Self::remove_ger_root(), Self::ger_remover_role()),
])
}
pub fn ger_map_slot_name() -> &'static StorageSlotName {
&GER_MAP_SLOT_NAME
}
pub fn removed_ger_hash_chain_lo_slot_name() -> &'static StorageSlotName {
&REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME
}
pub fn removed_ger_hash_chain_hi_slot_name() -> &'static StorageSlotName {
&REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME
}
pub fn faucet_registry_map_slot_name() -> &'static StorageSlotName {
&FAUCET_REGISTRY_MAP_SLOT_NAME
}
pub fn token_registry_map_slot_name() -> &'static StorageSlotName {
&TOKEN_REGISTRY_MAP_SLOT_NAME
}
pub fn faucet_metadata_map_slot_name() -> &'static StorageSlotName {
&FAUCET_METADATA_MAP_SLOT_NAME
}
pub fn network_id_slot_name() -> &'static StorageSlotName {
&NETWORK_ID_SLOT_NAME
}
pub fn claim_nullifiers_slot_name() -> &'static StorageSlotName {
&CLAIM_NULLIFIERS_SLOT_NAME
}
pub fn cgi_chain_hash_lo_slot_name() -> &'static StorageSlotName {
&CGI_CHAIN_HASH_LO_SLOT_NAME
}
pub fn cgi_chain_hash_hi_slot_name() -> &'static StorageSlotName {
&CGI_CHAIN_HASH_HI_SLOT_NAME
}
pub fn let_frontier_slot_name() -> &'static StorageSlotName {
&LET_FRONTIER_SLOT_NAME
}
pub fn let_root_lo_slot_name() -> &'static StorageSlotName {
&LET_ROOT_LO_SLOT_NAME
}
pub fn let_root_hi_slot_name() -> &'static StorageSlotName {
&LET_ROOT_HI_SLOT_NAME
}
pub fn let_num_leaves_slot_name() -> &'static StorageSlotName {
&LET_NUM_LEAVES_SLOT_NAME
}
pub fn allowed_notes() -> BTreeSet<NoteScriptRoot> {
let mut notes = BTreeSet::from([
ClaimNote::script_root(),
B2AggNote::script_root(),
ConfigAggBridgeNote::script_root(),
DeregisterAggFaucetNote::script_root(),
UpdateGerNote::script_root(),
RemoveGerNote::script_root(),
PauseConfigNote::script_root(),
RbacConfigNote::script_root(),
ConstantFeePolicyConfigNote::script_root(),
]);
notes.extend(AuthNetworkAccount::default_allowed_note_scripts());
notes
}
pub fn pause_note<R: FeltRng>(
config: PauseConfig,
sender: AccountId,
bridge_id: AccountId,
rng: &mut R,
) -> Result<Note, AgglayerBridgeError> {
let attachment = NetworkAccountTarget::new(bridge_id, NoteExecutionHint::Always)
.map_err(AgglayerBridgeError::NonPublicPauseNoteTarget)?;
PauseConfigNote::builder()
.sender(sender)
.target(bridge_id)
.config(config)
.attachment(attachment)
.generate_serial_number(rng)
.build()
.map(Into::into)
.map_err(AgglayerBridgeError::PauseNoteCreationFailed)
}
const REGISTERED_GER_MAP_VALUE: Word =
Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]);
pub fn is_ger_registered(
ger: ExitRoot,
bridge_account: &Account,
) -> Result<bool, AgglayerBridgeError> {
Self::assert_bridge_account(bridge_account)?;
let ger_lower: Word = ger.to_elements()[0..4].try_into().unwrap();
let ger_upper: Word = ger.to_elements()[4..8].try_into().unwrap();
let ger_hash = Poseidon2::merge(&[ger_lower, ger_upper]);
let stored_value = bridge_account
.storage()
.get_map_item(AggLayerBridge::ger_map_slot_name(), StorageMapKey::from_raw(ger_hash))
.expect("provided account should have AggLayer Bridge specific storage slots");
if stored_value == Self::REGISTERED_GER_MAP_VALUE {
Ok(true)
} else {
Ok(false)
}
}
pub fn read_local_exit_root(account: &Account) -> Result<Vec<Felt>, AgglayerBridgeError> {
Self::assert_bridge_account(account)?;
let root_lo_slot = AggLayerBridge::let_root_lo_slot_name();
let root_hi_slot = AggLayerBridge::let_root_hi_slot_name();
let root_lo = account
.storage()
.get_item(root_lo_slot)
.expect("should be able to read LET root lo");
let root_hi = account
.storage()
.get_item(root_hi_slot)
.expect("should be able to read LET root hi");
let mut root = Vec::with_capacity(8);
root.extend(root_lo.to_vec());
root.extend(root_hi.to_vec());
Ok(root)
}
pub fn network_id(account: &Account) -> Result<u32, AgglayerBridgeError> {
Self::assert_bridge_account(account)?;
let value = account
.storage()
.get_item(AggLayerBridge::network_id_slot_name())
.expect("should be able to read the network ID");
let network_id = u32::try_from(value.to_vec()[0].as_canonical_u64())
.map_err(|_| AgglayerBridgeError::InvalidNetworkId)?;
Ok(network_id)
}
pub fn read_let_num_leaves(account: &Account) -> u64 {
let num_leaves_slot = AggLayerBridge::let_num_leaves_slot_name();
let value = account
.storage()
.get_item(num_leaves_slot)
.expect("should be able to read LET num leaves");
value.to_vec()[0].as_canonical_u64()
}
pub fn cgi_chain_hash(
bridge_account: &Account,
) -> Result<crate::claim_note::CgiChainHash, AgglayerBridgeError> {
Self::assert_bridge_account(bridge_account)?;
let cgi_chain_hash_lo = bridge_account
.storage()
.get_item(AggLayerBridge::cgi_chain_hash_lo_slot_name())
.expect("failed to get CGI hash chain lo slot");
let cgi_chain_hash_hi = bridge_account
.storage()
.get_item(AggLayerBridge::cgi_chain_hash_hi_slot_name())
.expect("failed to get CGI hash chain hi slot");
Ok(crate::claim_note::CgiChainHash::new(Self::chain_hash_bytes(
cgi_chain_hash_lo,
cgi_chain_hash_hi,
)))
}
pub fn removed_ger_hash_chain(
bridge_account: &Account,
) -> Result<RemovedGerHashChain, AgglayerBridgeError> {
Self::assert_bridge_account(bridge_account)?;
let chain_lo = bridge_account
.storage()
.get_item(AggLayerBridge::removed_ger_hash_chain_lo_slot_name())
.expect("failed to get removed GER hash chain lo slot");
let chain_hi = bridge_account
.storage()
.get_item(AggLayerBridge::removed_ger_hash_chain_hi_slot_name())
.expect("failed to get removed GER hash chain hi slot");
Ok(RemovedGerHashChain::new(Self::chain_hash_bytes(chain_lo, chain_hi)))
}
fn chain_hash_bytes(lo: Word, hi: Word) -> [u8; 32] {
lo.iter()
.chain(hi.iter())
.flat_map(|felt| {
(u32::try_from(felt.as_canonical_u64()).expect("Felt value does not fit into u32"))
.to_le_bytes()
})
.collect::<Vec<u8>>()
.try_into()
.expect("keccak hash should consist of exactly 32 bytes")
}
fn assert_bridge_account(account: &Account) -> Result<(), AgglayerBridgeError> {
Self::assert_storage_slots(account)?;
Self::assert_code_commitment(account)?;
Ok(())
}
fn assert_storage_slots(account: &Account) -> Result<(), AgglayerBridgeError> {
let account_storage_slot_names: Vec<&StorageSlotName> = account
.storage()
.slots()
.iter()
.map(|storage_slot| storage_slot.name())
.collect::<Vec<&StorageSlotName>>();
let are_slots_present = Self::slot_names()
.iter()
.all(|slot_name| account_storage_slot_names.contains(slot_name));
if !are_slots_present {
return Err(AgglayerBridgeError::StorageSlotsMismatch);
}
Ok(())
}
fn assert_code_commitment(account: &Account) -> Result<(), AgglayerBridgeError> {
if BRIDGE_CODE_COMMITMENT != account.code().commitment() {
return Err(AgglayerBridgeError::CodeCommitmentMismatch);
}
Ok(())
}
fn slot_names() -> Vec<&'static StorageSlotName> {
vec![
&*GER_MAP_SLOT_NAME,
&*LET_FRONTIER_SLOT_NAME,
&*LET_ROOT_LO_SLOT_NAME,
&*LET_ROOT_HI_SLOT_NAME,
&*LET_NUM_LEAVES_SLOT_NAME,
&*FAUCET_REGISTRY_MAP_SLOT_NAME,
&*TOKEN_REGISTRY_MAP_SLOT_NAME,
&*FAUCET_METADATA_MAP_SLOT_NAME,
&*REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME,
&*REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME,
&*CGI_CHAIN_HASH_LO_SLOT_NAME,
&*CGI_CHAIN_HASH_HI_SLOT_NAME,
&*CLAIM_NULLIFIERS_SLOT_NAME,
&*NETWORK_ID_SLOT_NAME,
PausableStorage::is_paused_slot(),
]
}
}
impl From<AggLayerBridge> for AccountComponent {
fn from(bridge: AggLayerBridge) -> Self {
let bridge_storage_slots = vec![
StorageSlot::with_empty_map(GER_MAP_SLOT_NAME.clone()),
StorageSlot::with_empty_map(LET_FRONTIER_SLOT_NAME.clone()),
StorageSlot::with_value(LET_ROOT_LO_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(LET_ROOT_HI_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_empty_map(FAUCET_REGISTRY_MAP_SLOT_NAME.clone()),
StorageSlot::with_empty_map(TOKEN_REGISTRY_MAP_SLOT_NAME.clone()),
StorageSlot::with_empty_map(FAUCET_METADATA_MAP_SLOT_NAME.clone()),
StorageSlot::with_value(REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(CGI_CHAIN_HASH_LO_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_value(CGI_CHAIN_HASH_HI_SLOT_NAME.clone(), Word::empty()),
StorageSlot::with_empty_map(CLAIM_NULLIFIERS_SLOT_NAME.clone()),
StorageSlot::with_value(
NETWORK_ID_SLOT_NAME.clone(),
Word::new([Felt::from(bridge.network_id), Felt::ZERO, Felt::ZERO, Felt::ZERO]),
),
];
bridge_component(bridge_storage_slots)
}
}
#[derive(Debug, Error)]
pub enum AgglayerBridgeError {
#[error(
"provided account does not have storage slots required for the AggLayer Bridge account"
)]
StorageSlotsMismatch,
#[error(
"the code commitment of the provided account does not match the code commitment of the AggLayer Bridge account"
)]
CodeCommitmentMismatch,
#[error("bridge role {0} must have at least one initial holder")]
EmptyBridgeRole(RoleSymbol),
#[error("the network ID stored in the bridge account does not fit into a u32")]
InvalidNetworkId,
#[error("bridge account must be public to be named by a network account target")]
NonPublicPauseNoteTarget(#[source] NetworkAccountTargetError),
#[error("failed to create a PAUSE_CONFIG note for the bridge account")]
PauseNoteCreationFailed(#[source] NoteError),
}
fn bridge_component(storage_slots: Vec<StorageSlot>) -> AccountComponent {
let package = agglayer_bridge_component_package();
let metadata = AccountComponentMetadata::new("agglayer::bridge")
.with_description("Bridge component for AggLayer");
AccountComponent::new(package, storage_slots, metadata)
.expect("bridge component should satisfy the requirements of a valid account component")
}