Skip to main content

miden_agglayer/
bridge.rs

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