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