diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c548751e0..0d9df6188d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Store transaction reference block number alongside each registered GER in the bridge's GER map ([#2579](https://github.com/0xMiden/protocol/pull/2579)). - Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). - Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)). - Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)). diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 7fc10e19b0..f94d389c21 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -104,7 +104,9 @@ Asserts the note sender matches the bridge admin stored in Asserts the note sender matches the GER manager stored in `miden::agglayer::bridge::ger_manager`, then computes `KEY = rpo256::merge(GER_UPPER, GER_LOWER)` and stores -`KEY -> [1, 0, 0, 0]` in the `ger` map slot. This marks the GER as "known". +`KEY -> [1, block_num, 0, 0]` in the `ger` map slot, where `block_num` is the +transaction's reference block number. This marks the GER as "known" and records +when it was registered. #### `bridge_in::verify_leaf_bridge` @@ -129,7 +131,7 @@ Verifies a bridge-in claim: | Slot name | Slot type | Key encoding | Value encoding | Purpose | |-----------|-----------|-------------|----------------|---------| -| `miden::agglayer::bridge::ger` | Map | `rpo256::merge(GER_UPPER, GER_LOWER)` | `[1, 0, 0, 0]` if known; `[0, 0, 0, 0]` if absent | Known Global Exit Root set | +| `miden::agglayer::bridge::ger` | Map | `rpo256::merge(GER_UPPER, GER_LOWER)` | `[1, block_num, 0, 0]` if known (block_num = reference block at registration); `[0, 0, 0, 0]` if absent | Known Global Exit Root set | | `miden::agglayer::let` | Map | `[h, 0, 0, 0]` and `[h, 1, 0, 0]` (for h = 0..31) | Per index h: two keys yield one double-word (2 words = 8 felts, a Keccak-256 digest). Absent keys return zeros. | Local Exit Tree MMR frontier | | `miden::agglayer::let::root_lo` | Value | -- | `[root_0, root_1, root_2, root_3]` | LET root low word (Keccak-256 lower 16 bytes) | | `miden::agglayer::let::root_hi` | Value | -- | `[root_4, root_5, root_6, root_7]` | LET root high word (Keccak-256 upper 16 bytes) | diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 2e2d80da89..e2af6240ab 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -3,6 +3,7 @@ use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note use miden::protocol::native_account +use miden::protocol::tx # ERRORS # ================================================================================================= @@ -31,7 +32,8 @@ const IS_FAUCET_REGISTERED_FLAG=1 #! Updates the Global Exit Root (GER) in the bridge account storage. #! #! Computes hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) and stores it in a map -#! with value [GER_KNOWN_FLAG, 0, 0, 0] to indicate the GER is known. +#! with value [GER_KNOWN_FLAG, block_num, 0, 0] to indicate the GER is known and +#! record the block at which it was registered. #! #! Panics if the note sender is not the global exit root manager. #! @@ -49,9 +51,11 @@ pub proc update_ger exec.rpo256::merge # => [GER_HASH, pad(12)] - # prepare VALUE = [0, 0, 0, GER_KNOWN_FLAG] - push.GER_KNOWN_FLAG.0.0.0 - # => [0, 0, 0, GER_KNOWN_FLAG, GER_HASH, pad(12)] + # prepare VALUE = [0, 0, block_num, GER_KNOWN_FLAG] + push.GER_KNOWN_FLAG + exec.tx::get_block_number + push.0.0 + # => [0, 0, block_num, GER_KNOWN_FLAG, GER_HASH, pad(12)] swapw # => [GER_HASH, VALUE, pad(12)] @@ -86,13 +90,13 @@ pub proc assert_valid_ger # => [slot_id_prefix, slot_id_suffix, GER_HASH] exec.active_account::get_map_item - # => [VALUE] + # => [0, 0, block_num, GER_KNOWN_FLAG] - # assert the GER is known in storage (VALUE = [0, 0, 0, GER_KNOWN_FLAG]) - push.GER_KNOWN_FLAG.0.0.0 - # => [0, 0, 0, GER_KNOWN_FLAG, VALUE] + # assert the GER is known in storage (check only the flag element) + drop drop drop + # => [GER_KNOWN_FLAG] - assert_eqw.err=ERR_GER_NOT_FOUND + assert.err=ERR_GER_NOT_FOUND # => [] end diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 8dbe916cbe..386338c25f 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -3,9 +3,10 @@ extern crate alloc; use alloc::vec; use alloc::vec::Vec; -use miden_core::{Felt, FieldElement, ONE, Word, ZERO}; +use miden_core::{Felt, FieldElement, ONE, Word}; use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{Account, AccountComponent, AccountId, StorageSlot, StorageSlotName}; +use miden_protocol::block::BlockNumber; use miden_protocol::crypto::hash::rpo::Rpo256; use miden_utils_sync::LazyLock; use thiserror::Error; @@ -102,7 +103,12 @@ impl AggLayerBridge { // CONSTANTS // -------------------------------------------------------------------------------------------- - const REGISTERED_GER_MAP_VALUE: Word = Word::new([ONE, ZERO, ZERO, ZERO]); + /// Index of the GER-known flag within the stored GER map value word. + const GER_FLAG_INDEX: usize = 0; + /// Index of the block number within the stored GER map value word. + const GER_BLOCK_NUM_INDEX: usize = 1; + /// Flag value indicating a GER is registered (matches MASM `GER_KNOWN_FLAG`). + const REGISTERED_GER_FLAG: Felt = ONE; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -158,16 +164,45 @@ impl AggLayerBridge { /// Returns a boolean indicating whether the provided GER is present in storage of the provided /// bridge account. /// + /// The GER map stores `[GER_KNOWN_FLAG, block_num, 0, 0]` for registered GERs, where + /// `block_num` is the reference block number at registration time. This method checks only + /// the flag element. + /// /// # Errors /// /// Returns an error if: /// - the provided account is not an [`AggLayerBridge`] account. pub fn is_ger_registered( ger: ExitRoot, - bridge_account: Account, + bridge_account: &Account, ) -> Result { - // check that the provided account is a bridge account - Self::assert_bridge_account(&bridge_account)?; + let stored_value = Self::get_ger_value(ger, bridge_account)?; + Ok(stored_value[Self::GER_FLAG_INDEX] == Self::REGISTERED_GER_FLAG) + } + + /// Returns the block number at which the provided GER was registered, or `None` if the GER + /// is not registered. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerBridge`] account. + pub fn get_ger_block_number( + ger: ExitRoot, + bridge_account: &Account, + ) -> Result, AgglayerBridgeError> { + let stored_value = Self::get_ger_value(ger, bridge_account)?; + if stored_value[Self::GER_FLAG_INDEX] != Self::REGISTERED_GER_FLAG { + return Ok(None); + } + let block_num = u32::try_from(stored_value[Self::GER_BLOCK_NUM_INDEX].as_int()) + .map_err(|_| AgglayerBridgeError::InvalidBlockNumber)?; + Ok(Some(BlockNumber::from(block_num))) + } + + /// Looks up the raw value stored for the given GER in the bridge account's GER map. + fn get_ger_value(ger: ExitRoot, bridge_account: &Account) -> Result { + Self::assert_bridge_account(bridge_account)?; // Compute the expected GER hash: rpo256::merge(GER_UPPER, GER_LOWER) let mut ger_lower: [Felt; 4] = ger.to_elements()[0..4].try_into().unwrap(); @@ -182,18 +217,12 @@ impl AggLayerBridge { ger_upper.reverse(); let ger_hash = Rpo256::merge(&[ger_upper.into(), ger_lower.into()]); - // Get the value stored by the GER hash. If this GER was registered, the value would be - // equal to [1, 0, 0, 0] let stored_value = bridge_account .storage() .get_map_item(AggLayerBridge::ger_map_slot_name(), ger_hash) .expect("provided account should have AggLayer Bridge specific storage slots"); - if stored_value == Self::REGISTERED_GER_MAP_VALUE { - Ok(true) - } else { - Ok(false) - } + Ok(stored_value) } /// Reads the Local Exit Root (double-word) from the bridge account's storage. @@ -365,6 +394,8 @@ pub enum AgglayerBridgeError { "the code commitment of the provided account does not match the code commitment of the AggLayer Bridge account" )] CodeCommitmentMismatch, + #[error("stored block number exceeds u32::MAX")] + InvalidBlockNumber, } // HELPER FUNCTIONS diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 1d2f560d51..3de93421dd 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -97,9 +97,18 @@ async fn update_ger_note_updates_storage() -> anyhow::Result<()> { let mut updated_bridge_account = bridge_account.clone(); updated_bridge_account.apply_delta(executed_transaction.account_delta())?; - let is_registered = AggLayerBridge::is_ger_registered(ger, updated_bridge_account)?; + let is_registered = AggLayerBridge::is_ger_registered(ger, &updated_bridge_account)?; assert!(is_registered, "GER was not registered in the bridge account"); + // Verify the stored block number matches the transaction's reference block + let block_num = AggLayerBridge::get_ger_block_number(ger, &updated_bridge_account)? + .expect("GER should have a block number stored"); + assert_eq!( + block_num, + mock_chain.latest_block_header().block_num(), + "stored block number should match the transaction's reference block" + ); + Ok(()) }