Skip to main content

miden_agglayer/
bridge.rs

1extern crate alloc;
2
3use alloc::collections::BTreeSet;
4use alloc::vec;
5use alloc::vec::Vec;
6
7use miden_core::{Felt, ONE, Word, ZERO};
8use miden_protocol::account::component::AccountComponentMetadata;
9use miden_protocol::account::{Account, AccountComponent, AccountId, StorageSlot, StorageSlotName};
10use miden_protocol::block::account_tree::AccountIdKey;
11use miden_protocol::crypto::hash::poseidon2::Poseidon2;
12use miden_protocol::note::NoteScriptRoot;
13use miden_utils_sync::LazyLock;
14use thiserror::Error;
15
16use super::agglayer_bridge_component_library;
17use crate::claim_note::CgiChainHash;
18pub use crate::{
19    B2AggNote,
20    ClaimNote,
21    ClaimNoteStorage,
22    ConfigAggBridgeNote,
23    EthAddress,
24    EthAmount,
25    EthAmountError,
26    EthEmbeddedAccountId,
27    ExitRoot,
28    GlobalIndex,
29    GlobalIndexError,
30    LeafData,
31    MetadataHash,
32    ProofData,
33    SmtNode,
34    UpdateGerNote,
35};
36
37// CONSTANTS
38// ================================================================================================
39// Include the generated agglayer constants
40include!(concat!(env!("OUT_DIR"), "/agglayer_constants.rs"));
41
42// AGGLAYER BRIDGE STRUCT
43// ================================================================================================
44
45// bridge config
46// ------------------------------------------------------------------------------------------------
47
48static BRIDGE_ADMIN_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
49    StorageSlotName::new("agglayer::bridge::admin_account_id")
50        .expect("bridge admin account ID storage slot name should be valid")
51});
52static GER_MANAGER_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
53    StorageSlotName::new("agglayer::bridge::ger_manager_account_id")
54        .expect("GER manager account ID storage slot name should be valid")
55});
56static GER_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
57    StorageSlotName::new("agglayer::bridge::ger_map")
58        .expect("GER map storage slot name should be valid")
59});
60static FAUCET_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
61    StorageSlotName::new("agglayer::bridge::faucet_registry_map")
62        .expect("faucet registry map storage slot name should be valid")
63});
64static TOKEN_REGISTRY_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
65    StorageSlotName::new("agglayer::bridge::token_registry_map")
66        .expect("token registry map storage slot name should be valid")
67});
68static FAUCET_METADATA_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
69    StorageSlotName::new("agglayer::bridge::faucet_metadata_map")
70        .expect("faucet metadata map storage slot name should be valid")
71});
72static NETWORK_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
73    StorageSlotName::new("agglayer::bridge::network_id")
74        .expect("network ID storage slot name should be valid")
75});
76
77// bridge in
78// ------------------------------------------------------------------------------------------------
79
80static CLAIM_NULLIFIERS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
81    StorageSlotName::new("agglayer::bridge::claim_nullifiers")
82        .expect("claim nullifiers storage slot name should be valid")
83});
84static CGI_CHAIN_HASH_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
85    StorageSlotName::new("agglayer::bridge::cgi_chain_hash_lo")
86        .expect("CGI chain hash_lo storage slot name should be valid")
87});
88static CGI_CHAIN_HASH_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
89    StorageSlotName::new("agglayer::bridge::cgi_chain_hash_hi")
90        .expect("CGI chain hash_hi storage slot name should be valid")
91});
92
93// bridge out
94// ------------------------------------------------------------------------------------------------
95
96static LET_FRONTIER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
97    StorageSlotName::new("agglayer::bridge::let_frontier")
98        .expect("LET frontier storage slot name should be valid")
99});
100static LET_ROOT_LO_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
101    StorageSlotName::new("agglayer::bridge::let_root_lo")
102        .expect("LET root_lo storage slot name should be valid")
103});
104static LET_ROOT_HI_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
105    StorageSlotName::new("agglayer::bridge::let_root_hi")
106        .expect("LET root_hi storage slot name should be valid")
107});
108static LET_NUM_LEAVES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
109    StorageSlotName::new("agglayer::bridge::let_num_leaves")
110        .expect("LET num_leaves storage slot name should be valid")
111});
112
113/// An [`AccountComponent`] implementing the AggLayer Bridge.
114///
115/// It reexports the procedures from `agglayer::bridge`. When linking against this
116/// component, the `agglayer` library must be available to the assembler.
117/// The procedures of this component are:
118/// - `register_faucet`, which registers a faucet in the bridge.
119/// - `update_ger`, which injects a new GER into the storage map.
120/// - `bridge_out`, which bridges an asset out of Miden to the destination network.
121/// - `claim`, which validates a claim against the AggLayer bridge and creates a MINT note for the
122///   AggLayer Faucet.
123///
124/// ## Storage Layout
125///
126/// - [`Self::bridge_admin_id_slot_name`]: Stores the bridge admin account ID.
127/// - [`Self::ger_manager_id_slot_name`]: Stores the GER manager account ID.
128/// - [`Self::ger_map_slot_name`]: Stores the GERs.
129/// - [`Self::faucet_registry_map_slot_name`]: Stores the faucet registry map.
130/// - [`Self::token_registry_map_slot_name`]: Stores the token address → faucet ID map.
131/// - [`Self::faucet_metadata_map_slot_name`]: Stores conversion metadata (origin address, origin
132///   network, scale, metadata hash) for all registered faucets, keyed by sub-key scheme based on
133///   faucet ID.
134/// - [`Self::network_id_slot_name`]: Stores the bridge's AggLayer network ID.
135/// - [`Self::claim_nullifiers_slot_name`]: Stores the CLAIM note nullifiers map (RPO(leaf_index,
136///   source_bridge_network) → \[1, 0, 0, 0\]).
137/// - [`Self::cgi_chain_hash_lo_slot_name`]: Stores the lower 128 bits of the CGI chain hash.
138/// - [`Self::cgi_chain_hash_hi_slot_name`]: Stores the upper 128 bits of the CGI chain hash.
139/// - [`Self::let_frontier_slot_name`]: Stores the Local Exit Tree (LET) frontier.
140/// - [`Self::let_root_lo_slot_name`]: Stores the lower 128 bits of the LET root.
141/// - [`Self::let_root_hi_slot_name`]: Stores the upper 128 bits of the LET root.
142/// - [`Self::let_num_leaves_slot_name`]: Stores the number of leaves in the LET frontier.
143///
144/// The bridge starts with an empty faucet registry; faucets are registered at runtime via
145/// CONFIG_AGG_BRIDGE notes.
146///
147/// Claim validation compares the leaf's `destination_network` to the bridge's own network ID,
148/// which is stored in [`Self::network_id_slot_name`] at account creation and read at runtime by
149/// the bridge MASM. The network ID is set once and never mutated, so different deployments (e.g.
150/// testnet vs mainnet) can use different IDs.
151#[derive(Debug, Clone)]
152pub struct AggLayerBridge {
153    bridge_admin_id: AccountId,
154    ger_manager_id: AccountId,
155    network_id: u32,
156}
157
158impl AggLayerBridge {
159    // CONSTANTS
160    // --------------------------------------------------------------------------------------------
161
162    const REGISTERED_GER_MAP_VALUE: Word = Word::new([ONE, ZERO, ZERO, ZERO]);
163
164    // CONSTRUCTORS
165    // --------------------------------------------------------------------------------------------
166
167    /// Creates a new AggLayer bridge component with the standard configuration.
168    ///
169    /// `network_id` is the AggLayer network ID assigned to the Miden chain.
170    pub fn new(bridge_admin_id: AccountId, ger_manager_id: AccountId, network_id: u32) -> Self {
171        Self {
172            bridge_admin_id,
173            ger_manager_id,
174            network_id,
175        }
176    }
177
178    // PUBLIC ACCESSORS
179    // --------------------------------------------------------------------------------------------
180
181    // --- bridge config ----
182
183    /// Storage slot name for the bridge admin account ID.
184    pub fn bridge_admin_id_slot_name() -> &'static StorageSlotName {
185        &BRIDGE_ADMIN_ID_SLOT_NAME
186    }
187
188    /// Storage slot name for the GER manager account ID.
189    pub fn ger_manager_id_slot_name() -> &'static StorageSlotName {
190        &GER_MANAGER_ID_SLOT_NAME
191    }
192
193    /// Storage slot name for the GERs map.
194    pub fn ger_map_slot_name() -> &'static StorageSlotName {
195        &GER_MAP_SLOT_NAME
196    }
197
198    /// Storage slot name for the faucet registry map.
199    pub fn faucet_registry_map_slot_name() -> &'static StorageSlotName {
200        &FAUCET_REGISTRY_MAP_SLOT_NAME
201    }
202
203    /// Storage slot name for the token registry map.
204    pub fn token_registry_map_slot_name() -> &'static StorageSlotName {
205        &TOKEN_REGISTRY_MAP_SLOT_NAME
206    }
207
208    /// Storage slot name for the faucet metadata map.
209    ///
210    /// This map stores conversion metadata (origin address, origin network, scale, metadata hash)
211    /// for all registered faucets, keyed by sub-key scheme based on faucet ID.
212    pub fn faucet_metadata_map_slot_name() -> &'static StorageSlotName {
213        &FAUCET_METADATA_MAP_SLOT_NAME
214    }
215
216    /// Storage slot name for the bridge's AggLayer network ID.
217    ///
218    /// Holds the network ID assigned to this bridge as a single felt in the first word element.
219    /// It is set at account creation and never mutated by any bridge procedure.
220    pub fn network_id_slot_name() -> &'static StorageSlotName {
221        &NETWORK_ID_SLOT_NAME
222    }
223
224    // --- bridge in --------
225
226    /// Storage slot name for the CLAIM note nullifiers map.
227    pub fn claim_nullifiers_slot_name() -> &'static StorageSlotName {
228        &CLAIM_NULLIFIERS_SLOT_NAME
229    }
230
231    /// Storage slot name for the lower 128 bits of the CGI chain hash.
232    pub fn cgi_chain_hash_lo_slot_name() -> &'static StorageSlotName {
233        &CGI_CHAIN_HASH_LO_SLOT_NAME
234    }
235
236    /// Storage slot name for the upper 128 bits of the CGI chain hash.
237    pub fn cgi_chain_hash_hi_slot_name() -> &'static StorageSlotName {
238        &CGI_CHAIN_HASH_HI_SLOT_NAME
239    }
240
241    // --- bridge out -------
242
243    /// Storage slot name for the Local Exit Tree (LET) frontier.
244    pub fn let_frontier_slot_name() -> &'static StorageSlotName {
245        &LET_FRONTIER_SLOT_NAME
246    }
247
248    /// Storage slot name for the lower 32 bits of the LET root.
249    pub fn let_root_lo_slot_name() -> &'static StorageSlotName {
250        &LET_ROOT_LO_SLOT_NAME
251    }
252
253    /// Storage slot name for the upper 32 bits of the LET root.
254    pub fn let_root_hi_slot_name() -> &'static StorageSlotName {
255        &LET_ROOT_HI_SLOT_NAME
256    }
257
258    /// Storage slot name for the number of leaves in the LET frontier.
259    pub fn let_num_leaves_slot_name() -> &'static StorageSlotName {
260        &LET_NUM_LEAVES_SLOT_NAME
261    }
262
263    // ALLOWED NOTES
264    // --------------------------------------------------------------------------------------------
265
266    /// Returns the set of input-note script roots that AggLayer bridge accounts accept.
267    ///
268    /// The bridge's [`AuthNetworkAccount`] component is initialized with this allowlist, which
269    /// means any transaction consuming a note outside this set is rejected before reaching
270    /// `output_note::create`.
271    ///
272    /// [`AuthNetworkAccount`]: miden_standards::account::auth::AuthNetworkAccount
273    pub fn allowed_notes() -> BTreeSet<NoteScriptRoot> {
274        BTreeSet::from([
275            ClaimNote::script_root(),
276            B2AggNote::script_root(),
277            ConfigAggBridgeNote::script_root(),
278            UpdateGerNote::script_root(),
279        ])
280    }
281
282    /// Returns a boolean indicating whether the provided GER is present in storage of the provided
283    /// bridge account.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if:
288    /// - the provided account is not an [`AggLayerBridge`] account.
289    pub fn is_ger_registered(
290        ger: ExitRoot,
291        bridge_account: &Account,
292    ) -> Result<bool, AgglayerBridgeError> {
293        // check that the provided account is a bridge account
294        Self::assert_bridge_account(bridge_account)?;
295
296        // Compute the expected GER hash: poseidon2::merge(GER_LOWER, GER_UPPER)
297        let ger_lower: Word = ger.to_elements()[0..4].try_into().unwrap();
298        let ger_upper: Word = ger.to_elements()[4..8].try_into().unwrap();
299        let ger_hash = Poseidon2::merge(&[ger_lower, ger_upper]);
300
301        // Get the value stored by the GER hash. If this GER was registered, the value would be
302        // equal to [1, 0, 0, 0]
303        let stored_value = bridge_account
304            .storage()
305            .get_map_item(AggLayerBridge::ger_map_slot_name(), ger_hash)
306            .expect("provided account should have AggLayer Bridge specific storage slots");
307
308        if stored_value == Self::REGISTERED_GER_MAP_VALUE {
309            Ok(true)
310        } else {
311            Ok(false)
312        }
313    }
314
315    /// Reads the Local Exit Root (double-word) from the bridge account's storage.
316    ///
317    /// The Local Exit Root is stored in two dedicated value slots:
318    /// - [`AggLayerBridge::let_root_lo_slot_name`] — low word of the root
319    /// - [`AggLayerBridge::let_root_hi_slot_name`] — high word of the root
320    ///
321    /// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo`, followed by the 4
322    /// elements of `root_hi`. For an empty/uninitialized tree, all elements are zeros.
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if:
327    /// - the provided account is not an [`AggLayerBridge`] account.
328    pub fn read_local_exit_root(account: &Account) -> Result<Vec<Felt>, AgglayerBridgeError> {
329        // check that the provided account is a bridge account
330        Self::assert_bridge_account(account)?;
331
332        let root_lo_slot = AggLayerBridge::let_root_lo_slot_name();
333        let root_hi_slot = AggLayerBridge::let_root_hi_slot_name();
334
335        let root_lo = account
336            .storage()
337            .get_item(root_lo_slot)
338            .expect("should be able to read LET root lo");
339        let root_hi = account
340            .storage()
341            .get_item(root_hi_slot)
342            .expect("should be able to read LET root hi");
343
344        let mut root = Vec::with_capacity(8);
345        root.extend(root_lo.to_vec());
346        root.extend(root_hi.to_vec());
347
348        Ok(root)
349    }
350
351    /// Returns the AggLayer network ID stored in the bridge account.
352    ///
353    /// # Errors
354    ///
355    /// Returns an error if:
356    /// - the provided account is not an [`AggLayerBridge`] account.
357    pub fn network_id(account: &Account) -> Result<u32, AgglayerBridgeError> {
358        // check that the provided account is a bridge account
359        Self::assert_bridge_account(account)?;
360
361        let value = account
362            .storage()
363            .get_item(AggLayerBridge::network_id_slot_name())
364            .expect("should be able to read the network ID");
365        let network_id = u32::try_from(value.to_vec()[0].as_canonical_u64())
366            .map_err(|_| AgglayerBridgeError::InvalidNetworkId)?;
367
368        Ok(network_id)
369    }
370
371    /// Returns the number of leaves in the Local Exit Tree (LET) frontier.
372    pub fn read_let_num_leaves(account: &Account) -> u64 {
373        let num_leaves_slot = AggLayerBridge::let_num_leaves_slot_name();
374        let value = account
375            .storage()
376            .get_item(num_leaves_slot)
377            .expect("should be able to read LET num leaves");
378        value.to_vec()[0].as_canonical_u64()
379    }
380
381    /// Returns the claimed global index (CGI) chain hash from the corresponding storage slot.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error if:
386    /// - the provided account is not an [`AggLayerBridge`] account.
387    pub fn cgi_chain_hash(bridge_account: &Account) -> Result<CgiChainHash, AgglayerBridgeError> {
388        // check that the provided account is a bridge account
389        Self::assert_bridge_account(bridge_account)?;
390
391        let cgi_chain_hash_lo = bridge_account
392            .storage()
393            .get_item(AggLayerBridge::cgi_chain_hash_lo_slot_name())
394            .expect("failed to get CGI hash chain lo slot");
395        let cgi_chain_hash_hi = bridge_account
396            .storage()
397            .get_item(AggLayerBridge::cgi_chain_hash_hi_slot_name())
398            .expect("failed to get CGI hash chain hi slot");
399
400        let cgi_chain_hash_bytes = cgi_chain_hash_lo
401            .iter()
402            .chain(cgi_chain_hash_hi.iter())
403            .flat_map(|felt| {
404                (u32::try_from(felt.as_canonical_u64()).expect("Felt value does not fit into u32"))
405                    .to_le_bytes()
406            })
407            .collect::<Vec<u8>>();
408
409        Ok(CgiChainHash::new(
410            cgi_chain_hash_bytes
411                .try_into()
412                .expect("keccak hash should consist of exactly 32 bytes"),
413        ))
414    }
415
416    // HELPER FUNCTIONS
417    // --------------------------------------------------------------------------------------------
418
419    /// Checks that the provided account is an [`AggLayerBridge`] account.
420    ///
421    /// # Errors
422    ///
423    /// Returns an error if:
424    /// - the provided account does not have all AggLayer Bridge specific storage slots.
425    /// - the code commitment of the provided account does not match the code commitment of the
426    ///   [`AggLayerBridge`].
427    fn assert_bridge_account(account: &Account) -> Result<(), AgglayerBridgeError> {
428        // check that the storage slots are as expected
429        Self::assert_storage_slots(account)?;
430
431        // check that the code commitment matches the code commitment of the bridge account
432        Self::assert_code_commitment(account)?;
433
434        Ok(())
435    }
436
437    /// Checks that the provided account has all storage slots required for the [`AggLayerBridge`].
438    ///
439    /// # Errors
440    ///
441    /// Returns an error if:
442    /// - provided account does not have all AggLayer Bridge specific storage slots.
443    fn assert_storage_slots(account: &Account) -> Result<(), AgglayerBridgeError> {
444        // get the storage slot names of the provided account
445        let account_storage_slot_names: Vec<&StorageSlotName> = account
446            .storage()
447            .slots()
448            .iter()
449            .map(|storage_slot| storage_slot.name())
450            .collect::<Vec<&StorageSlotName>>();
451
452        // check that all bridge specific storage slots are presented in the provided account
453        let are_slots_present = Self::slot_names()
454            .iter()
455            .all(|slot_name| account_storage_slot_names.contains(slot_name));
456        if !are_slots_present {
457            return Err(AgglayerBridgeError::StorageSlotsMismatch);
458        }
459
460        Ok(())
461    }
462
463    /// Checks that the code commitment of the provided account matches the code commitment of the
464    /// [`AggLayerBridge`].
465    ///
466    /// # Errors
467    ///
468    /// Returns an error if:
469    /// - the code commitment of the provided account does not match the code commitment of the
470    ///   [`AggLayerBridge`].
471    fn assert_code_commitment(account: &Account) -> Result<(), AgglayerBridgeError> {
472        if BRIDGE_CODE_COMMITMENT != account.code().commitment() {
473            return Err(AgglayerBridgeError::CodeCommitmentMismatch);
474        }
475
476        Ok(())
477    }
478
479    /// Returns a vector of all [`AggLayerBridge`] storage slot names.
480    fn slot_names() -> Vec<&'static StorageSlotName> {
481        vec![
482            &*GER_MAP_SLOT_NAME,
483            &*LET_FRONTIER_SLOT_NAME,
484            &*LET_ROOT_LO_SLOT_NAME,
485            &*LET_ROOT_HI_SLOT_NAME,
486            &*LET_NUM_LEAVES_SLOT_NAME,
487            &*FAUCET_REGISTRY_MAP_SLOT_NAME,
488            &*TOKEN_REGISTRY_MAP_SLOT_NAME,
489            &*FAUCET_METADATA_MAP_SLOT_NAME,
490            &*BRIDGE_ADMIN_ID_SLOT_NAME,
491            &*GER_MANAGER_ID_SLOT_NAME,
492            &*CGI_CHAIN_HASH_LO_SLOT_NAME,
493            &*CGI_CHAIN_HASH_HI_SLOT_NAME,
494            &*CLAIM_NULLIFIERS_SLOT_NAME,
495            &*NETWORK_ID_SLOT_NAME,
496        ]
497    }
498}
499
500impl From<AggLayerBridge> for AccountComponent {
501    fn from(bridge: AggLayerBridge) -> Self {
502        let bridge_admin_word = AccountIdKey::new(bridge.bridge_admin_id).as_word();
503        let ger_manager_word = AccountIdKey::new(bridge.ger_manager_id).as_word();
504
505        let bridge_storage_slots = vec![
506            StorageSlot::with_empty_map(GER_MAP_SLOT_NAME.clone()),
507            StorageSlot::with_empty_map(LET_FRONTIER_SLOT_NAME.clone()),
508            StorageSlot::with_value(LET_ROOT_LO_SLOT_NAME.clone(), Word::empty()),
509            StorageSlot::with_value(LET_ROOT_HI_SLOT_NAME.clone(), Word::empty()),
510            StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()),
511            StorageSlot::with_empty_map(FAUCET_REGISTRY_MAP_SLOT_NAME.clone()),
512            StorageSlot::with_empty_map(TOKEN_REGISTRY_MAP_SLOT_NAME.clone()),
513            StorageSlot::with_empty_map(FAUCET_METADATA_MAP_SLOT_NAME.clone()),
514            StorageSlot::with_value(BRIDGE_ADMIN_ID_SLOT_NAME.clone(), bridge_admin_word),
515            StorageSlot::with_value(GER_MANAGER_ID_SLOT_NAME.clone(), ger_manager_word),
516            StorageSlot::with_value(CGI_CHAIN_HASH_LO_SLOT_NAME.clone(), Word::empty()),
517            StorageSlot::with_value(CGI_CHAIN_HASH_HI_SLOT_NAME.clone(), Word::empty()),
518            StorageSlot::with_empty_map(CLAIM_NULLIFIERS_SLOT_NAME.clone()),
519            StorageSlot::with_value(
520                NETWORK_ID_SLOT_NAME.clone(),
521                Word::new([Felt::from(bridge.network_id), ZERO, ZERO, ZERO]),
522            ),
523        ];
524        bridge_component(bridge_storage_slots)
525    }
526}
527
528// AGGLAYER BRIDGE ERROR
529// ================================================================================================
530
531/// AggLayer Bridge related errors.
532#[derive(Debug, Error)]
533pub enum AgglayerBridgeError {
534    #[error(
535        "provided account does not have storage slots required for the AggLayer Bridge account"
536    )]
537    StorageSlotsMismatch,
538    #[error(
539        "the code commitment of the provided account does not match the code commitment of the AggLayer Bridge account"
540    )]
541    CodeCommitmentMismatch,
542    #[error("the network ID stored in the bridge account does not fit into a u32")]
543    InvalidNetworkId,
544}
545
546// HELPER FUNCTIONS
547// ================================================================================================
548
549/// Creates an AggLayer Bridge component with the specified storage slots.
550fn bridge_component(storage_slots: Vec<StorageSlot>) -> AccountComponent {
551    let library = agglayer_bridge_component_library();
552    let metadata = AccountComponentMetadata::new("agglayer::bridge")
553        .with_description("Bridge component for AggLayer");
554
555    AccountComponent::new(library, storage_slots, metadata)
556        .expect("bridge component should satisfy the requirements of a valid account component")
557}