Skip to main content

miden_agglayer/
bridge.rs

1extern crate alloc;
2
3use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::vec;
5use alloc::vec::Vec;
6
7use miden_core::{Felt, Word};
8use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata};
9use miden_protocol::account::{
10    Account,
11    AccountComponent,
12    AccountId,
13    AccountProcedureRoot,
14    RoleSymbol,
15    StorageMapKey,
16    StorageSlot,
17    StorageSlotName,
18};
19use miden_protocol::crypto::hash::poseidon2::Poseidon2;
20use miden_protocol::crypto::rand::FeltRng;
21use miden_protocol::errors::NoteError;
22use miden_protocol::note::{Note, NoteScriptRoot};
23use miden_standards::account::access::{PausableStorage, RoleConfig};
24use miden_standards::account::auth::AuthNetworkAccount;
25use miden_standards::note::{
26    ConstantFeePolicyConfigNote,
27    NetworkAccountTarget,
28    NetworkAccountTargetError,
29    NoteExecutionHint,
30    PauseConfig,
31    PauseConfigNote,
32    RbacConfigNote,
33};
34use miden_standards::procedure_root;
35use miden_utils_sync::LazyLock;
36use thiserror::Error;
37
38use super::agglayer_bridge_component_package;
39use crate::utils::Keccak256Output;
40
41/// Removed-GER hash chain representation (32-byte Keccak256 hash)
42pub type RemovedGerHashChain = Keccak256Output;
43pub use miden_standards::interop::eth::{
44    EthAddress,
45    EthAmount,
46    EthAmountError,
47    EthEmbeddedAccountId,
48};
49
50pub use crate::{
51    B2AggNote,
52    ClaimNote,
53    ClaimNoteStorage,
54    ConfigAggBridgeNote,
55    DeregisterAggFaucetNote,
56    ExitRoot,
57    GlobalIndex,
58    GlobalIndexError,
59    LeafData,
60    MetadataHash,
61    ProofData,
62    RemoveGerNote,
63    SmtNode,
64    UpdateGerNote,
65};
66
67// CONSTANTS
68// ================================================================================================
69// Include the generated agglayer constants
70include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs"));
71
72// AGGLAYER BRIDGE STRUCT
73// ================================================================================================
74
75// bridge config
76// ------------------------------------------------------------------------------------------------
77
78static GER_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
79    StorageSlotName::new("agglayer::bridge::ger_map")
80        .expect("GER map storage slot name should be valid")
81});
82static REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
83    StorageSlotName::new("agglayer::bridge::removed_ger_hash_chain_lo")
84        .expect("removed GER hash chain lo storage slot name should be valid")
85});
86static REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
87    StorageSlotName::new("agglayer::bridge::removed_ger_hash_chain_hi")
88        .expect("removed GER hash chain hi storage slot name should be valid")
89});
90static FAUCET_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
91    StorageSlotName::new("agglayer::bridge::faucet_registry_map")
92        .expect("faucet registry map storage slot name should be valid")
93});
94static TOKEN_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
95    StorageSlotName::new("agglayer::bridge::token_registry_map")
96        .expect("token registry map storage slot name should be valid")
97});
98static FAUCET_METADATA_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
99    StorageSlotName::new("agglayer::bridge::faucet_metadata_map")
100        .expect("faucet metadata map storage slot name should be valid")
101});
102static NETWORK_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
103    StorageSlotName::new("agglayer::bridge::network_id")
104        .expect("network ID storage slot name should be valid")
105});
106
107// bridge in
108// ------------------------------------------------------------------------------------------------
109
110static CLAIM_NULLIFIERS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
111    StorageSlotName::new("agglayer::bridge::claim_nullifiers")
112        .expect("claim nullifiers storage slot name should be valid")
113});
114static CGI_CHAIN_HASH_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
115    StorageSlotName::new("agglayer::bridge::cgi_chain_hash_lo")
116        .expect("CGI chain hash_lo storage slot name should be valid")
117});
118static CGI_CHAIN_HASH_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
119    StorageSlotName::new("agglayer::bridge::cgi_chain_hash_hi")
120        .expect("CGI chain hash_hi storage slot name should be valid")
121});
122
123// bridge out
124// ------------------------------------------------------------------------------------------------
125
126static LET_FRONTIER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
127    StorageSlotName::new("agglayer::bridge::let_frontier")
128        .expect("LET frontier storage slot name should be valid")
129});
130static LET_ROOT_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
131    StorageSlotName::new("agglayer::bridge::let_root_lo")
132        .expect("LET root_lo storage slot name should be valid")
133});
134static LET_ROOT_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
135    StorageSlotName::new("agglayer::bridge::let_root_hi")
136        .expect("LET root_hi storage slot name should be valid")
137});
138static LET_NUM_LEAVES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
139    StorageSlotName::new("agglayer::bridge::let_num_leaves")
140        .expect("LET num_leaves storage slot name should be valid")
141});
142
143// BRIDGE RBAC ROLES
144// ================================================================================================
145
146static FAUCET_MANAGER_ROLE: LazyLock<RoleSymbol> = LazyLock::new(|| {
147    RoleSymbol::new("FAUCET_MNGR").expect("FAUCET_MNGR role symbol should be valid")
148});
149static GER_INJECTOR_ROLE: LazyLock<RoleSymbol> = LazyLock::new(|| {
150    RoleSymbol::new("GER_INJECTOR").expect("GER_INJECTOR role symbol should be valid")
151});
152static GER_REMOVER_ROLE: LazyLock<RoleSymbol> = LazyLock::new(|| {
153    RoleSymbol::new("GER_REMOVER").expect("GER_REMOVER role symbol should be valid")
154});
155
156/// The assembled bridge account component code, used to resolve the roots of the bridge's
157/// role-gated procedures.
158static BRIDGE_COMPONENT_CODE: LazyLock<AccountComponentCode> =
159    LazyLock::new(|| AccountComponentCode::from(agglayer_bridge_component_package()));
160
161procedure_root!(
162    REGISTER_FAUCET_ROOT,
163    AggLayerBridge::COMPONENT_NAMESPACE,
164    "register_faucet",
165    AggLayerBridge::code()
166);
167procedure_root!(
168    STORE_FAUCET_METADATA_HASH_ROOT,
169    AggLayerBridge::COMPONENT_NAMESPACE,
170    "store_faucet_metadata_hash",
171    AggLayerBridge::code()
172);
173procedure_root!(
174    UPDATE_GER_ROOT,
175    AggLayerBridge::COMPONENT_NAMESPACE,
176    "update_ger",
177    AggLayerBridge::code()
178);
179procedure_root!(
180    REMOVE_GER_ROOT,
181    AggLayerBridge::COMPONENT_NAMESPACE,
182    "remove_ger",
183    AggLayerBridge::code()
184);
185procedure_root!(
186    DEREGISTER_FAUCET_ROOT,
187    AggLayerBridge::COMPONENT_NAMESPACE,
188    "deregister_faucet",
189    AggLayerBridge::code()
190);
191
192// BRIDGE ROLES
193// ================================================================================================
194
195/// The accounts that initially hold each of the bridge's privileged RBAC roles.
196///
197/// Used to seed the bridge account's RBAC role membership at creation. Each role gates a distinct
198/// set of bridge procedures:
199/// - `FAUCET_MNGR` gates `register_faucet` and `store_faucet_metadata_hash`.
200/// - `GER_INJECTOR` gates `update_ger`.
201/// - `GER_REMOVER` gates `remove_ger`.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct BridgeRoles {
204    roles: Vec<RoleConfig>,
205}
206
207impl BridgeRoles {
208    /// Creates the initial bridge role membership from the holders of each role.
209    ///
210    /// The roles are left administered by the `ADMIN` role.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`AgglayerBridgeError::EmptyBridgeRole`] if any of the three roles is given an empty
215    /// set of holders.
216    pub fn new(
217        faucet_managers: BTreeSet<AccountId>,
218        ger_injectors: BTreeSet<AccountId>,
219        ger_removers: BTreeSet<AccountId>,
220    ) -> Result<Self, AgglayerBridgeError> {
221        let mut roles = Vec::new();
222        for (role, members) in [
223            (AggLayerBridge::faucet_manager_role(), &faucet_managers),
224            (AggLayerBridge::ger_injector_role(), &ger_injectors),
225            (AggLayerBridge::ger_remover_role(), &ger_removers),
226        ] {
227            if members.is_empty() {
228                return Err(AgglayerBridgeError::EmptyBridgeRole(role));
229            }
230            roles.push(RoleConfig::new(role).with_members(members.iter().copied()));
231        }
232
233        Ok(Self { roles })
234    }
235}
236
237impl IntoIterator for BridgeRoles {
238    type Item = RoleConfig;
239    type IntoIter = alloc::vec::IntoIter<RoleConfig>;
240
241    fn into_iter(self) -> Self::IntoIter {
242        self.roles.into_iter()
243    }
244}
245
246// AGG LAYER BRIDGE
247// ================================================================================================
248
249/// An [`AccountComponent`] implementing the AggLayer Bridge.
250///
251/// It reexports the procedures from `agglayer::bridge`. When linking against this
252/// component, the `agglayer` package must be available to the assembler.
253/// The procedures of this component are:
254/// - `register_faucet`, which registers a faucet in the bridge.
255/// - `deregister_faucet`, which clears a previously-registered faucet from both the faucet registry
256///   and token registry maps.
257/// - `update_ger`, which injects a new GER into the storage map.
258/// - `remove_ger`, which removes a GER from the storage map and folds it into the running
259///   removed-GER keccak256 hash chain.
260/// - `bridge_out`, which bridges an asset out of Miden to the destination network.
261/// - `claim`, which validates a claim against the AggLayer bridge and creates a MINT note for the
262///   AggLayer Faucet.
263///
264/// ## Access control
265///
266/// The bridge's privileged roles are managed by the account's RBAC stack
267/// (`RoleBasedAccessControl` + `Authority`), installed alongside this component at account
268/// creation. The role-gated procedures call `authority::assert_authorized`, which requires the note
269/// sender to hold the role mapped to the procedure. See [`BridgeRoles`] and
270/// [`AggLayerBridge::procedure_roles`].
271///
272/// ## Storage Layout
273///
274/// - [`Self::ger_map_slot_name`]: Stores the GERs.
275/// - [`Self::removed_ger_hash_chain_lo_slot_name`]: Stores the lower 128 bits of the removed-GER
276///   keccak256 hash chain.
277/// - [`Self::removed_ger_hash_chain_hi_slot_name`]: Stores the upper 128 bits of the removed-GER
278///   keccak256 hash chain.
279/// - [`Self::faucet_registry_map_slot_name`]: Stores the faucet registry map.
280/// - [`Self::token_registry_map_slot_name`]: Stores the token address → faucet ID map.
281/// - [`Self::faucet_metadata_map_slot_name`]: Stores conversion metadata (origin address, origin
282///   network, scale, metadata hash) for all registered faucets, keyed by sub-key scheme based on
283///   faucet ID.
284/// - [`Self::network_id_slot_name`]: Stores the bridge's AggLayer network ID.
285/// - [`Self::claim_nullifiers_slot_name`]: Stores the CLAIM note nullifiers map (RPO(leaf_index,
286///   source_bridge_network) → \[1, 0, 0, 0\]).
287/// - [`Self::cgi_chain_hash_lo_slot_name`]: Stores the lower 128 bits of the CGI chain hash.
288/// - [`Self::cgi_chain_hash_hi_slot_name`]: Stores the upper 128 bits of the CGI chain hash.
289/// - [`Self::let_frontier_slot_name`]: Stores the Local Exit Tree (LET) frontier.
290/// - [`Self::let_root_lo_slot_name`]: Stores the lower 128 bits of the LET root.
291/// - [`Self::let_root_hi_slot_name`]: Stores the upper 128 bits of the LET root.
292/// - [`Self::let_num_leaves_slot_name`]: Stores the number of leaves in the LET frontier.
293///
294/// The bridge starts with an empty faucet registry; faucets are registered at runtime via
295/// CONFIG_AGG_BRIDGE notes and can be removed via DEREGISTER_AGG_FAUCET notes.
296///
297/// Claim validation compares the leaf's `destination_network` to the bridge's own network ID,
298/// which is stored in [`Self::network_id_slot_name`] at account creation and read at runtime by
299/// the bridge MASM. The network ID is set once and never mutated, so different deployments (e.g.
300/// testnet vs mainnet) can use different IDs.
301#[derive(Debug, Clone, Copy)]
302pub struct AggLayerBridge {
303    network_id: u32,
304}
305
306impl AggLayerBridge {
307    // CONSTANTS
308    // --------------------------------------------------------------------------------------------
309
310    /// Namespace of the assembled bridge account component package (the
311    /// `asm/components/bridge/bridge.masm` wrapper). Procedure roots are resolved as
312    /// `<namespace>::<proc_name>`.
313    const COMPONENT_NAMESPACE: &'static str = "agglayer::components::bridge";
314
315    // CONSTRUCTORS
316    // --------------------------------------------------------------------------------------------
317
318    /// Creates a new AggLayer bridge component with the standard configuration.
319    ///
320    /// `network_id` is the AggLayer network ID assigned to the Miden chain; it is written to the
321    /// [`Self::network_id_slot_name`] storage slot at account creation.
322    pub fn new(network_id: u32) -> Self {
323        Self { network_id }
324    }
325
326    // RBAC ROLES
327    // --------------------------------------------------------------------------------------------
328
329    /// Returns the assembled bridge account component code.
330    pub fn code() -> &'static AccountComponentCode {
331        &BRIDGE_COMPONENT_CODE
332    }
333
334    /// Returns the `FAUCET_MNGR` role symbol. Holders may register faucets and store faucet
335    /// metadata (`register_faucet`, `store_faucet_metadata_hash`).
336    pub fn faucet_manager_role() -> RoleSymbol {
337        FAUCET_MANAGER_ROLE.clone()
338    }
339
340    /// Returns the `GER_INJECTOR` role symbol. Holders may inject GERs (`update_ger`).
341    pub fn ger_injector_role() -> RoleSymbol {
342        GER_INJECTOR_ROLE.clone()
343    }
344
345    /// Returns the `GER_REMOVER` role symbol. Holders may remove GERs (`remove_ger`).
346    pub fn ger_remover_role() -> RoleSymbol {
347        GER_REMOVER_ROLE.clone()
348    }
349
350    /// Returns the procedure root of the bridge's `register_faucet` procedure.
351    pub fn register_faucet_root() -> AccountProcedureRoot {
352        *REGISTER_FAUCET_ROOT
353    }
354
355    /// Returns the procedure root of the bridge's `store_faucet_metadata_hash` procedure.
356    pub fn store_faucet_metadata_hash_root() -> AccountProcedureRoot {
357        *STORE_FAUCET_METADATA_HASH_ROOT
358    }
359
360    /// Returns the procedure root of the bridge's `update_ger` procedure.
361    pub fn update_ger_root() -> AccountProcedureRoot {
362        *UPDATE_GER_ROOT
363    }
364
365    /// Returns the procedure root of the bridge's `remove_ger` procedure.
366    pub fn remove_ger_root() -> AccountProcedureRoot {
367        *REMOVE_GER_ROOT
368    }
369
370    /// Returns the procedure root of the bridge's `deregister_faucet` procedure.
371    pub fn deregister_faucet_root() -> AccountProcedureRoot {
372        *DEREGISTER_FAUCET_ROOT
373    }
374
375    /// Returns the fixed procedure-to-role map used to configure the account's `Authority`
376    /// (`RbacControlled`) component. Each role-gated bridge procedure is mapped to the role
377    /// required to invoke it.
378    pub fn procedure_roles() -> BTreeMap<AccountProcedureRoot, RoleSymbol> {
379        BTreeMap::from([
380            (Self::register_faucet_root(), Self::faucet_manager_role()),
381            (Self::store_faucet_metadata_hash_root(), Self::faucet_manager_role()),
382            (Self::deregister_faucet_root(), Self::faucet_manager_role()),
383            (Self::update_ger_root(), Self::ger_injector_role()),
384            (Self::remove_ger_root(), Self::ger_remover_role()),
385        ])
386    }
387
388    // PUBLIC ACCESSORS
389    // --------------------------------------------------------------------------------------------
390
391    // --- bridge config ----
392
393    /// Storage slot name for the GERs map.
394    pub fn ger_map_slot_name() -> &'static StorageSlotName {
395        &GER_MAP_SLOT_NAME
396    }
397
398    /// Storage slot name for the lower 128 bits of the removed-GER keccak256 hash chain.
399    pub fn removed_ger_hash_chain_lo_slot_name() -> &'static StorageSlotName {
400        &REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME
401    }
402
403    /// Storage slot name for the upper 128 bits of the removed-GER keccak256 hash chain.
404    pub fn removed_ger_hash_chain_hi_slot_name() -> &'static StorageSlotName {
405        &REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME
406    }
407
408    /// Storage slot name for the faucet registry map.
409    pub fn faucet_registry_map_slot_name() -> &'static StorageSlotName {
410        &FAUCET_REGISTRY_MAP_SLOT_NAME
411    }
412
413    /// Storage slot name for the token registry map.
414    pub fn token_registry_map_slot_name() -> &'static StorageSlotName {
415        &TOKEN_REGISTRY_MAP_SLOT_NAME
416    }
417
418    /// Storage slot name for the faucet metadata map.
419    ///
420    /// This map stores conversion metadata (origin address, origin network, scale, metadata hash)
421    /// for all registered faucets, keyed by sub-key scheme based on faucet ID.
422    pub fn faucet_metadata_map_slot_name() -> &'static StorageSlotName {
423        &FAUCET_METADATA_MAP_SLOT_NAME
424    }
425
426    /// Storage slot name for the bridge's AggLayer network ID.
427    ///
428    /// Holds the network ID assigned to this bridge as a single felt in the first word element.
429    /// It is set at account creation and never mutated by any bridge procedure.
430    pub fn network_id_slot_name() -> &'static StorageSlotName {
431        &NETWORK_ID_SLOT_NAME
432    }
433
434    // --- bridge in --------
435
436    /// Storage slot name for the CLAIM note nullifiers map.
437    pub fn claim_nullifiers_slot_name() -> &'static StorageSlotName {
438        &CLAIM_NULLIFIERS_SLOT_NAME
439    }
440
441    /// Storage slot name for the lower 128 bits of the CGI chain hash.
442    pub fn cgi_chain_hash_lo_slot_name() -> &'static StorageSlotName {
443        &CGI_CHAIN_HASH_LO_SLOT_NAME
444    }
445
446    /// Storage slot name for the upper 128 bits of the CGI chain hash.
447    pub fn cgi_chain_hash_hi_slot_name() -> &'static StorageSlotName {
448        &CGI_CHAIN_HASH_HI_SLOT_NAME
449    }
450
451    // --- bridge out -------
452
453    /// Storage slot name for the Local Exit Tree (LET) frontier.
454    pub fn let_frontier_slot_name() -> &'static StorageSlotName {
455        &LET_FRONTIER_SLOT_NAME
456    }
457
458    /// Storage slot name for the lower 32 bits of the LET root.
459    pub fn let_root_lo_slot_name() -> &'static StorageSlotName {
460        &LET_ROOT_LO_SLOT_NAME
461    }
462
463    /// Storage slot name for the upper 32 bits of the LET root.
464    pub fn let_root_hi_slot_name() -> &'static StorageSlotName {
465        &LET_ROOT_HI_SLOT_NAME
466    }
467
468    /// Storage slot name for the number of leaves in the LET frontier.
469    pub fn let_num_leaves_slot_name() -> &'static StorageSlotName {
470        &LET_NUM_LEAVES_SLOT_NAME
471    }
472
473    // ALLOWED NOTES
474    // --------------------------------------------------------------------------------------------
475
476    /// Returns the input-note script roots allowlisted on a newly deployed AggLayer bridge.
477    ///
478    /// A live account's allowlist is available through
479    /// [`NetworkAccount::allowed_notes`](miden_standards::account::auth::NetworkAccount::allowed_notes).
480    pub fn allowed_notes() -> BTreeSet<NoteScriptRoot> {
481        let mut notes = BTreeSet::from([
482            ClaimNote::script_root(),
483            B2AggNote::script_root(),
484            ConfigAggBridgeNote::script_root(),
485            DeregisterAggFaucetNote::script_root(),
486            UpdateGerNote::script_root(),
487            RemoveGerNote::script_root(),
488            PauseConfigNote::script_root(),
489            RbacConfigNote::script_root(),
490            ConstantFeePolicyConfigNote::script_root(),
491        ]);
492        notes.extend(AuthNetworkAccount::default_allowed_note_scripts());
493        notes
494    }
495
496    // PAUSE NOTE
497    // --------------------------------------------------------------------------------------------
498
499    /// Builds a [`PauseConfigNote`] that toggles the emergency pause of the bridge account
500    /// `bridge_id`. `sender` must hold the bridge's `ADMIN` role.
501    ///
502    /// Use this instead of [`PauseConfigNote::builder`] directly: it reports a non-public
503    /// `bridge_id` as [`AgglayerBridgeError::NonPublicPauseNoteTarget`] rather than as an opaque
504    /// note creation failure.
505    ///
506    /// # Errors
507    /// Returns an error if `bridge_id` is not a public account, or if note creation fails.
508    pub fn pause_note<R: FeltRng>(
509        config: PauseConfig,
510        sender: AccountId,
511        bridge_id: AccountId,
512        rng: &mut R,
513    ) -> Result<Note, AgglayerBridgeError> {
514        let attachment = NetworkAccountTarget::new(bridge_id, NoteExecutionHint::Always)
515            .map_err(AgglayerBridgeError::NonPublicPauseNoteTarget)?;
516
517        PauseConfigNote::builder()
518            .sender(sender)
519            .target(bridge_id)
520            .config(config)
521            .attachment(attachment)
522            .generate_serial_number(rng)
523            .build()
524            .map(Into::into)
525            .map_err(AgglayerBridgeError::PauseNoteCreationFailed)
526    }
527
528    // STORAGE READERS
529    // --------------------------------------------------------------------------------------------
530
531    const REGISTERED_GER_MAP_VALUE: Word =
532        Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]);
533
534    /// Returns a boolean indicating whether the provided GER is present in storage of the provided
535    /// bridge account.
536    ///
537    /// # Errors
538    ///
539    /// Returns an error if:
540    /// - the provided account is not an [`AggLayerBridge`] account.
541    pub fn is_ger_registered(
542        ger: ExitRoot,
543        bridge_account: &Account,
544    ) -> Result<bool, AgglayerBridgeError> {
545        // check that the provided account is a bridge account
546        Self::assert_bridge_account(bridge_account)?;
547
548        // Compute the expected GER hash: poseidon2::merge(GER_LOWER, GER_UPPER)
549        let ger_lower: Word = ger.to_elements()[0..4].try_into().unwrap();
550        let ger_upper: Word = ger.to_elements()[4..8].try_into().unwrap();
551        let ger_hash = Poseidon2::merge(&[ger_lower, ger_upper]);
552
553        // Get the value stored by the GER hash. If this GER was registered, the value would be
554        // equal to [1, 0, 0, 0]
555        let stored_value = bridge_account
556            .storage()
557            .get_map_item(AggLayerBridge::ger_map_slot_name(), StorageMapKey::from_raw(ger_hash))
558            .expect("provided account should have AggLayer Bridge specific storage slots");
559
560        if stored_value == Self::REGISTERED_GER_MAP_VALUE {
561            Ok(true)
562        } else {
563            Ok(false)
564        }
565    }
566
567    /// Reads the Local Exit Root (double-word) from the bridge account's storage.
568    ///
569    /// The Local Exit Root is stored in two dedicated value slots:
570    /// - [`AggLayerBridge::let_root_lo_slot_name`] — low word of the root
571    /// - [`AggLayerBridge::let_root_hi_slot_name`] — high word of the root
572    ///
573    /// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo`, followed by the 4
574    /// elements of `root_hi`. For an empty/uninitialized tree, all elements are zeros.
575    ///
576    /// # Errors
577    ///
578    /// Returns an error if:
579    /// - the provided account is not an [`AggLayerBridge`] account.
580    pub fn read_local_exit_root(account: &Account) -> Result<Vec<Felt>, AgglayerBridgeError> {
581        // check that the provided account is a bridge account
582        Self::assert_bridge_account(account)?;
583
584        let root_lo_slot = AggLayerBridge::let_root_lo_slot_name();
585        let root_hi_slot = AggLayerBridge::let_root_hi_slot_name();
586
587        let root_lo = account
588            .storage()
589            .get_item(root_lo_slot)
590            .expect("should be able to read LET root lo");
591        let root_hi = account
592            .storage()
593            .get_item(root_hi_slot)
594            .expect("should be able to read LET root hi");
595
596        let mut root = Vec::with_capacity(8);
597        root.extend(root_lo.to_vec());
598        root.extend(root_hi.to_vec());
599
600        Ok(root)
601    }
602
603    /// Returns the AggLayer network ID stored in the bridge account.
604    ///
605    /// # Errors
606    ///
607    /// Returns an error if:
608    /// - the provided account is not an [`AggLayerBridge`] account.
609    pub fn network_id(account: &Account) -> Result<u32, AgglayerBridgeError> {
610        // check that the provided account is a bridge account
611        Self::assert_bridge_account(account)?;
612
613        let value = account
614            .storage()
615            .get_item(AggLayerBridge::network_id_slot_name())
616            .expect("should be able to read the network ID");
617        let network_id = u32::try_from(value.to_vec()[0].as_canonical_u64())
618            .map_err(|_| AgglayerBridgeError::InvalidNetworkId)?;
619
620        Ok(network_id)
621    }
622
623    /// Returns the number of leaves in the Local Exit Tree (LET) frontier.
624    pub fn read_let_num_leaves(account: &Account) -> u64 {
625        let num_leaves_slot = AggLayerBridge::let_num_leaves_slot_name();
626        let value = account
627            .storage()
628            .get_item(num_leaves_slot)
629            .expect("should be able to read LET num leaves");
630        value.to_vec()[0].as_canonical_u64()
631    }
632
633    /// Returns the claimed global index (CGI) chain hash from the corresponding storage slot.
634    ///
635    /// # Errors
636    ///
637    /// Returns an error if:
638    /// - the provided account is not an [`AggLayerBridge`] account.
639    pub fn cgi_chain_hash(
640        bridge_account: &Account,
641    ) -> Result<crate::claim_note::CgiChainHash, AgglayerBridgeError> {
642        // check that the provided account is a bridge account
643        Self::assert_bridge_account(bridge_account)?;
644
645        let cgi_chain_hash_lo = bridge_account
646            .storage()
647            .get_item(AggLayerBridge::cgi_chain_hash_lo_slot_name())
648            .expect("failed to get CGI hash chain lo slot");
649        let cgi_chain_hash_hi = bridge_account
650            .storage()
651            .get_item(AggLayerBridge::cgi_chain_hash_hi_slot_name())
652            .expect("failed to get CGI hash chain hi slot");
653
654        Ok(crate::claim_note::CgiChainHash::new(Self::chain_hash_bytes(
655            cgi_chain_hash_lo,
656            cgi_chain_hash_hi,
657        )))
658    }
659
660    /// Returns the removed-GER keccak256 hash chain from the corresponding storage slots.
661    ///
662    /// The chain is the running keccak256 of all removed GERs:
663    /// `chain_n = keccak256(chain_{n-1} || removed_ger_n)` with `chain_0 = 0...0`.
664    ///
665    /// # Errors
666    ///
667    /// Returns an error if:
668    /// - the provided account is not an [`AggLayerBridge`] account.
669    pub fn removed_ger_hash_chain(
670        bridge_account: &Account,
671    ) -> Result<RemovedGerHashChain, AgglayerBridgeError> {
672        // check that the provided account is a bridge account
673        Self::assert_bridge_account(bridge_account)?;
674
675        let chain_lo = bridge_account
676            .storage()
677            .get_item(AggLayerBridge::removed_ger_hash_chain_lo_slot_name())
678            .expect("failed to get removed GER hash chain lo slot");
679        let chain_hi = bridge_account
680            .storage()
681            .get_item(AggLayerBridge::removed_ger_hash_chain_hi_slot_name())
682            .expect("failed to get removed GER hash chain hi slot");
683
684        Ok(RemovedGerHashChain::new(Self::chain_hash_bytes(chain_lo, chain_hi)))
685    }
686
687    // HELPER FUNCTIONS
688    // --------------------------------------------------------------------------------------------
689
690    /// Converts a keccak256 hash stored across two lo/hi storage words into its 32-byte form.
691    fn chain_hash_bytes(lo: Word, hi: Word) -> [u8; 32] {
692        lo.iter()
693            .chain(hi.iter())
694            .flat_map(|felt| {
695                (u32::try_from(felt.as_canonical_u64()).expect("Felt value does not fit into u32"))
696                    .to_le_bytes()
697            })
698            .collect::<Vec<u8>>()
699            .try_into()
700            .expect("keccak hash should consist of exactly 32 bytes")
701    }
702
703    /// Checks that the provided account is an [`AggLayerBridge`] account.
704    ///
705    /// # Errors
706    ///
707    /// Returns an error if:
708    /// - the provided account does not have all AggLayer Bridge specific storage slots.
709    /// - the code commitment of the provided account does not match the code commitment of the
710    ///   [`AggLayerBridge`].
711    fn assert_bridge_account(account: &Account) -> Result<(), AgglayerBridgeError> {
712        // check that the storage slots are as expected
713        Self::assert_storage_slots(account)?;
714
715        // check that the code commitment matches the code commitment of the bridge account
716        Self::assert_code_commitment(account)?;
717
718        Ok(())
719    }
720
721    /// Checks that the provided account has all storage slots required for the [`AggLayerBridge`].
722    ///
723    /// # Errors
724    ///
725    /// Returns an error if:
726    /// - provided account does not have all AggLayer Bridge specific storage slots.
727    fn assert_storage_slots(account: &Account) -> Result<(), AgglayerBridgeError> {
728        // get the storage slot names of the provided account
729        let account_storage_slot_names: Vec<&StorageSlotName> = account
730            .storage()
731            .slots()
732            .iter()
733            .map(|storage_slot| storage_slot.name())
734            .collect::<Vec<&StorageSlotName>>();
735
736        // check that all bridge specific storage slots are presented in the provided account
737        let are_slots_present = Self::slot_names()
738            .iter()
739            .all(|slot_name| account_storage_slot_names.contains(slot_name));
740        if !are_slots_present {
741            return Err(AgglayerBridgeError::StorageSlotsMismatch);
742        }
743
744        Ok(())
745    }
746
747    /// Checks that the code commitment of the provided account matches the code commitment of the
748    /// [`AggLayerBridge`].
749    ///
750    /// # Errors
751    ///
752    /// Returns an error if:
753    /// - the code commitment of the provided account does not match the code commitment of the
754    ///   [`AggLayerBridge`].
755    fn assert_code_commitment(account: &Account) -> Result<(), AgglayerBridgeError> {
756        if BRIDGE_CODE_COMMITMENT != account.code().commitment() {
757            return Err(AgglayerBridgeError::CodeCommitmentMismatch);
758        }
759
760        Ok(())
761    }
762
763    /// Returns a vector of all storage slot names a bridge account must have.
764    ///
765    /// Besides the [`AggLayerBridge`] component's own slots, this includes the standards-owned
766    /// `is_paused` slot: `pausable::assert_not_paused` treats a missing slot as unpaused, so this
767    /// validator certifies the slot exists. (In production the slot is guaranteed by
768    /// `AggLayerBridge::account_builder` always installing the `Pausable` component.)
769    fn slot_names() -> Vec<&'static StorageSlotName> {
770        vec![
771            &*GER_MAP_SLOT_NAME,
772            &*LET_FRONTIER_SLOT_NAME,
773            &*LET_ROOT_LO_SLOT_NAME,
774            &*LET_ROOT_HI_SLOT_NAME,
775            &*LET_NUM_LEAVES_SLOT_NAME,
776            &*FAUCET_REGISTRY_MAP_SLOT_NAME,
777            &*TOKEN_REGISTRY_MAP_SLOT_NAME,
778            &*FAUCET_METADATA_MAP_SLOT_NAME,
779            &*REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME,
780            &*REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME,
781            &*CGI_CHAIN_HASH_LO_SLOT_NAME,
782            &*CGI_CHAIN_HASH_HI_SLOT_NAME,
783            &*CLAIM_NULLIFIERS_SLOT_NAME,
784            &*NETWORK_ID_SLOT_NAME,
785            PausableStorage::is_paused_slot(),
786        ]
787    }
788}
789
790impl From<AggLayerBridge> for AccountComponent {
791    fn from(bridge: AggLayerBridge) -> Self {
792        let bridge_storage_slots = vec![
793            StorageSlot::with_empty_map(GER_MAP_SLOT_NAME.clone()),
794            StorageSlot::with_empty_map(LET_FRONTIER_SLOT_NAME.clone()),
795            StorageSlot::with_value(LET_ROOT_LO_SLOT_NAME.clone(), Word::empty()),
796            StorageSlot::with_value(LET_ROOT_HI_SLOT_NAME.clone(), Word::empty()),
797            StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()),
798            StorageSlot::with_empty_map(FAUCET_REGISTRY_MAP_SLOT_NAME.clone()),
799            StorageSlot::with_empty_map(TOKEN_REGISTRY_MAP_SLOT_NAME.clone()),
800            StorageSlot::with_empty_map(FAUCET_METADATA_MAP_SLOT_NAME.clone()),
801            StorageSlot::with_value(REMOVED_GER_HASH_CHAIN_LO_SLOT_NAME.clone(), Word::empty()),
802            StorageSlot::with_value(REMOVED_GER_HASH_CHAIN_HI_SLOT_NAME.clone(), Word::empty()),
803            StorageSlot::with_value(CGI_CHAIN_HASH_LO_SLOT_NAME.clone(), Word::empty()),
804            StorageSlot::with_value(CGI_CHAIN_HASH_HI_SLOT_NAME.clone(), Word::empty()),
805            StorageSlot::with_empty_map(CLAIM_NULLIFIERS_SLOT_NAME.clone()),
806            StorageSlot::with_value(
807                NETWORK_ID_SLOT_NAME.clone(),
808                Word::new([Felt::from(bridge.network_id), Felt::ZERO, Felt::ZERO, Felt::ZERO]),
809            ),
810        ];
811        bridge_component(bridge_storage_slots)
812    }
813}
814
815// AGGLAYER BRIDGE ERROR
816// ================================================================================================
817
818/// AggLayer Bridge related errors.
819#[derive(Debug, Error)]
820pub enum AgglayerBridgeError {
821    #[error(
822        "provided account does not have storage slots required for the AggLayer Bridge account"
823    )]
824    StorageSlotsMismatch,
825    #[error(
826        "the code commitment of the provided account does not match the code commitment of the AggLayer Bridge account"
827    )]
828    CodeCommitmentMismatch,
829    #[error("bridge role {0} must have at least one initial holder")]
830    EmptyBridgeRole(RoleSymbol),
831    #[error("the network ID stored in the bridge account does not fit into a u32")]
832    InvalidNetworkId,
833    #[error("bridge account must be public to be named by a network account target")]
834    NonPublicPauseNoteTarget(#[source] NetworkAccountTargetError),
835    #[error("failed to create a PAUSE_CONFIG note for the bridge account")]
836    PauseNoteCreationFailed(#[source] NoteError),
837}
838
839// HELPER FUNCTIONS
840// ================================================================================================
841
842/// Creates an AggLayer Bridge component with the specified storage slots.
843fn bridge_component(storage_slots: Vec<StorageSlot>) -> AccountComponent {
844    let package = agglayer_bridge_component_package();
845    let metadata = AccountComponentMetadata::new("agglayer::bridge")
846        .with_description("Bridge component for AggLayer");
847
848    AccountComponent::new(package, storage_slots, metadata)
849        .expect("bridge component should satisfy the requirements of a valid account component")
850}