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