From b8c256dbface02baef887865c8a75ff4829d11dc Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 6 Feb 2026 14:35:34 +0100 Subject: [PATCH 01/21] fix(AggLayer): use correct byte<>`Felt` conversion (#2387) * chore: unit test for compute_ger * fix: use LE byte<>felt conversion * chore: inline bytes32_to_felts into callers * fixup! fix: use LE byte<>felt conversion * Update crates/miden-testing/tests/agglayer/update_ger.rs * chore: unify underlying repr, use type aliases * changelog * Update crates/miden-agglayer/asm/bridge/crypto_utils.masm Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --------- Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- CHANGELOG.md | 4 + crates/miden-agglayer/Cargo.toml | 1 + crates/miden-agglayer/src/claim_note.rs | 48 +++------ crates/miden-agglayer/src/utils.rs | 13 --- .../tests/agglayer/test_utils.rs | 6 +- .../tests/agglayer/update_ger.rs | 99 ++++++++++++++++++- 6 files changed, 119 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a21848a0..3cac1a1a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ - [BREAKING] Updated note tag length to support up to 32 bits ([#2329](https://github.com/0xMiden/miden-base/pull/2329)). - [BREAKING] Moved standard note code into individual note modules ([#2363](https://github.com/0xMiden/miden-base/pull/2363)). - [BREAKING] Added `miden::standards::note_tag` module for account target note tags ([#2366](https://github.com/0xMiden/miden-base/pull/2366)). +- Unified the underlying representation of `ExitRoot` and `SmtNode` and use type aliases ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). + +### Fixes +- Fixed byte-to-felt conversion for `ExitRoot` and `SmtNode`'s `to_elements()` method ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). ## 0.13.3 (2026-01-27) diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml index 7541b7ea8d..65b0e3e081 100644 --- a/crates/miden-agglayer/Cargo.toml +++ b/crates/miden-agglayer/Cargo.toml @@ -23,6 +23,7 @@ testing = ["miden-protocol/testing"] # Miden dependencies miden-assembly = { workspace = true } miden-core = { workspace = true } +miden-core-lib = { workspace = true } miden-protocol = { workspace = true } miden-standards = { workspace = true } miden-utils-sync = { workspace = true } diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 0e919fd28f..75b2e65720 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -3,6 +3,7 @@ use alloc::vec; use alloc::vec::Vec; use miden_core::{Felt, FieldElement, Word}; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::account::AccountId; use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; @@ -18,18 +19,17 @@ use miden_protocol::note::{ }; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; -use crate::utils::bytes32_to_felts; use crate::{EthAddressFormat, EthAmount, claim_script}; // CLAIM NOTE STRUCTURES // ================================================================================================ -/// SMT node representation (32-byte hash) +/// Keccak256 output representation (32-byte hash) #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SmtNode([u8; 32]); +pub struct Keccak256Output([u8; 32]); -impl SmtNode { - /// Creates a new SMT node from a 32-byte array +impl Keccak256Output { + /// Creates a new Keccak256 output from a 32-byte array pub fn new(bytes: [u8; 32]) -> Self { Self(bytes) } @@ -39,44 +39,24 @@ impl SmtNode { &self.0 } - /// Converts the SMT node to 8 Felt elements (32-byte value as 8 u32 values in big-endian) - pub fn to_elements(&self) -> [Felt; 8] { - bytes32_to_felts(&self.0) + /// Converts the Keccak256 output to 8 Felt elements (32-byte value as 8 u32 values in + /// little-endian) + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) } } -impl From<[u8; 32]> for SmtNode { +impl From<[u8; 32]> for Keccak256Output { fn from(bytes: [u8; 32]) -> Self { Self::new(bytes) } } -/// Exit root representation (32-byte hash) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ExitRoot([u8; 32]); - -impl ExitRoot { - /// Creates a new exit root from a 32-byte array - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - /// Returns the inner 32-byte array - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } - - /// Converts the exit root to 8 Felt elements - pub fn to_elements(&self) -> [Felt; 8] { - bytes32_to_felts(&self.0) - } -} +/// SMT node representation (32-byte Keccak256 hash) +pub type SmtNode = Keccak256Output; -impl From<[u8; 32]> for ExitRoot { - fn from(bytes: [u8; 32]) -> Self { - Self::new(bytes) - } -} +/// Exit root representation (32-byte Keccak256 hash) +pub type ExitRoot = Keccak256Output; /// Proof data for CLAIM note creation. /// Contains SMT proofs and root hashes using typed representations. diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs index 88850de58c..9268722129 100644 --- a/crates/miden-agglayer/src/utils.rs +++ b/crates/miden-agglayer/src/utils.rs @@ -1,21 +1,8 @@ -use miden_core::FieldElement; use miden_protocol::Felt; // UTILITY FUNCTIONS // ================================================================================================ -/// Converts a bytes32 value (32 bytes) into an array of 8 Felt values. -/// -/// Note: These utility functions will eventually be replaced with similar functions from miden-vm. -pub fn bytes32_to_felts(bytes32: &[u8; 32]) -> [Felt; 8] { - let mut result = [Felt::ZERO; 8]; - for (i, chunk) in bytes32.chunks(4).enumerate() { - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result[i] = Felt::from(value); - } - result -} - /// Convert 8 Felt values (u32 limbs in little-endian order) to U256 bytes in little-endian format. pub fn felts_to_u256_bytes(limbs: [Felt; 8]) -> [u8; 32] { let mut bytes = [0u8; 32]; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index b77d99e1bf..b0d28cea6f 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -91,9 +91,9 @@ pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { let global_index = [0u32, 0, 0, 0, 0, 1, 0, 2]; let mainnet_exit_root: [u8; 32] = [ - 0xe3, 0xd3, 0x3b, 0x7e, 0x1f, 0x64, 0xb4, 0x04, 0x47, 0x2f, 0x53, 0xd1, 0xe4, 0x56, 0xc9, - 0xfa, 0x02, 0x47, 0x03, 0x13, 0x72, 0xa3, 0x08, 0x0f, 0x82, 0xf2, 0x57, 0xa2, 0x60, 0x8a, - 0x63, 0x1f, + 0x7e, 0x3b, 0xd3, 0xe3, 0x04, 0xb4, 0x64, 0x1f, 0xd1, 0x53, 0x2f, 0x47, 0xfa, 0xc9, 0x56, + 0xe4, 0x13, 0x03, 0x47, 0x02, 0x0f, 0x08, 0xa3, 0x72, 0xa2, 0x57, 0xf2, 0x82, 0x1f, 0x63, + 0x8a, 0x60, ]; let rollup_exit_root: [u8; 32] = [ diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 6b2f973ae9..8b4a9c2200 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -1,10 +1,23 @@ -use miden_agglayer::{ExitRoot, UpdateGerNote, create_existing_bridge_account}; -use miden_protocol::Word; +extern crate alloc; + +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_agglayer::utils::felts_to_u256_bytes; +use miden_agglayer::{ExitRoot, UpdateGerNote, agglayer_library, create_existing_bridge_account}; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; +use miden_core_lib::handlers::keccak256::KeccakPreimage; use miden_protocol::account::StorageSlotName; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, Word}; use miden_testing::{Auth, MockChain}; +use super::test_utils::execute_program_with_default_host; + #[tokio::test] async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -62,3 +75,85 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { Ok(()) } + +/// Tests compute_ger with known mainnet and rollup exit roots. +/// +/// The GER (Global Exit Root) is computed as keccak256(mainnet_exit_root || rollup_exit_root). +#[tokio::test] +async fn test_compute_ger_basic() -> anyhow::Result<()> { + let agglayer_lib = agglayer_library(); + + // Define test exit roots (32 bytes each) + let mainnet_exit_root: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, + ]; + + let rollup_exit_root: [u8; 32] = [ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, + ]; + + // Concatenate the two roots (64 bytes total) + let mut ger_preimage = Vec::with_capacity(64); + ger_preimage.extend_from_slice(&mainnet_exit_root); + ger_preimage.extend_from_slice(&rollup_exit_root); + + // Compute expected GER using keccak256 + let expected_ger_preimage = KeccakPreimage::new(ger_preimage.clone()); + let expected_ger_felts: [Felt; 8] = expected_ger_preimage.digest().as_ref().try_into().unwrap(); + + let ger_bytes = felts_to_u256_bytes(expected_ger_felts); + + let ger = ExitRoot::from(ger_bytes); + // sanity check + assert_eq!(ger.to_elements(), expected_ger_felts); + + // Convert exit roots to packed u32 felts for memory initialization + let mainnet_felts = bytes_to_packed_u32_felts(&mainnet_exit_root); + let rollup_felts = bytes_to_packed_u32_felts(&rollup_exit_root); + + // Build memory initialization: mainnet at ptr 0, rollup at ptr 8 + let mem_init: Vec = mainnet_felts + .iter() + .chain(rollup_felts.iter()) + .enumerate() + .map(|(i, f)| format!("push.{} mem_store.{}", f.as_int(), i)) + .collect(); + let mem_init_code = mem_init.join("\n"); + + let source = format!( + r#" + use miden::core::sys + use miden::agglayer::crypto_utils + + begin + # Initialize memory with exit roots + {mem_init_code} + + # Call compute_ger with pointer to exit roots + push.0 + exec.crypto_utils::compute_ger + exec.sys::truncate_stack + end + "# + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib.clone()) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, None).await?; + + let result_digest: Vec = exec_output.stack[0..8].to_vec(); + + assert_eq!(result_digest, expected_ger_felts); + + Ok(()) +} From 0d9d083e0077a9a439e2f56fa1974438eb572244 Mon Sep 17 00:00:00 2001 From: Marti Date: Sat, 14 Feb 2026 17:56:51 +0100 Subject: [PATCH 02/21] feat(AggLayer): Move padding to the end of `NoteStorage` for `CLAIM` note (#2405) * feat: move padding to the end of LEAF_DATA * chore: uncomment temp unsued code * changelog --- CHANGELOG.md | 1 + .../asm/bridge/agglayer_faucet.masm | 33 +++++++++++++------ .../miden-agglayer/asm/bridge/bridge_in.masm | 2 +- .../asm/bridge/crypto_utils.masm | 2 ++ .../asm/note_scripts/CLAIM.masm | 16 ++++----- crates/miden-agglayer/src/claim_note.rs | 8 ++--- crates/miden-testing/tests/agglayer/mod.rs | 6 +++- .../tests/agglayer/test_utils.rs | 6 ++++ 8 files changed, 50 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adb569765..fd2bfb7026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - [BREAKING] Moved standard note code into individual note modules ([#2363](https://github.com/0xMiden/miden-base/pull/2363)). - [BREAKING] Added `miden::standards::note_tag` module for account target note tags ([#2366](https://github.com/0xMiden/miden-base/pull/2366)). - Unified the underlying representation of `ExitRoot` and `SmtNode` and use type aliases ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). +- [BREAKING] Moved padding to the end of `CLAIM` `NoteStorage` layout ([#2405](https://github.com/0xMiden/miden-base/pull/2405)). ### Fixes - Fixed byte-to-felt conversion for `ExitRoot` and `SmtNode`'s `to_elements()` method ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index ddf0e4b99c..2b4ec3a249 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -33,14 +33,20 @@ const CLAIM_NOTE_DATA_MEM_ADDR = 712 const OUTPUT_NOTE_INPUTS_MEM_ADDR = 0 const OUTPUT_NOTE_TAG_MEM_ADDR = 574 const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 -const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 552 -const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 556 - -const DESTINATION_ADDRESS_0 = 547 -const DESTINATION_ADDRESS_1 = 548 -const DESTINATION_ADDRESS_2 = 549 -const DESTINATION_ADDRESS_3 = 550 -const DESTINATION_ADDRESS_4 = 551 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 549 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 550 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 = 551 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 = 552 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = 553 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556 + +const DESTINATION_ADDRESS_0 = 544 +const DESTINATION_ADDRESS_1 = 545 +const DESTINATION_ADDRESS_2 = 546 +const DESTINATION_ADDRESS_3 = 547 +const DESTINATION_ADDRESS_4 = 548 # P2ID output note constants const P2ID_NOTE_NUM_STORAGE_ITEMS = 2 @@ -86,8 +92,14 @@ end # Inputs: [] # Outputs: [U256[0], U256[1]] proc get_raw_claim_amount - padw mem_loadw_be.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 - padw mem_loadw_be.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 end # Inputs: [U256[0], U256[1]] @@ -220,6 +232,7 @@ end #! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) #! ], #! LEAF_DATA_KEY => [ +#! leafType[1], // Leaf type (1 felt, uint8) #! originNetwork[1], // Origin network identifier (1 felt, uint32) #! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index 734c2a2fd9..fd7cebfd51 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -81,7 +81,7 @@ end #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) #! metadata[8], // ABI encoded metadata (8 felts, fixed size) -#! EMPTY_WORD // padding +#! padding[3], // padding (3 felts) - not used in the hash #! ], #! } #! diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 4a9534882d..e2243ccd0a 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -34,12 +34,14 @@ const CUR_HASH_HI_LOCAL = 4 #! Operand stack: [LEAF_DATA_KEY] #! Advice map: { #! LEAF_DATA_KEY => [ +#! leafType[1], // Leaf type (1 felt, uint8) #! originNetwork[1], // Origin network identifier (1 felt, uint32) #! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) #! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! padding[3], // padding (3 felts) - not used in the hash #! ], #! } #! Outputs: [LEAF_VALUE[8]] diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index e213a9f1ff..1aa0a2a1ea 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -69,13 +69,13 @@ end #! #! LEAF_DATA_KEY => [ #! leafType[1], // Leaf type (1 felt, uint32) -#! padding[3], // padding (3 felts) #! originNetwork[1], // Origin network identifier (1 felt, uint32) #! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) #! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! padding[3], // padding (3 felts) #! ] #! #! TODO: Will be removed in future PR @@ -154,13 +154,13 @@ end #! - mainnetExitRoot [520..527]: 8 felts #! - rollupExitRoot [528..535]: 8 felts #! - leafType [536] : 1 felt -#! - padding [537..539]: 3 felts -#! - originNetwork [540] : 1 felt -#! - originTokenAddress [541..545]: 5 felts -#! - destinationNetwork [546] : 1 felt -#! - destinationAddress [547..551]: 5 felts -#! - amount [552..559]: 8 felts -#! - metadata [560..567]: 8 felts +#! - originNetwork [537] : 1 felt +#! - originTokenAddress [538..542]: 5 felts +#! - destinationNetwork [543] : 1 felt +#! - destinationAddress [544..548]: 5 felts +#! - amount [549..556]: 8 felts +#! - metadata [557..564]: 8 felts +#! - padding [565..567]: 3 felts #! - output_p2id_serial_num [568..571]: 4 felts #! - target_faucet_account_id [572..573]: 2 felts #! - output_note_tag [574] : 1 felt diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 75b2e65720..5fd465d5f5 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -129,7 +129,7 @@ impl SequentialCommit for LeafData { type Commitment = Word; fn to_elements(&self) -> Vec { - const LEAF_DATA_ELEMENT_COUNT: usize = 32; // 1 + 3 + 1 + 5 + 1 + 5 + 8 + 8 (leafType + padding + networks + addresses + amount + metadata) + const LEAF_DATA_ELEMENT_COUNT: usize = 32; // 1 + 1 + 5 + 1 + 5 + 8 + 8 + 3 (leafType + networks + addresses + amount + metadata + padding) let mut elements = Vec::with_capacity(LEAF_DATA_ELEMENT_COUNT); // LeafType (uint32 as Felt): 0u32 for transfer Ether / ERC20 tokens, 1u32 for message @@ -137,9 +137,6 @@ impl SequentialCommit for LeafData { // for a `CLAIM` note, leafType is always 0 (transfer Ether / ERC20 tokens) elements.push(Felt::ZERO); - // Padding - elements.extend(vec![Felt::ZERO; 3]); - // Origin network elements.push(Felt::new(self.origin_network as u64)); @@ -158,6 +155,9 @@ impl SequentialCommit for LeafData { // Metadata (8 u32 felts) elements.extend(self.metadata.iter().map(|&v| Felt::new(v as u64))); + // Padding + elements.extend(vec![Felt::ZERO; 3]); + elements } } diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index f96326ffa3..3cb09e8467 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,5 +1,9 @@ pub mod asset_conversion; -mod bridge_in; +// TODO: Uncomment this when https://github.com/0xMiden/miden-base/issues/2397 is ready. +// The mainnet exit root is hardcoded to pass the current test (i.e. we set the expected mainnet +// root to whatever the current implementation computes), and changing any impl. details will break +// the test, forcing us to artificially change the expected root every time. +// mod bridge_in; mod bridge_out; mod crypto_utils; mod global_index; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index b0d28cea6f..af1e987ae3 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -49,6 +49,11 @@ pub async fn execute_program_with_default_host( processor.execute(&program, &mut host).await } +/* +// TODO: Uncomment this when https://github.com/0xMiden/miden-base/issues/2397 is ready. +// The mainnet exit root is hardcoded to pass the current test (i.e. we set the expected mainnet +// root to whatever the current implementation computes), and changing any impl. details will break +// the test, forcing us to artificially change the expected root every time. // TESTING HELPERS // ================================================================================================ @@ -125,3 +130,4 @@ pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { metadata, ) } +*/ From 046b02bcae5ad948d2e3bbe9eaf7f32aecada1d3 Mon Sep 17 00:00:00 2001 From: Marti Date: Sat, 14 Feb 2026 18:37:05 +0100 Subject: [PATCH 03/21] feat(AggLayer): byte packing from felts to match Solidity `abi.encodePacked` (#2401) * generate test vectors for getLeafValue * feat: introduce MetadataHash wrapper * feat: refactor amount * feat: introduce LeafValueVector * feat: pack leaf data + test chore: remove u32_words_to_solidity_bytes32_hex * chore: move helper func before tests * feat: EthAddressFormat byte ordering feat: change Address ordering to BE limbs * fix: adjust mainnet exit root to match * feat: test get_leaf_value against vec * feat(test): compare all packed elements from memory * feat: change within-felt byte ordering to LE * feat: make EthAmount internal repr bytes * chore: use bytes_to_packed_u32_felts helper * chore: replace felts_to_u256_bytes with felts_to_bytes * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> * chore: refactor into get_and compute_ leaf_value * chore: document mem layout for pack_leaf_data --------- Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- .gitmodules | 4 + Makefile | 1 + .../asm/bridge/crypto_utils.masm | 148 ++++++- .../asm/bridge/eth_address.masm | 80 +++- .../solidity-compat/foundry.toml | 5 +- .../lib/openzeppelin-contracts-upgradeable | 1 + .../test-vectors/leaf_value_vectors.json | 10 + .../test/LeafValueTestVectors.t.sol | 64 +++ crates/miden-agglayer/src/b2agg_note.rs | 3 +- crates/miden-agglayer/src/claim_note.rs | 20 +- crates/miden-agglayer/src/errors/agglayer.rs | 6 +- .../miden-agglayer/src/eth_types/address.rs | 53 ++- crates/miden-agglayer/src/eth_types/amount.rs | 139 +------ .../src/eth_types/metadata_hash.rs | 34 ++ crates/miden-agglayer/src/eth_types/mod.rs | 4 +- crates/miden-agglayer/src/lib.rs | 2 +- crates/miden-agglayer/src/utils.rs | 13 +- .../tests/agglayer/asset_conversion.rs | 8 +- .../miden-testing/tests/agglayer/bridge_in.rs | 9 +- .../tests/agglayer/crypto_utils.rs | 386 +++++++++++------- .../solidity_miden_address_conversion.rs | 16 +- .../tests/agglayer/test_utils.rs | 14 +- .../tests/agglayer/update_ger.rs | 4 +- 23 files changed, 655 insertions(+), 369 deletions(-) create mode 160000 crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json create mode 100644 crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol create mode 100644 crates/miden-agglayer/src/eth_types/metadata_hash.rs diff --git a/.gitmodules b/.gitmodules index b02c269a3f..0333b7b402 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,7 @@ [submodule "crates/miden-agglayer/solidity-compat/lib/agglayer-contracts"] path = crates/miden-agglayer/solidity-compat/lib/agglayer-contracts url = https://github.com/agglayer/agglayer-contracts +[submodule "crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable"] + path = crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git + branch = release-v4.9 diff --git a/Makefile b/Makefile index b2d5c45df3..733dfa1ac6 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,7 @@ generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Fo cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVectors cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateCanonicalZeros cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVerificationProofData + cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateLeafValueVectors # --- benchmarking -------------------------------------------------------------------------------- diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index e2243ccd0a..fec507020e 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -13,10 +13,21 @@ type MemoryAddress = u32 # CONSTANTS # ================================================================================================= +# the number of bytes in the leaf data to hash (matches Solidity's abi.encodePacked output) const LEAF_DATA_BYTES = 113 + +# the number of words (4 felts each) in the advice map data const LEAF_DATA_NUM_WORDS = 8 + +# the memory address where leaf data is stored const LEAF_DATA_START_PTR = 0 +# the local memory offset where we store the leaf data start pointer +const PACKING_START_PTR_LOCAL= 0 + +# the number of elements to pack (113 bytes = 29 elements, rounding up from 28.25) +const PACKED_DATA_NUM_ELEMENTS = 29 + # The offset of the first half of the current Keccak256 hash value in the local memory of the # `calculate_root` procedure. const CUR_HASH_LO_LOCAL = 0 @@ -28,7 +39,8 @@ const CUR_HASH_HI_LOCAL = 4 # PUBLIC INTERFACE # ================================================================================================= -#! Given the leaf data key returns the leaf value. +#! Given the leaf data key, loads the leaf data from advice map to memory, packs the data in-place, +#! and computes the leaf value by hashing the packed bytes. #! #! Inputs: #! Operand stack: [LEAF_DATA_KEY] @@ -40,7 +52,7 @@ const CUR_HASH_HI_LOCAL = 4 #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) -#! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! metadata_hash[8], // Metadata hash (8 felts, bytes32 as 8 u32 felts) #! padding[3], // padding (3 felts) - not used in the hash #! ], #! } @@ -55,7 +67,26 @@ pub proc get_leaf_value(leaf_data_key: BeWord) -> DoubleWord exec.mem::pipe_preimage_to_memory drop # => [] - push.LEAF_DATA_BYTES push.LEAF_DATA_START_PTR + # compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR + push.LEAF_DATA_START_PTR + exec.compute_leaf_value + # => [LEAF_VALUE[8]] +end + +#! Given a memory address where the unpacked leaf data starts, packs the leaf data in-place, and +#! computes the leaf value by hashing the packed bytes. +#! +#! Inputs: [LEAF_DATA_START_PTR] +#! Outputs: [LEAF_VALUE[8]] +#! +#! Invocation: exec +pub proc compute_leaf_value(leaf_data_start_ptr: MemoryAddress) -> DoubleWord + dup + # => [leaf_data_start_ptr, leaf_data_start_ptr] + exec.pack_leaf_data + # => [leaf_data_start_ptr] + + push.LEAF_DATA_BYTES swap # => [start_ptr, byte_len] exec.keccak256::hash_bytes @@ -197,3 +228,114 @@ proc calculate_root( loc_loadw_be.CUR_HASH_HI_LOCAL swapw loc_loadw_be.CUR_HASH_LO_LOCAL # => [ROOT_LO, ROOT_HI] end + +#! Packs the raw leaf data by shifting left 3 bytes to match Solidity's abi.encodePacked format. +#! +#! The raw data has leafType occupying 4 bytes (as a u32 felt) but Solidity's abi.encodePacked +#! only uses 1 byte for uint8 leafType. This procedure shifts all data left by 3 bytes so that: +#! - Byte 0: leafType (1 byte) +#! - Bytes 1-4: originNetwork (4 bytes) +#! - etc. +#! +#! The Keccak precompile expects u32 values packed in little-endian byte order. +#! For each packed element, we drop the leading 3 bytes and rebuild the u32 so that +#! bytes [b0, b1, b2, b3] map to u32::from_le_bytes([b0, b1, b2, b3]). +#! With little-endian input limbs, the first byte comes from the MSB of `curr` and +#! the next three bytes come from the LSBs of `next`: +#! packed = ((curr >> 24) & 0xFF) +#! | (next & 0xFF) << 8 +#! | ((next >> 8) & 0xFF) << 16 +#! | ((next >> 16) & 0xFF) << 24 +#! +#! To help visualize the packing process, consider that each field element represents a 4-byte +#! value [u8; 4] (LE). +#! Memory before is: +#! ptr+0: 1 felt: [a, b, c, d] +#! ptr+1: 1 felt: [e, f, g, h] +#! ptr+2..6: 5 felts: [i, j, k, l, m, ...] +#! +#! Memory after: +#! ptr+0: 1 felt: [d, e, f, g] +#! ptr+1: 1 felt: [h, i, j, k] +#! ptr+2..6: 5 felts: [l, ...] +#! +#! Inputs: [leaf_data_start_ptr] +#! Outputs: [] +#! +#! Invocation: exec +@locals(1) # start_ptr +pub proc pack_leaf_data(leaf_data_start_ptr: MemoryAddress) + loc_store.PACKING_START_PTR_LOCAL + # => [] + + # initialize loop counter to 0 + push.0 + + # push initial condition (true) to enter the loop + push.1 + + # loop through elements from 0 to PACKED_DATA_NUM_ELEMENTS - 1 (28) + while.true + # => [counter] + + # compute source address: packing_start_ptr + counter + dup loc_load.PACKING_START_PTR_LOCAL add + # => [src_addr, counter] + + # load current element + mem_load + # => [curr_elem, counter] + + # extract MSB (upper 8 bits) which becomes the first little-endian byte + dup u32shr.24 + # => [curr_msb, curr_elem, counter] + + # compute source address for next element (counter + 1) + dup.2 loc_load.PACKING_START_PTR_LOCAL add add.1 + # => [next_src_addr, curr_lsb, curr_elem, counter] + + # load next element + mem_load + # => [next_elem, curr_lsb, curr_elem, counter] + + # keep curr_msb on top for combination + swap + # => [curr_msb, next_elem, curr_elem, counter] + + # add next byte0 (bits 0..7) into bits 8..15 + dup.1 u32and.0xFF u32shl.8 u32or + # => [partial, next_elem, curr_elem, counter] + + # add next byte1 (bits 8..15) into bits 16..23 + dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or + # => [partial, next_elem, curr_elem, counter] + + # add next byte2 (bits 16..23) into bits 24..31 + dup.1 u32shr.16 u32and.0xFF u32shl.24 u32or + # => [packed_elem, next_elem, curr_elem, counter] + + # drop the next and current elements (no longer needed) + movdn.2 drop drop + # => [packed_elem, counter] + + # compute destination address: packing_start_ptr + counter (in-place) + dup.1 loc_load.PACKING_START_PTR_LOCAL add + # => [dest_addr, packed_elem, counter] + + # store packed element + mem_store + # => [counter] + + # increment counter + add.1 + # => [counter + 1] + + # check if we should continue (counter < PACKED_DATA_NUM_ELEMENTS) + dup push.PACKED_DATA_NUM_ELEMENTS lt + # => [should_continue, counter] + end + # => [counter] + + drop + # => [] +end \ No newline at end of file diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index 57a8e9f298..aa01df3480 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -8,13 +8,37 @@ const U32_MAX=4294967295 const TWO_POW_32=4294967296 const ERR_NOT_U32="address limb is not u32" -const ERR_ADDR4_NONZERO="most-significant 4 bytes (addr4) must be zero" +const ERR_MSB_NONZERO="most-significant 4 bytes must be zero for AccountId" const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" - # ETHEREUM ADDRESS PROCEDURES # ================================================================================================= +#! Swaps byte order in a u32 limb (LE <-> BE). +#! +#! Inputs: [value] +#! Outputs: [swapped] +proc swap_u32_bytes + # part0 = (value & 0xFF) << 24 + dup u32and.0xFF u32shl.24 + # => [value, part0] + + # part1 = ((value >> 8) & 0xFF) << 16 + dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or + # => [value, part01] + + # part2 = ((value >> 16) & 0xFF) << 8 + dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or + # => [value, part012] + + # part3 = (value >> 24) + dup.1 u32shr.24 u32or + # => [value, swapped] + + swap drop + # => [swapped] +end + #! Builds a single felt from two u32 limbs (little-endian limb order). #! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. #! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping @@ -25,22 +49,29 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" proc build_felt # --- validate u32 limbs --- u32assert2.err=ERR_NOT_U32 + # => [lo_be, hi_be] + + # limbs are little-endian bytes; swap to big-endian for building account ID + exec.swap_u32_bytes + swap + exec.swap_u32_bytes + swap # => [lo, hi] # keep copies for the overflow check dup.1 dup.1 - # => [lo, hi, lo, hi] + # => [lo_be, hi_be, lo_be, hi_be] # felt = (hi * 2^32) + lo swap push.TWO_POW_32 mul add - # => [felt, lo, hi] + # => [felt, lo_be, hi_be] # ensure no reduction mod p happened: # split felt back into (hi, lo) and compare to inputs dup u32split - # => [hi2, lo2, felt, lo, hi] + # => [hi2, lo2, felt, lo_be, hi_be] movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD # => [lo2, felt, lo] @@ -51,37 +82,42 @@ end #! Converts an Ethereum address format (address[5] type) back into an AccountId [prefix, suffix] type. #! -#! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*: -#! addr0 = bytes[16..19] (least-significant 4 bytes) -#! addr1 = bytes[12..15] -#! addr2 = bytes[ 8..11] -#! addr3 = bytes[ 4.. 7] -#! addr4 = bytes[ 0.. 3] (most-significant 4 bytes) +#! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *big-endian limb order* +#! (matching Solidity ABI encoding). Each limb encodes its 4 bytes in little-endian order: +#! limb0 = bytes[0..4] (most-significant 4 bytes, must be zero for AccountId) +#! limb1 = bytes[4..8] +#! limb2 = bytes[8..12] +#! limb3 = bytes[12..16] +#! limb4 = bytes[16..20] (least-significant 4 bytes) #! -#! The most-significant 4 bytes must be zero for a valid AccountId conversion (addr4 == 0). +#! The most-significant 4 bytes must be zero for a valid AccountId conversion (be0 == 0). #! The remaining 16 bytes are treated as two 8-byte words (conceptual u64 values): -#! prefix = (addr3 << 32) | addr2 # bytes[4..11] -#! suffix = (addr1 << 32) | addr0 # bytes[12..19] +#! prefix = (bswap(limb1) << 32) | bswap(limb2) # bytes[4..12] +#! suffix = (bswap(limb3) << 32) | bswap(limb4) # bytes[12..20] #! #! These 8-byte words are represented as field elements by packing two u32 limbs into a felt. #! The packing is done via build_felt, which validates limbs are u32 and checks the packed value #! did not reduce mod p (i.e. the word fits in the field). #! -#! Inputs: [addr0, addr1, addr2, addr3, addr4] +#! Inputs: [limb0, limb1, limb2, limb3, limb4] #! Outputs: [prefix, suffix] #! #! Invocation: exec pub proc to_account_id - # addr4 must be 0 (most-significant limb) - movup.4 - eq.0 assert.err=ERR_ADDR4_NONZERO - # => [addr0, addr1, addr2, addr3] + # limb0 must be 0 (most-significant limb, on top) + assertz.err=ERR_MSB_NONZERO + # => [limb1, limb2, limb3, limb4] + + # Reorder for suffix = build_felt(limb4, limb3) where limb4=lo, limb3=hi + movup.2 movup.3 + # => [limb4, limb3, limb1, limb2] exec.build_felt - # => [suffix, addr2, addr3] + # => [suffix, limb1, limb2] - movdn.2 - # => [addr2, addr3, suffix] + # Reorder for prefix = build_felt(limb2, limb1) where limb2=lo, limb1=hi + swap movup.2 + # => [limb2, limb1, suffix] exec.build_felt # => [prefix, suffix] diff --git a/crates/miden-agglayer/solidity-compat/foundry.toml b/crates/miden-agglayer/solidity-compat/foundry.toml index c22ad7e3f6..d8633d0d32 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.toml +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -4,7 +4,10 @@ out = "out" solc = "0.8.20" src = "src" -remappings = ["@agglayer/=lib/agglayer-contracts/contracts/"] +remappings = [ + "@agglayer/=lib/agglayer-contracts/contracts/", + "@openzeppelin/contracts-upgradeable4/=lib/openzeppelin-contracts-upgradeable/contracts/", +] # Emit extra output for test vector generation ffi = false diff --git a/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000000..2d081f24ca --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json new file mode 100644 index 0000000000..f20fb04025 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json @@ -0,0 +1,10 @@ +{ + "amount": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000", + "destination_address": "0xD9b20Fe633b609B01081aD0428e81f8Dd604F5C5", + "destination_network": 7, + "leaf_type": 0, + "leaf_value": "0xb67e42971034605367b7e92d1ad1d4648c3ffe0bea9b08115cd9aa2e616b2f88", + "metadata_hash": "0x6c7a91a5fb41dee8f0bc1c86b5587334583186f14acfa253e2f7c2833d1d6fdf", + "origin_network": 0, + "origin_token_address": "0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7" +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol new file mode 100644 index 0000000000..18ef6d12ca --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; + +/** + * @title LeafValueTestVectors + * @notice Test contract that generates test vectors for verifying compatibility + * between Solidity's getLeafValue and Miden's keccak hash implementation. + * + * Run with: forge test -vv --match-contract LeafValueTestVectors + * + * The output can be compared against the Rust get_leaf_value implementation. + */ +contract LeafValueTestVectors is Test, DepositContractV2 { + /** + * @notice Generates leaf value test vectors and saves to JSON file. + * Uses real transaction data from Lumia explorer: + * https://explorer.lumia.org/tx/0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188 + * + * Output file: test-vectors/leaf_value_vectors.json + */ + function test_generateLeafValueVectors() public { + // Test vector from real Lumia bridge transaction + uint8 leafType = 0; // 0 for ERC20/ETH transfer + uint32 originNetwork = 0; + address originTokenAddress = 0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7; + uint32 destinationNetwork = 7; + address destinationAddress = 0xD9b20Fe633b609B01081aD0428e81f8Dd604F5C5; + uint256 amount = 2000000000000000000; // 2e18 + + // Original metadata from the transaction (ABI encoded: name, symbol, decimals) + bytes memory metadata = hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000b4c756d696120546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054c554d4941000000000000000000000000000000000000000000000000000000"; + bytes32 metadataHash = keccak256(metadata); + + // Compute the leaf value using the official DepositContractV2 implementation + bytes32 leafValue = getLeafValue( + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + // Serialize to JSON + // Note: amount is serialized as bytes32 (hex string) to properly handle u256 values + string memory obj = "root"; + vm.serializeUint(obj, "leaf_type", leafType); + vm.serializeUint(obj, "origin_network", originNetwork); + vm.serializeAddress(obj, "origin_token_address", originTokenAddress); + vm.serializeUint(obj, "destination_network", destinationNetwork); + vm.serializeAddress(obj, "destination_address", destinationAddress); + vm.serializeBytes32(obj, "amount", bytes32(amount)); + vm.serializeBytes32(obj, "metadata_hash", metadataHash); + string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); + + // Save to file + string memory outputPath = "test-vectors/leaf_value_vectors.json"; + vm.writeJson(json, outputPath); + } +} diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs index 255082032a..602fdacfac 100644 --- a/crates/miden-agglayer/src/b2agg_note.rs +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -123,7 +123,8 @@ fn build_note_storage( ) -> Result { let mut elements = Vec::with_capacity(6); - elements.push(Felt::new(destination_network as u64)); + let destination_network = u32::from_le_bytes(destination_network.to_be_bytes()); + elements.push(Felt::from(destination_network)); elements.extend(destination_address.to_elements()); NoteStorage::new(elements) diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 5fd465d5f5..084d2d7254 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -19,7 +19,7 @@ use miden_protocol::note::{ }; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; -use crate::{EthAddressFormat, EthAmount, claim_script}; +use crate::{EthAddressFormat, EthAmount, MetadataHash, claim_script}; // CLAIM NOTE STRUCTURES // ================================================================================================ @@ -121,8 +121,8 @@ pub struct LeafData { pub destination_address: EthAddressFormat, /// Amount of tokens (uint256) pub amount: EthAmount, - /// ABI encoded metadata (fixed size of 8 u32 values) - pub metadata: [u32; 8], + /// Metadata hash (32 bytes) + pub metadata_hash: MetadataHash, } impl SequentialCommit for LeafData { @@ -137,14 +137,16 @@ impl SequentialCommit for LeafData { // for a `CLAIM` note, leafType is always 0 (transfer Ether / ERC20 tokens) elements.push(Felt::ZERO); - // Origin network - elements.push(Felt::new(self.origin_network as u64)); + // Origin network (encode as little-endian bytes for keccak) + let origin_network = u32::from_le_bytes(self.origin_network.to_be_bytes()); + elements.push(Felt::from(origin_network)); // Origin token address (5 u32 felts) elements.extend(self.origin_token_address.to_elements()); - // Destination network - elements.push(Felt::new(self.destination_network as u64)); + // Destination network (encode as little-endian bytes for keccak) + let destination_network = u32::from_le_bytes(self.destination_network.to_be_bytes()); + elements.push(Felt::from(destination_network)); // Destination address (5 u32 felts) elements.extend(self.destination_address.to_elements()); @@ -152,8 +154,8 @@ impl SequentialCommit for LeafData { // Amount (uint256 as 8 u32 felts) elements.extend(self.amount.to_elements()); - // Metadata (8 u32 felts) - elements.extend(self.metadata.iter().map(|&v| Felt::new(v as u64))); + // Metadata hash (8 u32 felts) + elements.extend(self.metadata_hash.to_elements()); // Padding elements.extend(vec![Felt::ZERO; 3]); diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index a1874001d9..f5b26c15c7 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -9,9 +9,6 @@ use miden_protocol::errors::MasmError; // AGGLAYER ERRORS // ================================================================================================ -/// Error Message: "most-significant 4 bytes (addr4) must be zero" -pub const ERR_ADDR4_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes (addr4) must be zero"); - /// Error Message: "B2AGG note attachment target account does not match consuming account" pub const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("B2AGG note attachment target account does not match consuming account"); /// Error Message: "B2AGG script expects exactly 6 note storage items" @@ -37,6 +34,9 @@ pub const ERR_LEADING_BITS_NON_ZERO: MasmError = MasmError::from_static_str("lea /// Error Message: "number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)" pub const ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)"); +/// Error Message: "most-significant 4 bytes must be zero for AccountId" +pub const ERR_MSB_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes must be zero for AccountId"); + /// Error Message: "address limb is not u32" pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); diff --git a/crates/miden-agglayer/src/eth_types/address.rs b/crates/miden-agglayer/src/eth_types/address.rs index f2a94ed6df..7f8810c79d 100644 --- a/crates/miden-agglayer/src/eth_types/address.rs +++ b/crates/miden-agglayer/src/eth_types/address.rs @@ -1,8 +1,9 @@ use alloc::format; use alloc::string::{String, ToString}; +use alloc::vec::Vec; use core::fmt; -use miden_core::FieldElement; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::Felt; use miden_protocol::account::AccountId; use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; @@ -17,15 +18,16 @@ use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; /// /// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the /// most-significant byte). -/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *little-endian limb order*: -/// - addr0 = bytes[16..19] (least-significant 4 bytes) -/// - addr1 = bytes[12..15] -/// - addr2 = bytes[ 8..11] -/// - addr3 = bytes[ 4.. 7] -/// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) +/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *big-endian limb order* (each limb encodes its 4 +/// bytes in little-endian order so felts map to keccak bytes directly): +/// - `address[0]` = bytes[0..4] (most-significant 4 bytes, zero for embedded AccountId) +/// - `address[1]` = bytes[4..8] +/// - `address[2]` = bytes[8..12] +/// - `address[3]` = bytes[12..16] +/// - `address[4]` = bytes[16..20] (least-significant 4 bytes) /// - Embedded AccountId format: `0x00000000 || prefix(8) || suffix(8)`, where: -/// - prefix = (addr3 << 32) | addr2 = bytes[4..11] as a big-endian u64 -/// - suffix = (addr1 << 32) | addr0 = bytes[12..19] as a big-endian u64 +/// - prefix = bytes[4..12] as a big-endian u64 +/// - suffix = bytes[12..20] as a big-endian u64 /// /// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure /// `Felt::new(u64)` does not reduce mod p (checked explicitly in `to_account_id`). @@ -104,31 +106,22 @@ impl EthAddressFormat { // INTERNAL API - For CLAIM note processing // -------------------------------------------------------------------------------------------- - /// Converts the Ethereum address format into an array of 5 [`Felt`] values for MASM processing. + /// Converts the Ethereum address format into an array of 5 [`Felt`] values for Miden VM. /// /// **Internal API**: This function is used internally during CLAIM note processing to convert - /// the address format into the MASM `address[5]` representation expected by the + /// the address into the MASM `address[5]` representation expected by the /// `to_account_id` procedure. /// - /// The returned order matches the MASM `address\[5\]` convention (*little-endian limb order*): - /// - addr0 = bytes[16..19] (least-significant 4 bytes) - /// - addr1 = bytes[12..15] - /// - addr2 = bytes[ 8..11] - /// - addr3 = bytes[ 4.. 7] - /// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) + /// The returned order matches the Solidity ABI encoding convention (*big-endian limb order*): + /// - `address[0]` = bytes[0..4] (most-significant 4 bytes, zero for embedded AccountId) + /// - `address[1]` = bytes[4..8] + /// - `address[2]` = bytes[8..12] + /// - `address[3]` = bytes[12..16] + /// - `address[4]` = bytes[16..20] (least-significant 4 bytes) /// - /// Each limb is interpreted as a big-endian `u32` and stored in a [`Felt`]. - pub fn to_elements(&self) -> [Felt; 5] { - let mut result = [Felt::ZERO; 5]; - - // i=0 -> bytes[16..20], i=4 -> bytes[0..4] - for (felt, chunk) in result.iter_mut().zip(self.0.chunks(4).skip(1).rev()) { - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - // u32 values always fit in Felt, so this conversion is safe - *felt = Felt::try_from(value as u64).expect("u32 value should always fit in Felt"); - } - - result + /// Each limb is interpreted as a little-endian `u32` and stored in a [`Felt`]. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) } /// Converts the Ethereum address format back to an [`AccountId`]. @@ -162,7 +155,7 @@ impl EthAddressFormat { /// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. /// Requires the first 4 bytes be zero. - /// Returns prefix and suffix values that match the MASM little-endian limb implementation: + /// Returns prefix and suffix values that match the MASM little-endian limb byte encoding: /// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2 /// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0 fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddressConversionError> { diff --git a/crates/miden-agglayer/src/eth_types/amount.rs b/crates/miden-agglayer/src/eth_types/amount.rs index dc0b9948cd..face01feee 100644 --- a/crates/miden-agglayer/src/eth_types/amount.rs +++ b/crates/miden-agglayer/src/eth_types/amount.rs @@ -1,29 +1,8 @@ -use core::fmt; +use alloc::vec::Vec; -use miden_core::FieldElement; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::Felt; -// ================================================================================================ -// ETHEREUM AMOUNT ERROR -// ================================================================================================ - -/// Error type for Ethereum amount conversions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EthAmountError { - /// The amount doesn't fit in the target type. - Overflow, -} - -impl fmt::Display for EthAmountError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - EthAmountError::Overflow => { - write!(f, "amount overflow: value doesn't fit in target type") - }, - } - } -} - // ================================================================================================ // ETHEREUM AMOUNT // ================================================================================================ @@ -33,119 +12,23 @@ impl fmt::Display for EthAmountError { /// This type provides a more typed representation of Ethereum amounts compared to raw `[u32; 8]` /// arrays, while maintaining compatibility with the existing MASM processing pipeline. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct EthAmount([u32; 8]); +pub struct EthAmount([u8; 32]); impl EthAmount { - /// Creates a new [`EthAmount`] from an array of 8 u32 values. - /// - /// The values are stored in little-endian order where `values[0]` contains - /// the least significant 32 bits. - pub const fn new(values: [u32; 8]) -> Self { - Self(values) - } - - /// Creates an [`EthAmount`] from a single u64 value. - /// - /// This is useful for smaller amounts that fit in a u64. The value is - /// stored in the first two u32 slots with the remaining slots set to zero. - pub const fn from_u64(value: u64) -> Self { - let low = value as u32; - let high = (value >> 32) as u32; - Self([low, high, 0, 0, 0, 0, 0, 0]) - } - - /// Creates an [`EthAmount`] from a single u32 value. - /// - /// This is useful for smaller amounts that fit in a u32. The value is - /// stored in the first u32 slot with the remaining slots set to zero. - pub const fn from_u32(value: u32) -> Self { - Self([value, 0, 0, 0, 0, 0, 0, 0]) - } - - /// Returns the raw array of 8 u32 values. - pub const fn as_array(&self) -> &[u32; 8] { - &self.0 - } - - /// Converts the amount into an array of 8 u32 values. - pub const fn into_array(self) -> [u32; 8] { - self.0 - } - - /// Returns true if the amount is zero. - pub fn is_zero(&self) -> bool { - self.0.iter().all(|&x| x == 0) - } - - /// Attempts to convert the amount to a u64. - /// - /// # Errors - /// Returns [`EthAmountError::Overflow`] if the amount doesn't fit in a u64 - /// (i.e., if any of the upper 6 u32 values are non-zero). - pub fn try_to_u64(&self) -> Result { - if self.0[2..].iter().any(|&x| x != 0) { - Err(EthAmountError::Overflow) - } else { - Ok((self.0[1] as u64) << 32 | self.0[0] as u64) - } - } - - /// Attempts to convert the amount to a u32. - /// - /// # Errors - /// Returns [`EthAmountError::Overflow`] if the amount doesn't fit in a u32 - /// (i.e., if any of the upper 7 u32 values are non-zero). - pub fn try_to_u32(&self) -> Result { - if self.0[1..].iter().any(|&x| x != 0) { - Err(EthAmountError::Overflow) - } else { - Ok(self.0[0]) - } + /// Creates an [`EthAmount`] from a 32-byte array. + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) } /// Converts the amount to a vector of field elements for note storage. /// /// Each u32 value in the amount array is converted to a [`Felt`]. - pub fn to_elements(&self) -> [Felt; 8] { - let mut result = [Felt::ZERO; 8]; - for (i, &value) in self.0.iter().enumerate() { - result[i] = Felt::from(value); - } - result + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) } -} - -impl From<[u32; 8]> for EthAmount { - fn from(values: [u32; 8]) -> Self { - Self(values) - } -} -impl From for [u32; 8] { - fn from(amount: EthAmount) -> Self { - amount.0 - } -} - -impl From for EthAmount { - fn from(value: u64) -> Self { - Self::from_u64(value) - } -} - -impl From for EthAmount { - fn from(value: u32) -> Self { - Self::from_u32(value) - } -} - -impl fmt::Display for EthAmount { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // For display purposes, show as a hex string of the full 256-bit value - write!(f, "0x")?; - for &value in self.0.iter().rev() { - write!(f, "{:08x}", value)?; - } - Ok(()) + /// Returns the raw 32-byte array. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 } } diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs new file mode 100644 index 0000000000..287f66e71a --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -0,0 +1,34 @@ +use alloc::vec::Vec; + +use miden_core_lib::handlers::bytes_to_packed_u32_felts; +use miden_protocol::Felt; + +// ================================================================================================ +// METADATA HASH +// ================================================================================================ + +/// Represents a Keccak256 metadata hash as 32 bytes. +/// +/// This type provides a typed representation of metadata hashes for the agglayer bridge, +/// while maintaining compatibility with the existing MASM processing pipeline. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MetadataHash([u8; 32]); + +impl MetadataHash { + /// Creates a new [`MetadataHash`] from a 32-byte array. + pub const fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the raw 32-byte array. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Converts the metadata hash to 8 Felt elements for MASM processing. + /// + /// Each 4-byte chunk is converted to a u32 using little-endian byte order. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) + } +} diff --git a/crates/miden-agglayer/src/eth_types/mod.rs b/crates/miden-agglayer/src/eth_types/mod.rs index c8184cbc8d..791486e35a 100644 --- a/crates/miden-agglayer/src/eth_types/mod.rs +++ b/crates/miden-agglayer/src/eth_types/mod.rs @@ -1,5 +1,7 @@ pub mod address; pub mod amount; +pub mod metadata_hash; pub use address::EthAddressFormat; -pub use amount::{EthAmount, EthAmountError}; +pub use amount::EthAmount; +pub use metadata_hash::MetadataHash; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index e4e0eae9c9..cc34fc5545 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -42,7 +42,7 @@ pub use claim_note::{ SmtNode, create_claim_note, }; -pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError}; +pub use eth_types::{EthAddressFormat, EthAmount, MetadataHash}; pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs index 9268722129..8b5e8d3820 100644 --- a/crates/miden-agglayer/src/utils.rs +++ b/crates/miden-agglayer/src/utils.rs @@ -1,15 +1,18 @@ +use alloc::vec::Vec; + use miden_protocol::Felt; // UTILITY FUNCTIONS // ================================================================================================ -/// Convert 8 Felt values (u32 limbs in little-endian order) to U256 bytes in little-endian format. -pub fn felts_to_u256_bytes(limbs: [Felt; 8]) -> [u8; 32] { - let mut bytes = [0u8; 32]; - for (i, limb) in limbs.iter().enumerate() { +/// Converts Felt u32 limbs to bytes using little-endian byte order. +/// TODO remove once we move to v0.21.0 which has `packed_u32_elements_to_bytes` +pub fn felts_to_bytes(limbs: &[Felt]) -> Vec { + let mut bytes = Vec::with_capacity(limbs.len() * 4); + for limb in limbs.iter() { let u32_value = limb.as_int() as u32; let limb_bytes = u32_value.to_le_bytes(); - bytes[i * 4..(i + 1) * 4].copy_from_slice(&limb_bytes); + bytes.extend_from_slice(&limb_bytes); } bytes } diff --git a/crates/miden-testing/tests/agglayer/asset_conversion.rs b/crates/miden-testing/tests/agglayer/asset_conversion.rs index c37b1c206b..1e3c91cbd1 100644 --- a/crates/miden-testing/tests/agglayer/asset_conversion.rs +++ b/crates/miden-testing/tests/agglayer/asset_conversion.rs @@ -16,7 +16,7 @@ fn felts_to_u256(felts: Vec) -> U256 { assert_eq!(felts.len(), 8, "expected exactly 8 felts"); let array: [Felt; 8] = [felts[0], felts[1], felts[2], felts[3], felts[4], felts[5], felts[6], felts[7]]; - let bytes = utils::felts_to_u256_bytes(array); + let bytes = utils::felts_to_bytes(&array); U256::from_little_endian(&bytes) } @@ -199,7 +199,7 @@ fn test_felts_to_u256_bytes_sequential_values() { Felt::new(7), Felt::new(8), ]; - let result = utils::felts_to_u256_bytes(limbs); + let result = utils::felts_to_bytes(&limbs); assert_eq!(result.len(), 32); // Verify the byte layout: limbs are processed in little-endian order, each as little-endian u32 @@ -214,13 +214,13 @@ fn test_felts_to_u256_bytes_sequential_values() { fn test_felts_to_u256_bytes_edge_cases() { // Test case 1: All zeros (minimum) let limbs = [Felt::new(0); 8]; - let result = utils::felts_to_u256_bytes(limbs); + let result = utils::felts_to_bytes(&limbs); assert_eq!(result.len(), 32); assert!(result.iter().all(|&b| b == 0)); // Test case 2: All max u32 values (maximum) let limbs = [Felt::new(u32::MAX as u64); 8]; - let result = utils::felts_to_u256_bytes(limbs); + let result = utils::felts_to_bytes(&limbs); assert_eq!(result.len(), 32); assert!(result.iter().all(|&b| b == 255)); } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 173e6a8d57..df2976650f 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -8,6 +8,7 @@ use miden_agglayer::{ EthAddressFormat, EthAmount, LeafData, + MetadataHash, OutputNoteData, ProofData, create_claim_note, @@ -88,7 +89,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { origin_network, origin_token_address, destination_network, - metadata, + metadata_hash, ) = claim_note_test_inputs(); // Convert AccountId to destination address bytes in the test @@ -98,7 +99,9 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let serial_num = builder.rng_mut().draw_word(); // Convert amount to EthAmount for the LeafData - let amount_eth = EthAmount::from_u32(claim_amount); + let mut claim_amount_bytes = [0u8; 32]; + claim_amount_bytes[28..32].copy_from_slice(&claim_amount.to_be_bytes()); + let amount_eth = EthAmount::new(claim_amount_bytes); // Convert Vec<[u8; 32]> to [SmtNode; 32] for SMT proofs let local_proof_array: [SmtNode; 32] = smt_proof_local_exit_root[0..32] @@ -129,7 +132,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { destination_network, destination_address: EthAddressFormat::new(destination_address), amount: amount_eth, - metadata, + metadata_hash: MetadataHash::new(metadata_hash), }; let output_note_data = OutputNoteData { diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 392795086c..b7992887a3 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -5,26 +5,23 @@ use alloc::sync::Arc; use alloc::vec::Vec; use anyhow::Context; -use miden_agglayer::agglayer_library; +use miden_agglayer::claim_note::Keccak256Output; +use miden_agglayer::utils::felts_to_bytes; +use miden_agglayer::{EthAddressFormat, EthAmount, LeafData, MetadataHash, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; -use miden_core_lib::handlers::bytes_to_packed_u32_felts; -use miden_core_lib::handlers::keccak256::KeccakPreimage; -use miden_crypto::FieldElement; +use miden_crypto::SequentialCommit; use miden_crypto::hash::keccak::Keccak256Digest; use miden_processor::AdviceInputs; use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Hasher, Word}; +use miden_protocol::{Felt, Word}; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; +use miden_tx::utils::hex_to_bytes; use serde::Deserialize; use super::test_utils::{execute_program_with_default_host, keccak_digest_to_word_strings}; -// LEAF_DATA_NUM_WORDS is defined as 8 in crypto_utils.masm, representing 8 Miden words of 4 felts -// each -const LEAF_DATA_FELTS: usize = 32; - /// Merkle proof verification vectors JSON embedded at compile time from the Foundry-generated file. const MERKLE_PROOF_VECTORS_JSON: &str = include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); @@ -45,146 +42,56 @@ static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = La .expect("failed to parse Merkle proof vectors JSON") }); -fn u32_words_to_solidity_bytes32_hex(words: &[u64]) -> String { - assert_eq!(words.len(), 8, "expected 8 u32 words = 32 bytes"); - let mut out = [0u8; 32]; +/// Leaf data test vectors JSON embedded at compile time from the Foundry-generated file. +const LEAF_VALUE_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); - for (i, &w) in words.iter().enumerate() { - let le = (w as u32).to_le_bytes(); - out[i * 4..i * 4 + 4].copy_from_slice(&le); - } +// TEST VECTOR STRUCTURES +// ================================================================================================ - let mut s = String::from("0x"); - for b in out { - s.push_str(&format!("{:02x}", b)); - } - s +/// Deserialized leaf value test vector from Solidity-generated JSON. +#[derive(Debug, Deserialize)] +struct LeafValueVector { + origin_network: u32, + origin_token_address: String, + destination_network: u32, + destination_address: String, + amount: String, + metadata_hash: String, + #[allow(dead_code)] + leaf_value: String, } -// Helper: parse 0x-prefixed hex into a fixed-size byte array -fn hex_to_fixed(s: &str) -> [u8; N] { - let s = s.strip_prefix("0x").unwrap_or(s); - assert_eq!(s.len(), N * 2, "expected {} hex chars", N * 2); - let mut out = [0u8; N]; - for i in 0..N { - out[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap(); +impl LeafValueVector { + /// Converts this test vector into a `LeafData` instance. + fn to_leaf_data(&self) -> LeafData { + LeafData { + origin_network: self.origin_network, + origin_token_address: EthAddressFormat::from_hex(&self.origin_token_address) + .expect("valid origin token address hex"), + destination_network: self.destination_network, + destination_address: EthAddressFormat::from_hex(&self.destination_address) + .expect("valid destination address hex"), + amount: EthAmount::new(hex_to_bytes(&self.amount).expect("valid amount hex")), + metadata_hash: MetadataHash::new( + hex_to_bytes(&self.metadata_hash).expect("valid metadata hash hex"), + ), + } } - out -} - -#[tokio::test] -async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { - let agglayer_lib = agglayer_library(); - - // === Values from hardhat test === - let leaf_type: u8 = 0; - let origin_network: u32 = 0; - let token_address: [u8; 20] = hex_to_fixed("0x1234567890123456789012345678901234567890"); - let destination_network: u32 = 1; - let destination_address: [u8; 20] = hex_to_fixed("0x0987654321098765432109876543210987654321"); - let amount_u64: u64 = 1; // 1e19 - let metadata_hash: [u8; 32] = - hex_to_fixed("0x2cdc14cacf6fec86a549f0e4d01e83027d3b10f29fa527c1535192c1ca1aac81"); - - // Expected hash value from Solidity implementation - let expected_hash = "0xf6825f6c59be2edf318d7251f4b94c0e03eb631b76a0e7b977fd8ed3ff925a3f"; - - // abi.encodePacked( - // uint8, uint32, address, uint32, address, uint256, bytes32 - // ) - let mut amount_u256_be = [0u8; 32]; - amount_u256_be[24..32].copy_from_slice(&amount_u64.to_be_bytes()); - - let mut input_u8 = Vec::with_capacity(113); - input_u8.push(leaf_type); - input_u8.extend_from_slice(&origin_network.to_be_bytes()); - input_u8.extend_from_slice(&token_address); - input_u8.extend_from_slice(&destination_network.to_be_bytes()); - input_u8.extend_from_slice(&destination_address); - input_u8.extend_from_slice(&amount_u256_be); - input_u8.extend_from_slice(&metadata_hash); - - let len_bytes = input_u8.len(); - assert_eq!(len_bytes, 113); - - let preimage = KeccakPreimage::new(input_u8.clone()); - let mut input_felts = bytes_to_packed_u32_felts(&input_u8); - // Pad to LEAF_DATA_FELTS (128 bytes) as expected by the downstream code - input_felts.resize(LEAF_DATA_FELTS, Felt::ZERO); - assert_eq!(input_felts.len(), LEAF_DATA_FELTS); - - // Arbitrary key to store input in advice map (in prod this is RPO(input_felts)) - let key: Word = Hasher::hash_elements(&input_felts); - let advice_inputs = AdviceInputs::default().with_map(vec![(key, input_felts)]); - - let source = format!( - r#" - use miden::core::sys - use miden::core::crypto::hashes::keccak256 - use miden::agglayer::crypto_utils - - begin - push.{key} - - exec.crypto_utils::get_leaf_value - exec.sys::truncate_stack - end - "# - ); - - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(agglayer_lib.clone()) - .unwrap() - .assemble_program(&source) - .unwrap(); - - let exec_output = execute_program_with_default_host(program, Some(advice_inputs)).await?; - - let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); - let hex_digest = u32_words_to_solidity_bytes32_hex(&digest); - - let keccak256_digest: Vec = preimage.digest().as_ref().iter().map(Felt::as_int).collect(); - let keccak256_hex_digest = u32_words_to_solidity_bytes32_hex(&keccak256_digest); - - assert_eq!(digest, keccak256_digest); - assert_eq!(hex_digest, keccak256_hex_digest); - assert_eq!(hex_digest, expected_hash); - Ok(()) } -#[tokio::test] -async fn test_solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { - let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; - - // Validate array lengths - assert_eq!(merkle_paths.leaves.len(), merkle_paths.roots.len()); - // paths have 32 nodes for each leaf/root, so the overall paths length should be 32 times longer - // than leaves/roots length - assert_eq!(merkle_paths.leaves.len() * 32, merkle_paths.merkle_paths.len()); - - for leaf_index in 0..32 { - let source = merkle_proof_verification_code(leaf_index, merkle_paths); - - let tx_script = CodeBuilder::new() - .with_statically_linked_library(&agglayer_library())? - .compile_tx_script(source)?; +// HELPER FUNCTIONS +// ================================================================================================ - TransactionContextBuilder::with_existing_mock_account() - .tx_script(tx_script.clone()) - .build()? - .execute() - .await - .context(format!("failed to execute transaction with leaf index {leaf_index}"))?; +fn felts_to_le_bytes(limbs: &[Felt]) -> Vec { + let mut bytes = Vec::with_capacity(limbs.len() * 4); + for limb in limbs.iter() { + let u32_value = limb.as_int() as u32; + bytes.extend_from_slice(&u32_value.to_le_bytes()); } - - Ok(()) + bytes } -// HELPER FUNCTIONS -// ================================================================================================ - fn merkle_proof_verification_code( index: usize, merkle_paths: &MerkleProofVerificationFile, @@ -200,7 +107,7 @@ fn merkle_proof_verification_code( // memory pointers for the merkle path and the expected root never change store_path_source.push_str(&format!( " -\tpush.[{node_hi}] mem_storew_be.{} dropw +\tpush.[{node_hi}] mem_storew_be.{} dropw \tpush.[{node_lo}] mem_storew_be.{} dropw ", height * 8, @@ -219,14 +126,14 @@ fn merkle_proof_verification_code( format!( r#" use miden::agglayer::crypto_utils - + begin # store the merkle path to the memory (double word slots from 0 to 248) {store_path_source} # => [] # store the root to the memory (double word slot 256) - push.[{root_lo}] mem_storew_be.256 dropw + push.[{root_lo}] mem_storew_be.256 dropw push.[{root_hi}] mem_storew_be.260 dropw # => [] @@ -246,3 +153,200 @@ fn merkle_proof_verification_code( "# ) } + +// TESTS +// ================================================================================================ + +/// Test that the `pack_leaf_data` procedure produces the correct byte layout. +#[tokio::test] +async fn pack_leaf_data() -> anyhow::Result<()> { + let vector: LeafValueVector = + serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("Failed to parse leaf value vector"); + + let leaf_data = vector.to_leaf_data(); + + // Build expected bytes + let mut expected_packed_bytes: Vec = Vec::new(); + expected_packed_bytes.push(0u8); + expected_packed_bytes.extend_from_slice(&leaf_data.origin_network.to_be_bytes()); + expected_packed_bytes.extend_from_slice(leaf_data.origin_token_address.as_bytes()); + expected_packed_bytes.extend_from_slice(&leaf_data.destination_network.to_be_bytes()); + expected_packed_bytes.extend_from_slice(leaf_data.destination_address.as_bytes()); + expected_packed_bytes.extend_from_slice(leaf_data.amount.as_bytes()); + let metadata_hash_bytes: [u8; 32] = hex_to_bytes(&vector.metadata_hash).unwrap(); + expected_packed_bytes.extend_from_slice(&metadata_hash_bytes); + assert_eq!(expected_packed_bytes.len(), 113); + + let agglayer_lib = agglayer_library(); + let leaf_data_elements = leaf_data.to_elements(); + let leaf_data_bytes: Vec = felts_to_bytes(&leaf_data_elements); + assert_eq!( + leaf_data_bytes.len(), + 128, + "expected 8 words * 4 felts * 4 bytes per felt = 128 bytes" + ); + assert_eq!(leaf_data_bytes[116..], vec![0; 12], "the last 3 felts are pure padding"); + assert_eq!(leaf_data_bytes[3], expected_packed_bytes[0], "the first byte is the leaf type"); + assert_eq!( + leaf_data_bytes[4..8], + expected_packed_bytes[1..5], + "the next 4 bytes are the origin network" + ); + assert_eq!( + leaf_data_bytes[8..28], + expected_packed_bytes[5..25], + "the next 20 bytes are the origin token address" + ); + assert_eq!( + leaf_data_bytes[28..32], + expected_packed_bytes[25..29], + "the next 4 bytes are the destination network" + ); + assert_eq!( + leaf_data_bytes[32..52], + expected_packed_bytes[29..49], + "the next 20 bytes are the destination address" + ); + assert_eq!( + leaf_data_bytes[52..84], + expected_packed_bytes[49..81], + "the next 32 bytes are the amount" + ); + assert_eq!( + leaf_data_bytes[84..116], + expected_packed_bytes[81..113], + "the next 32 bytes are the metadata hash" + ); + + assert_eq!(leaf_data_bytes[3..116], expected_packed_bytes, "byte packing is as expected"); + + let key: Word = leaf_data.to_commitment(); + let advice_inputs = AdviceInputs::default().with_map(vec![(key, leaf_data_elements.clone())]); + + let source = format!( + r#" + use miden::core::mem + use miden::agglayer::crypto_utils + + const LEAF_DATA_START_PTR = 0 + const LEAF_DATA_NUM_WORDS = 8 + + begin + push.{key} + + adv.push_mapval + push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS + exec.mem::pipe_preimage_to_memory drop + + exec.crypto_utils::pack_leaf_data + end + "# + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib.clone()) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, Some(advice_inputs)).await?; + + // Read packed elements from memory at addresses 0..29 + let ctx = miden_processor::ContextId::root(); + let err_ctx = (); + + let packed_elements: Vec = (0..29u32) + .map(|addr| { + exec_output + .memory + .read_element(ctx, Felt::from(addr), &err_ctx) + .expect("address should be valid") + }) + .collect(); + + let packed_bytes: Vec = felts_to_le_bytes(&packed_elements); + + // push 3 more zero bytes for packing, since `pack_leaf_data` should leave us with the last 3 + // bytes set to 0 (prep for hashing, where padding bytes must be 0) + expected_packed_bytes.extend_from_slice(&[0u8; 3]); + + assert_eq!( + &packed_bytes, &expected_packed_bytes, + "Packed bytes don't match expected Solidity encoding" + ); + + Ok(()) +} + +#[tokio::test] +async fn get_leaf_value() -> anyhow::Result<()> { + let vector: LeafValueVector = + serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("Failed to parse leaf value vector"); + + let leaf_data = vector.to_leaf_data(); + let key: Word = leaf_data.to_commitment(); + let advice_inputs = AdviceInputs::default().with_map(vec![(key, leaf_data.to_elements())]); + + let source = format!( + r#" + use miden::core::mem + use miden::core::sys + use miden::agglayer::crypto_utils + + begin + push.{key} + exec.crypto_utils::get_leaf_value + exec.sys::truncate_stack + end + "# + ); + let agglayer_lib = agglayer_library(); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib.clone()) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, Some(advice_inputs)).await?; + let computed_leaf_value: Vec = exec_output.stack[0..8].to_vec(); + let expected_leaf_value_bytes: [u8; 32] = + hex_to_bytes(&vector.leaf_value).expect("valid leaf value hex"); + let expected_leaf_value: Vec = + Keccak256Output::from(expected_leaf_value_bytes).to_elements(); + + assert_eq!(computed_leaf_value, expected_leaf_value); + Ok(()) +} + +#[tokio::test] +async fn test_solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { + let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; + + // Validate array lengths + assert_eq!(merkle_paths.leaves.len(), merkle_paths.roots.len()); + // paths have 32 nodes for each leaf/root, so the overall paths length should be 32 times longer + // than leaves/roots length + assert_eq!(merkle_paths.leaves.len() * 32, merkle_paths.merkle_paths.len()); + + for leaf_index in 0..32 { + let source = merkle_proof_verification_code(leaf_index, merkle_paths); + + let tx_script = CodeBuilder::new() + .with_statically_linked_library(&agglayer_library())? + .compile_tx_script(source)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script.clone()) + .build()? + .execute() + .await + .context(format!("failed to execute transaction with leaf index {leaf_index}"))?; + } + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 2083a9dd36..f1bf21ea6d 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -109,7 +109,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { let eth_address = EthAddressFormat::from_account_id(*original_account_id); let address_felts = eth_address.to_elements().to_vec(); - let le: Vec = address_felts + let limbs: Vec = address_felts .iter() .map(|f| { let val = f.as_int(); @@ -118,13 +118,13 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { }) .collect(); - assert_eq!(le[4], 0, "test {}: expected msw limb (le[4]) to be zero", idx); + let limb0 = limbs[0]; + let limb1 = limbs[1]; + let limb2 = limbs[2]; + let limb3 = limbs[3]; + let limb4 = limbs[4]; - let addr0 = le[0]; - let addr1 = le[1]; - let addr2 = le[2]; - let addr3 = le[3]; - let addr4 = le[4]; + assert_eq!(limb0, 0, "test {}: expected msb limb (limb0) to be zero", idx); let account_id_felts: [Felt; 2] = (*original_account_id).into(); let expected_prefix = account_id_felts[0].as_int(); @@ -141,7 +141,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { exec.sys::truncate_stack end "#, - addr4, addr3, addr2, addr1, addr0 + limb4, limb3, limb2, limb1, limb0 ); let program = Assembler::new(Arc::new(DefaultSourceManager::default())) diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index af1e987ae3..8e00b9084c 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -68,7 +68,7 @@ pub async fn execute_program_with_default_host( /// - origin_network: u32 /// - origin_token_address: [u8; 20] /// - destination_network: u32 -/// - metadata: [u32; 8] +/// - metadata: [u8; 32] pub type ClaimNoteTestInputs = ( Vec<[u8; 32]>, Vec<[u8; 32]>, @@ -78,7 +78,7 @@ pub type ClaimNoteTestInputs = ( u32, [u8; 20], u32, - [u32; 8], + [u8; 32], ); /// Returns dummy test inputs for creating CLAIM notes with native types. @@ -96,9 +96,9 @@ pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { let global_index = [0u32, 0, 0, 0, 0, 1, 0, 2]; let mainnet_exit_root: [u8; 32] = [ - 0x7e, 0x3b, 0xd3, 0xe3, 0x04, 0xb4, 0x64, 0x1f, 0xd1, 0x53, 0x2f, 0x47, 0xfa, 0xc9, 0x56, - 0xe4, 0x13, 0x03, 0x47, 0x02, 0x0f, 0x08, 0xa3, 0x72, 0xa2, 0x57, 0xf2, 0x82, 0x1f, 0x63, - 0x8a, 0x60, + 0x05, 0xc2, 0xbe, 0x9d, 0xd7, 0xf4, 0x7e, 0xc6, 0x29, 0xae, 0x6a, 0xc1, 0x1a, 0x24, 0xb5, + 0x28, 0x59, 0xfd, 0x35, 0x8c, 0x31, 0x39, 0x00, 0xf5, 0x23, 0x1f, 0x84, 0x58, 0x63, 0x22, + 0xb5, 0x06, ]; let rollup_exit_root: [u8; 32] = [ @@ -116,7 +116,7 @@ pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { let destination_network = 2u32; - let metadata: [u32; 8] = [0; 8]; + let metadata_hash: [u8; 32] = [0u8; 32]; ( smt_proof_local_exit_root, @@ -127,7 +127,7 @@ pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { origin_network, origin_token_address, destination_network, - metadata, + metadata_hash, ) } */ diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 8b4a9c2200..6014650545 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -4,7 +4,7 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use miden_agglayer::utils::felts_to_u256_bytes; +use miden_agglayer::utils::felts_to_bytes; use miden_agglayer::{ExitRoot, UpdateGerNote, agglayer_library, create_existing_bridge_account}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; @@ -105,7 +105,7 @@ async fn test_compute_ger_basic() -> anyhow::Result<()> { let expected_ger_preimage = KeccakPreimage::new(ger_preimage.clone()); let expected_ger_felts: [Felt; 8] = expected_ger_preimage.digest().as_ref().try_into().unwrap(); - let ger_bytes = felts_to_u256_bytes(expected_ger_felts); + let ger_bytes: [u8; 32] = felts_to_bytes(&expected_ger_felts).try_into().unwrap(); let ger = ExitRoot::from(ger_bytes); // sanity check From 8ad690b436b700ab838183192abe2a1959c598d9 Mon Sep 17 00:00:00 2001 From: Marti Date: Sat, 14 Feb 2026 19:12:32 +0100 Subject: [PATCH 04/21] feat(AggLayer): `GlobalIndex` wrapper (#2407) * feat: GlobalIndex wrapper around 32 bytes * feat: reverse byte ordering checks for global index * Update crates/miden-agglayer/src/claim_note.rs --- .../miden-agglayer/asm/bridge/bridge_in.masm | 17 +- .../asm/bridge/eth_address.masm | 30 +--- crates/miden-agglayer/asm/bridge/utils.masm | 32 +++- crates/miden-agglayer/src/claim_note.rs | 10 +- .../src/eth_types/global_index.rs | 154 ++++++++++++++++++ crates/miden-agglayer/src/eth_types/mod.rs | 2 + crates/miden-agglayer/src/lib.rs | 2 +- .../miden-testing/tests/agglayer/bridge_in.rs | 3 +- .../tests/agglayer/global_index.rs | 42 +++-- .../tests/agglayer/test_utils.rs | 14 +- 10 files changed, 247 insertions(+), 59 deletions(-) create mode 100644 crates/miden-agglayer/src/eth_types/global_index.rs diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index fd7cebfd51..af1cc28201 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -1,4 +1,5 @@ use miden::agglayer::crypto_utils +use miden::agglayer::utils use miden::core::crypto::hashes::keccak256 use miden::core::mem use miden::protocol::active_account @@ -122,6 +123,9 @@ end #! Assert the global index is valid for a mainnet deposit. #! +#! Each element of the global index is a LE-packed u32 felt (as produced by +#! `bytes_to_packed_u32_felts` / `GlobalIndex::to_elements()`). +#! #! Inputs: [GLOBAL_INDEX[8]] #! Outputs: [leaf_index] #! @@ -136,15 +140,18 @@ pub proc process_global_index_mainnet # the top 191 bits of the global index are zero repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end - # the next element is a u32 mainnet flag bit - # enforce that this limb is one - # => [mainnet_flag, GLOBAL_INDEX[6..8], LEAF_VALUE[8]] + # the next element is the mainnet flag (LE-packed u32) + # byte-swap to get the BE value, then assert it is exactly 1 + # => [mainnet_flag_le, rollup_index_le, leaf_index_le, ...] + exec.utils::swap_u32_bytes assert.err=ERR_BRIDGE_NOT_MAINNET - # the next element is a u32 rollup index, must be zero for a mainnet deposit + # the next element is the rollup index, must be zero for a mainnet deposit + # (zero is byte-order-independent, so no swap needed) assertz.err=ERR_ROLLUP_INDEX_NON_ZERO - # finally, the leaf index = lowest 32 bits = last limb + # the leaf index is the last element; byte-swap from LE to BE to get the actual index + exec.utils::swap_u32_bytes # => [leaf_index] end diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index aa01df3480..bcf418ce21 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -1,3 +1,4 @@ +use miden::agglayer::utils use miden::core::crypto::hashes::keccak256 use miden::core::word @@ -14,31 +15,6 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" # ETHEREUM ADDRESS PROCEDURES # ================================================================================================= -#! Swaps byte order in a u32 limb (LE <-> BE). -#! -#! Inputs: [value] -#! Outputs: [swapped] -proc swap_u32_bytes - # part0 = (value & 0xFF) << 24 - dup u32and.0xFF u32shl.24 - # => [value, part0] - - # part1 = ((value >> 8) & 0xFF) << 16 - dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or - # => [value, part01] - - # part2 = ((value >> 16) & 0xFF) << 8 - dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or - # => [value, part012] - - # part3 = (value >> 24) - dup.1 u32shr.24 u32or - # => [value, swapped] - - swap drop - # => [swapped] -end - #! Builds a single felt from two u32 limbs (little-endian limb order). #! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. #! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping @@ -52,9 +28,9 @@ proc build_felt # => [lo_be, hi_be] # limbs are little-endian bytes; swap to big-endian for building account ID - exec.swap_u32_bytes + exec.utils::swap_u32_bytes swap - exec.swap_u32_bytes + exec.utils::swap_u32_bytes swap # => [lo, hi] diff --git a/crates/miden-agglayer/asm/bridge/utils.masm b/crates/miden-agglayer/asm/bridge/utils.masm index 598a392509..8a9d460d8c 100644 --- a/crates/miden-agglayer/asm/bridge/utils.masm +++ b/crates/miden-agglayer/asm/bridge/utils.masm @@ -1,4 +1,4 @@ -# Utility module containing helper procedures for the double word handling. +# Utility module containing helper procedures for double word handling and byte manipulation. # TYPE ALIASES # ================================================================================================= @@ -7,7 +7,35 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } type DoubleWord = struct { word_lo: BeWord, word_hi: BeWord } type MemoryAddress = u32 -# PUBLIC INTERFACE +# BYTE MANIPULATION +# ================================================================================================= + +#! Swaps byte order in a u32 limb (LE <-> BE). +#! +#! Inputs: [value] +#! Outputs: [swapped] +pub proc swap_u32_bytes + # part0 = (value & 0xFF) << 24 + dup u32and.0xFF u32shl.24 + # => [part0, value] + + # part1 = ((value >> 8) & 0xFF) << 16 + dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or + # => [part01, value] + + # part2 = ((value >> 16) & 0xFF) << 8 + dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or + # => [part012, value] + + # part3 = (value >> 24) + dup.1 u32shr.24 u32or + # => [swapped, value] + + swap drop + # => [swapped] +end + +# DOUBLE WORD MEMORY OPERATIONS # ================================================================================================= #! Stores two words to the provided global memory address. diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 084d2d7254..9eb906b117 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -19,7 +19,7 @@ use miden_protocol::note::{ }; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; -use crate::{EthAddressFormat, EthAmount, MetadataHash, claim_script}; +use crate::{EthAddressFormat, EthAmount, GlobalIndex, MetadataHash, claim_script}; // CLAIM NOTE STRUCTURES // ================================================================================================ @@ -66,8 +66,8 @@ pub struct ProofData { pub smt_proof_local_exit_root: [SmtNode; 32], /// SMT proof for rollup exit root (32 SMT nodes) pub smt_proof_rollup_exit_root: [SmtNode; 32], - /// Global index (uint256 as 8 u32 values) - pub global_index: [u32; 8], + /// Global index (uint256 as 32 bytes) + pub global_index: GlobalIndex, /// Mainnet exit root hash pub mainnet_exit_root: ExitRoot, /// Rollup exit root hash @@ -92,8 +92,8 @@ impl SequentialCommit for ProofData { elements.extend(node_felts); } - // Global index (uint256 as 8 u32 felts) - elements.extend(self.global_index.iter().map(|&v| Felt::new(v as u64))); + // Global index (uint256 as 32 bytes) + elements.extend(self.global_index.to_elements()); // Mainnet exit root (bytes32 as 8 u32 felts) let mainnet_exit_root_felts = self.mainnet_exit_root.to_elements(); diff --git a/crates/miden-agglayer/src/eth_types/global_index.rs b/crates/miden-agglayer/src/eth_types/global_index.rs new file mode 100644 index 0000000000..4c4688edc0 --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/global_index.rs @@ -0,0 +1,154 @@ +use alloc::vec::Vec; + +use miden_core_lib::handlers::bytes_to_packed_u32_felts; +use miden_protocol::Felt; +use miden_protocol::utils::{HexParseError, hex_to_bytes}; + +// ================================================================================================ +// GLOBAL INDEX ERROR +// ================================================================================================ + +/// Error type for GlobalIndex validation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GlobalIndexError { + /// The leading 160 bits of the global index are not zero. + LeadingBitsNonZero, + /// The mainnet flag is not 1. + InvalidMainnetFlag, + /// The rollup index is not zero for a mainnet deposit. + RollupIndexNonZero, +} + +// ================================================================================================ +// GLOBAL INDEX +// ================================================================================================ + +/// Represents an AggLayer global index as a 256-bit value (32 bytes). +/// +/// The global index is a uint256 that encodes (from MSB to LSB): +/// - Top 160 bits (limbs 0-4): must be zero +/// - 32 bits (limb 5): mainnet flag (value = 1 for mainnet, 0 for rollup) +/// - 32 bits (limb 6): rollup index (must be 0 for mainnet deposits) +/// - 32 bits (limb 7): leaf index (deposit index in the local exit tree) +/// +/// Bytes are stored in big-endian order, matching Solidity's uint256 representation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GlobalIndex([u8; 32]); + +impl GlobalIndex { + /// Creates a [`GlobalIndex`] from a hex string (with or without "0x" prefix). + /// + /// The hex string should represent a Solidity uint256 in big-endian format + /// (64 hex characters for 32 bytes). + pub fn from_hex(hex_str: &str) -> Result { + let bytes: [u8; 32] = hex_to_bytes(hex_str)?; + Ok(Self(bytes)) + } + + /// Creates a new [`GlobalIndex`] from a 32-byte array (big-endian). + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Validates that this is a valid mainnet deposit global index. + /// + /// Checks that: + /// - The top 160 bits (limbs 0-4, bytes 0-19) are zero + /// - The mainnet flag (limb 5, bytes 20-23) is exactly 1 + /// - The rollup index (limb 6, bytes 24-27) is 0 + pub fn validate_mainnet(&self) -> Result<(), GlobalIndexError> { + // Check limbs 0-4 are zero (bytes 0-19) + if self.0[0..20].iter().any(|&b| b != 0) { + return Err(GlobalIndexError::LeadingBitsNonZero); + } + + // Check mainnet flag limb (bytes 20-23) is exactly 1 + if !self.is_mainnet() { + return Err(GlobalIndexError::InvalidMainnetFlag); + } + + // Check rollup index is zero (bytes 24-27) + if u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) != 0 { + return Err(GlobalIndexError::RollupIndexNonZero); + } + + Ok(()) + } + + /// Returns the leaf index (limb 7, lowest 32 bits). + pub fn leaf_index(&self) -> u32 { + u32::from_be_bytes([self.0[28], self.0[29], self.0[30], self.0[31]]) + } + + /// Returns the rollup index (limb 6). + pub fn rollup_index(&self) -> u32 { + u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) + } + + /// Returns true if this is a mainnet deposit (mainnet flag = 1). + pub fn is_mainnet(&self) -> bool { + u32::from_be_bytes([self.0[20], self.0[21], self.0[22], self.0[23]]) == 1 + } + + /// Converts to field elements for note storage / MASM processing. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) + } + + /// Returns the raw 32-byte array (big-endian). + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use miden_core::FieldElement; + + use super::*; + + #[test] + fn test_mainnet_global_indices_from_production() { + // Real mainnet global indices from production + // Format: (1 << 64) + leaf_index for mainnet deposits + // 18446744073709786619 = 0x1_0000_0000_0003_95FB (leaf_index = 235003) + // 18446744073709786590 = 0x1_0000_0000_0003_95DE (leaf_index = 234974) + let test_cases = [ + ("0x00000000000000000000000000000000000000000000000100000000000395fb", 235003u32), + ("0x00000000000000000000000000000000000000000000000100000000000395de", 234974u32), + ]; + + for (hex, expected_leaf_index) in test_cases { + let gi = GlobalIndex::from_hex(hex).expect("valid hex"); + + // Validate as mainnet + assert!(gi.validate_mainnet().is_ok(), "should be valid mainnet global index"); + + // Construction sanity checks + assert!(gi.is_mainnet()); + assert_eq!(gi.rollup_index(), 0); + assert_eq!(gi.leaf_index(), expected_leaf_index); + + // Verify to_elements produces correct LE-packed u32 felts + // -------------------------------------------------------------------------------- + + let elements = gi.to_elements(); + assert_eq!(elements.len(), 8); + + // leading zeros + assert_eq!(elements[0..5], [Felt::ZERO; 5]); + + // mainnet flag: BE value 1 → LE-packed as 0x01000000 + assert_eq!(elements[5], Felt::new(u32::from_le_bytes(1u32.to_be_bytes()) as u64)); + + // rollup index + assert_eq!(elements[6], Felt::ZERO); + + // leaf index: BE value → LE-packed + assert_eq!( + elements[7], + Felt::new(u32::from_le_bytes(expected_leaf_index.to_be_bytes()) as u64) + ); + } + } +} diff --git a/crates/miden-agglayer/src/eth_types/mod.rs b/crates/miden-agglayer/src/eth_types/mod.rs index 791486e35a..c17753970a 100644 --- a/crates/miden-agglayer/src/eth_types/mod.rs +++ b/crates/miden-agglayer/src/eth_types/mod.rs @@ -1,7 +1,9 @@ pub mod address; pub mod amount; +pub mod global_index; pub mod metadata_hash; pub use address::EthAddressFormat; pub use amount::EthAmount; +pub use global_index::{GlobalIndex, GlobalIndexError}; pub use metadata_hash::MetadataHash; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index cc34fc5545..120a721239 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -42,7 +42,7 @@ pub use claim_note::{ SmtNode, create_claim_note, }; -pub use eth_types::{EthAddressFormat, EthAmount, MetadataHash}; +pub use eth_types::{EthAddressFormat, EthAmount, GlobalIndex, GlobalIndexError, MetadataHash}; pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index df2976650f..402c9fbb00 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -7,6 +7,7 @@ use miden_agglayer::{ ClaimNoteStorage, EthAddressFormat, EthAmount, + GlobalIndex, LeafData, MetadataHash, OutputNoteData, @@ -121,7 +122,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let proof_data = ProofData { smt_proof_local_exit_root: local_proof_array, smt_proof_rollup_exit_root: rollup_proof_array, - global_index, + global_index: GlobalIndex::new(global_index), mainnet_exit_root: ExitRoot::from(mainnet_exit_root), rollup_exit_root: ExitRoot::from(rollup_exit_root), }; diff --git a/crates/miden-testing/tests/agglayer/global_index.rs b/crates/miden-testing/tests/agglayer/global_index.rs index 92c53ef61e..ad29d767d0 100644 --- a/crates/miden-testing/tests/agglayer/global_index.rs +++ b/crates/miden-testing/tests/agglayer/global_index.rs @@ -2,12 +2,12 @@ extern crate alloc; use alloc::sync::Arc; -use miden_agglayer::agglayer_library; use miden_agglayer::errors::{ ERR_BRIDGE_NOT_MAINNET, ERR_LEADING_BITS_NON_ZERO, ERR_ROLLUP_INDEX_NON_ZERO, }; +use miden_agglayer::{GlobalIndex, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_processor::Program; @@ -15,8 +15,10 @@ use miden_testing::{ExecError, assert_execution_error}; use crate::agglayer::test_utils::execute_program_with_default_host; -fn assemble_process_global_index_program(global_index_be_u32_limbs: [u32; 8]) -> Program { - let [g0, g1, g2, g3, g4, g5, g6, g7] = global_index_be_u32_limbs; +fn assemble_process_global_index_program(global_index: GlobalIndex) -> Program { + // Convert GlobalIndex to 8 field elements (big-endian: [0]=MSB, [7]=LSB) + let elements = global_index.to_elements(); + let [g0, g1, g2, g3, g4, g5, g6, g7] = elements.try_into().unwrap(); let script_code = format!( r#" @@ -42,10 +44,15 @@ fn assemble_process_global_index_program(global_index_be_u32_limbs: [u32; 8]) -> #[tokio::test] async fn test_process_global_index_mainnet_returns_leaf_index() -> anyhow::Result<()> { - // 256-bit globalIndex encoded as 8 u32 limbs (big-endian): - // [top 191 bits = 0, mainnet flag = 1, rollup_index = 0, leaf_index = 2] - let global_index = [0, 0, 0, 0, 0, 1, 0, 2]; - let program = assemble_process_global_index_program(global_index); + // Global index format (32 bytes, big-endian like Solidity uint256): + // - bytes[0..20]: leading zeros + // - bytes[20..24]: mainnet_flag = 1 (BE u32) + // - bytes[24..28]: rollup_index = 0 (BE u32) + // - bytes[28..32]: leaf_index = 2 (BE u32) + let mut bytes = [0u8; 32]; + bytes[23] = 1; // mainnet flag = 1 (BE: LSB at byte 23) + bytes[31] = 2; // leaf index = 2 (BE: LSB at byte 31) + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let exec_output = execute_program_with_default_host(program, None).await?; @@ -55,8 +62,11 @@ async fn test_process_global_index_mainnet_returns_leaf_index() -> anyhow::Resul #[tokio::test] async fn test_process_global_index_mainnet_rejects_non_zero_leading_bits() { - let global_index = [1, 0, 0, 0, 0, 1, 0, 2]; - let program = assemble_process_global_index_program(global_index); + let mut bytes = [0u8; 32]; + bytes[3] = 1; // non-zero leading bits (BE: LSB of first u32 limb) + bytes[23] = 1; // mainnet flag = 1 + bytes[31] = 2; // leaf index = 2 + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_LEADING_BITS_NON_ZERO); @@ -64,9 +74,10 @@ async fn test_process_global_index_mainnet_rejects_non_zero_leading_bits() { #[tokio::test] async fn test_process_global_index_mainnet_rejects_flag_limb_upper_bits() { - // limb5 is the mainnet flag; only the lowest bit is allowed - let global_index = [0, 0, 0, 0, 0, 3, 0, 2]; - let program = assemble_process_global_index_program(global_index); + let mut bytes = [0u8; 32]; + bytes[23] = 3; // mainnet flag limb = 3 (upper bits set, only lowest bit allowed) + bytes[31] = 2; // leaf index = 2 + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_BRIDGE_NOT_MAINNET); @@ -74,8 +85,11 @@ async fn test_process_global_index_mainnet_rejects_flag_limb_upper_bits() { #[tokio::test] async fn test_process_global_index_mainnet_rejects_non_zero_rollup_index() { - let global_index = [0, 0, 0, 0, 0, 1, 7, 2]; - let program = assemble_process_global_index_program(global_index); + let mut bytes = [0u8; 32]; + bytes[23] = 1; // mainnet flag = 1 + bytes[27] = 7; // rollup index = 7 (BE: LSB at byte 27) + bytes[31] = 2; // leaf index = 2 + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_ROLLUP_INDEX_NON_ZERO); diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 8e00b9084c..d33a7225ef 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -62,7 +62,7 @@ pub async fn execute_program_with_default_host( /// Contains native types for the new ClaimNoteParams structure: /// - smt_proof_local_exit_root: `Vec<[u8; 32]>` (256 bytes32 values) /// - smt_proof_rollup_exit_root: `Vec<[u8; 32]>` (256 bytes32 values) -/// - global_index: [u32; 8] +/// - global_index: [u8; 32] /// - mainnet_exit_root: [u8; 32] /// - rollup_exit_root: [u8; 32] /// - origin_network: u32 @@ -72,7 +72,7 @@ pub async fn execute_program_with_default_host( pub type ClaimNoteTestInputs = ( Vec<[u8; 32]>, Vec<[u8; 32]>, - [u32; 8], + [u8; 32], [u8; 32], [u8; 32], u32, @@ -92,8 +92,14 @@ pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { // Create SMT proofs with 32 bytes32 values each (SMT path depth) let smt_proof_local_exit_root = vec![[0u8; 32]; 32]; let smt_proof_rollup_exit_root = vec![[0u8; 32]; 32]; - // Global index format: [top 5 limbs = 0, mainnet_flag = 1, rollup_index = 0, leaf_index = 2] - let global_index = [0u32, 0, 0, 0, 0, 1, 0, 2]; + // Global index format (32 bytes, big-endian like Solidity uint256): + // - bytes[0..20]: leading zeros (5 limbs) + // - bytes[20..24]: mainnet_flag = 1 (BE u32) + // - bytes[24..28]: rollup_index = 0 (BE u32) + // - bytes[28..32]: leaf_index = 2 (BE u32) + let mut global_index = [0u8; 32]; + global_index[23] = 1; // mainnet flag = 1 (BE: LSB at byte 23) + global_index[31] = 2; // leaf index = 2 (BE: LSB at byte 31) let mainnet_exit_root: [u8; 32] = [ 0x05, 0xc2, 0xbe, 0x9d, 0xd7, 0xf4, 0x7e, 0xc6, 0x29, 0xae, 0x6a, 0xc1, 0x1a, 0x24, 0xb5, From c5cc3d2abf4cf295b3f454973a41c80769bb6b7a Mon Sep 17 00:00:00 2001 From: Marti Date: Sun, 15 Feb 2026 11:46:45 +0100 Subject: [PATCH 05/21] feat(AggLayer): e2e bridge-in flow with real bridge data (#2413) * feat: infra for reading full real claim data chore: add claim test gen to makefile * chore: move deser structs to test_utils.rs * feat: use real claim data in bridge in test * fix: reverse keccak hash elements in ProofData serialization for MASM compatibility The keccak256::merge MASM function applies word::reverse to its input words before hashing. mem_stream (used to load SMT proof nodes) does NOT apply per-word reversal, while loc_loadw_be (used to load the current hash) DOES. To produce consistent per-word reversed format on the stack for both the current hash (via loc_storew_be/loc_loadw_be) and the path nodes (via mem_stream), SMT proof nodes must be stored in fully reversed element order in memory. Exit roots are per-word reversed because they are loaded via mem_load_double_word (which uses mem_loadw_be), and the per-word reversal by mem_loadw_be undoes the storage reversal to produce natural order on the stack. Also adds diagnostic and integration tests for Merkle proof verification using claim_asset_vectors data. Co-authored-by: marti fix: simplify bridge_in test assertions for stubbed scale function The test now correctly verifies that the bridge-in flow validates the Merkle proof and creates an output note, without asserting specific note content that depends on the stubbed scale_u256_to_native_amount function. Removes diagnostic test test_claim_asset_verify_leaf_via_advice_map which has a stack management issue unrelated to the core fix. Co-authored-by: marti chore: fix warnings and remove diagnostic test - Remove unused imports (Asset, NoteRecipient, NoteStorage) - Remove unused leaf_elements variable - Remove test_claim_asset_verify_leaf_via_advice_map diagnostic test (covered by test_bridge_in_claim_to_p2id) Co-authored-by: marti refactor: use data-level fix without MASM changes The keccak256::merge function (from external miden-core-lib) applies word::reverse internally and expects per-word reversed input. Since loc_loadw_be naturally produces per-word reversal, changing to loc_loadw_le would break merge's byte ordering. Instead, fix at the data level: - SMT proof nodes: fully reversed (to_memory_elements) so mem_stream's half-swap produces per-word reversed format matching loc_loadw_be output - Exit roots: per-word reversed (to_word_reversed_elements) so mem_load_double_word's mem_loadw_be reversal produces natural order - No MASM code changes needed Co-authored-by: marti chore: remove diagnostic tests Remove 7 diagnostic/exploratory tests that were added during debugging. The pre-existing tests (pack_leaf_data, get_leaf_value, test_solidity_verify_merkle_proof_compatibility) plus the fixed test_bridge_in_claim_to_p2id provide sufficient coverage. Co-authored-by: marti * refactor: replace mem_stream with mem_loadw_be in calculate_root Replace mem_stream (which requires RPO hasher state padding) with two explicit mem_loadw_be calls for loading Merkle path nodes. This uses the same instruction as loc_loadw_be for the current hash, ensuring both undergo identical element ordering. The change eliminates 12 elements of RPO padding from the stack, simplifies pointer management, and makes the memory access pattern more explicit. Co-authored-by: marti * chore: mem_loadw_le for merkle paths * chore: use LE for local word storage * chore: mem_loadw_le for expected root * test: prepare leaf in LE-felt order * chore: use LE for word storage in MMR frontier and canonical zeros - Replace loc_storew_be/loc_loadw_be with loc_storew_le/loc_loadw_le - Replace mem_load_double_word with inline mem_loadw_le - Replace mem_store_double_word with inline mem_storew_le - Update canonical_zeros.masm and build.rs to use mem_storew_le Co-authored-by: marti chore: update mem_load/store_double_word to LE and resolve TODO - Update utils.masm: mem_storew_be -> mem_storew_le, mem_loadw_be -> mem_loadw_le - Replace inline mem_loadw_le in crypto_utils.masm with exec.utils::mem_load_double_word - Restore mem_load/store_double_word usage in mmr_frontier32_keccak.masm and canonical_zeros.masm - Update stale comment in claim_note.rs Co-authored-by: marti * chore: simplify inline comments * lints * chore: re-enable bridge-in tests * chore: remove dead code * remove keccak_digest_to_word_strings; use SmtRoot/ExitRoot * chore: avoid loc mem, keep is_odd on stack * lints * remove unused import --------- Co-authored-by: Cursor Agent --- Makefile | 1 + .../asm/bridge/crypto_utils.masm | 66 +++-- .../asm/bridge/mmr_frontier32_keccak.masm | 24 +- crates/miden-agglayer/asm/bridge/utils.masm | 8 +- .../test-vectors/claim_asset_vectors.json | 82 ++++++ .../test/ClaimAssetTestVectors.t.sol | 136 ++++++++++ crates/miden-agglayer/src/claim_note.rs | 28 +- .../miden-testing/tests/agglayer/bridge_in.rs | 180 +++---------- .../tests/agglayer/crypto_utils.rs | 103 ++------ .../tests/agglayer/mmr_frontier.rs | 48 ++-- crates/miden-testing/tests/agglayer/mod.rs | 6 +- .../tests/agglayer/test_utils.rs | 249 +++++++++++------- 12 files changed, 530 insertions(+), 401 deletions(-) create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json create mode 100644 crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol diff --git a/Makefile b/Makefile index 733dfa1ac6..e80f600592 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,7 @@ generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Fo cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateCanonicalZeros cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVerificationProofData cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateLeafValueVectors + cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateClaimAssetVectors # --- benchmarking -------------------------------------------------------------------------------- diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index fec507020e..a0d92bc582 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -161,71 +161,67 @@ end #! represented as 32 Keccak256Digest values (64 words). #! - leaf_idx is the index of the provided leaf in the SMT. #! - [ROOT_LO, ROOT_HI] is the calculated root. -@locals(9) # current hash + is_odd flag +@locals(8) # current hash proc calculate_root( leaf_value: DoubleWord, merkle_path_ptr: MemoryAddress, leaf_idx: u32 ) -> DoubleWord # Local memory stores the current hash. It is initialized to the leaf value - loc_storew_be.CUR_HASH_LO_LOCAL dropw loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [merkle_path_ptr, leaf_idx] - # prepare the stack for the hash computation cycle - padw padw padw - # => [PAD, PAD, PAD, merkle_path_ptr, leaf_idx] - # Merkle path is guaranteed to contain 32 nodes repeat.32 - # load the Merkle path node onto the stack - mem_stream - # => [PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # load the Merkle path node word-by-word in LE-felt order + padw dup.4 mem_loadw_le + # => [PATH_NODE_LO, merkle_path_ptr, leaf_idx] + padw dup.8 add.4 mem_loadw_le + swapw + # => [PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr, leaf_idx] - # determine whether the last `leaf_idx` bit is 1 (is `leaf_idx` odd) - dup.13 u32and.1 - # => [is_odd, PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # advance merkle_path_ptr by 8 (two words = 8 element addresses) + movup.8 add.8 movdn.8 + # => [PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr+8, leaf_idx] - # store the is_odd flag to the local memory, so we could use it while all 16 top elements - # are occupied by the nodes - loc_store.8 - # => [PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # determine whether the last `leaf_idx` bit is 1 (is `leaf_idx` odd) + dup.9 u32and.1 + # => [is_odd, PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr+8, leaf_idx] # load the hash respective to the current height from the local memory - padw loc_loadw_be.CUR_HASH_HI_LOCAL padw loc_loadw_be.CUR_HASH_LO_LOCAL - # => [CURR_HASH_LO, CURR_HASH_HI, PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL + # => [CURR_HASH_LO, CURR_HASH_HI, is_odd, PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr, leaf_idx] - # load the is_odd flag back to the stack - loc_load.8 - # => [is_odd, CURR_HASH_LO, CURR_HASH_HI, PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # move the `is_odd` flag to the top of the stack + movup.8 + # => [is_odd, CURR_HASH_LO, CURR_HASH_HI, PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr, leaf_idx] # if is_odd flag equals 1 (`leaf_idx` is odd), change the order of the nodes on the stack - if.true - # rearrange the hashes: current position of the hash is odd, so it should be on the + if.true + # rearrange the hashes: current position of the hash is odd, so it should be on the # right swapdw - # => [PATH_NODE_LO, PATH_NODE_HI, CURR_HASH_LO, CURR_HASH_HI, PAD, merkle_path_ptr, leaf_idx] + # => [PATH_NODE_LO, PATH_NODE_HI, CURR_HASH_LO, CURR_HASH_HI, merkle_path_ptr, leaf_idx] end # compute the next height hash exec.keccak256::merge - # => [CURR_HASH_LO', CURR_HASH_HI', PAD, merkle_path_ptr, leaf_idx] + # => [CURR_HASH_LO', CURR_HASH_HI', merkle_path_ptr, leaf_idx] - # store the resulting hash to the local memory - loc_storew_be.CUR_HASH_LO_LOCAL swapw loc_storew_be.CUR_HASH_HI_LOCAL - # => [CURR_HASH_HI', CURR_HASH_LO', PAD, merkle_path_ptr, leaf_idx] + # store the resulting hash to the local memory and drop the hash words + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw + # => [merkle_path_ptr, leaf_idx] # update the `leaf_idx` (shift it right by 1 bit) - movup.13 u32shr.1 movdn.13 - # => [CURR_HASH_HI', CURR_HASH_LO', PAD, merkle_path_ptr, leaf_idx>>1] + swap u32shr.1 swap + # => [merkle_path_ptr, leaf_idx>>1] end # after all 32 hashes have been computed, the current hash stored in local memory represents # the root of the SMT, which should be returned - # - # remove 6 elements from the stack so that exactly 8 are remaining and rewrite them with the - # root value from the local memory - dropw drop drop - loc_loadw_be.CUR_HASH_HI_LOCAL swapw loc_loadw_be.CUR_HASH_LO_LOCAL + drop drop + padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ROOT_LO, ROOT_HI] end diff --git a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm index b789847639..f8be97dfa6 100644 --- a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm +++ b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm @@ -136,8 +136,8 @@ const CANONICAL_ZEROES_LOCAL = 8 @locals(264) # new_leaf/curr_hash + canonical_zeros pub proc append_and_update_frontier # set CUR_HASH = NEW_LEAF and store to local memory - loc_storew_be.CUR_HASH_LO_LOCAL dropw - loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [mmr_frontier_ptr] # get the current leaves number @@ -203,8 +203,8 @@ pub proc append_and_update_frontier # load the current hash from the local memory back to the stack # # in the first iteration the current hash will be equal to the new node - padw loc_loadw_be.CUR_HASH_HI_LOCAL - padw loc_loadw_be.CUR_HASH_LO_LOCAL + padw loc_loadw_le.CUR_HASH_HI_LOCAL + padw loc_loadw_le.CUR_HASH_LO_LOCAL swapdw # => [ # FRONTIER[curr_tree_height]_LO, FRONTIER[curr_tree_height]_HI, CUR_HASH_LO, @@ -217,16 +217,16 @@ pub proc append_and_update_frontier # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mmr_frontier_ptr] # store the current hash of the next height back to the local memory - loc_storew_be.CUR_HASH_LO_LOCAL dropw - loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [curr_tree_height, num_leaves, mmr_frontier_ptr] else # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] # # this height wasn't "occupied" yet: store the current hash as the subtree root # (frontier node) at height `curr_tree_height` - padw loc_loadw_be.CUR_HASH_HI_LOCAL - padw loc_loadw_be.CUR_HASH_LO_LOCAL + padw loc_loadw_le.CUR_HASH_HI_LOCAL + padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ # CUR_HASH_LO, CUR_HASH_HI, frontier[curr_tree_height]_ptr, curr_tree_height, # num_leaves, mmr_frontier_ptr @@ -256,8 +256,8 @@ pub proc append_and_update_frontier # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mmr_frontier_ptr] # store the current hash of the next height back to the local memory - loc_storew_be.CUR_HASH_LO_LOCAL dropw - loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [curr_tree_height, num_leaves, mmr_frontier_ptr] end # => [curr_tree_height, num_leaves, mmr_frontier_ptr] @@ -292,8 +292,8 @@ pub proc append_and_update_frontier # compute. # load the final hash (which is also the root of the tree) - padw loc_loadw_be.CUR_HASH_HI_LOCAL - padw loc_loadw_be.CUR_HASH_LO_LOCAL + padw loc_loadw_le.CUR_HASH_HI_LOCAL + padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] end diff --git a/crates/miden-agglayer/asm/bridge/utils.masm b/crates/miden-agglayer/asm/bridge/utils.masm index 8a9d460d8c..85ee284019 100644 --- a/crates/miden-agglayer/asm/bridge/utils.masm +++ b/crates/miden-agglayer/asm/bridge/utils.masm @@ -46,10 +46,10 @@ pub proc mem_store_double_word( double_word_to_store: DoubleWord, mem_ptr: MemoryAddress ) -> (DoubleWord, MemoryAddress) - dup.8 mem_storew_be swapw + dup.8 mem_storew_le swapw # => [WORD_2, WORD_1, ptr] - dup.8 add.4 mem_storew_be swapw + dup.8 add.4 mem_storew_le swapw # => [WORD_1, WORD_2, ptr] end @@ -58,9 +58,9 @@ end #! Inputs: [ptr] #! Outputs: [WORD_1, WORD_2] pub proc mem_load_double_word(mem_ptr: MemoryAddress) -> DoubleWord - padw dup.4 add.4 mem_loadw_be + padw dup.4 add.4 mem_loadw_le # => [WORD_2, ptr] - padw movup.8 mem_loadw_be + padw movup.8 mem_loadw_le # => [WORD_1, WORD_2] end diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json new file mode 100644 index 0000000000..df09f5a6a5 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json @@ -0,0 +1,82 @@ +{ + "amount": "0x00000000000000000000000000000000000000000000000000005af3107a4000", + "destination_address": "0x00000000b0E79c68cafC54802726C6F102Cca300", + "destination_network": 20, + "global_exit_root": "0xe1cbfbde30bd598ee9aa2ac913b60d53e3297e51ed138bf86c500dd7d2391e7d", + "global_index": "0x0000000000000000000000000000000000000000000000010000000000039e88", + "leaf_type": 0, + "leaf_value": "0xc58420b9b4ba439bb5f6f68096270f4df656553ec67150d4d087416b9ef6ea9d", + "mainnet_exit_root": "0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c", + "metadata_hash": "0x945d61756eddd06a335ceff22d61480fc2086e85e74a55db5485f814626247d5", + "origin_network": 0, + "origin_token_address": "0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF", + "rollup_exit_root": "0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858", + "smt_proof_local_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0xe37d456460231cf80063f57ee83a02f70d810c568b3bfb71156d52445f7a885a", + "0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344", + "0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d", + "0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968", + "0x3236bf576fca1adf85917ec7888c4b89cce988564b6028f7d66807763aaa7b04", + "0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af", + "0x054ba828046324ff4794fce22adefb23b3ce749cd4df75ade2dc9f41dd327c31", + "0x4e9220076c344bf223c7e7cb2d47c9f0096c48def6a9056e41568de4f01d2716", + "0xca6369acd49a7515892f5936227037cc978a75853409b20f1145f1d44ceb7622", + "0x5a925caf7bfdf31344037ba5b42657130d049f7cb9e87877317e79fce2543a0c", + "0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb", + "0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc", + "0x4111a1a05cc06ad682bb0f213170d7d57049920d20fc4e0f7556a21b283a7e2a", + "0x77a0f8b0e0b4e5a57f5e381b3892bb41a0bcdbfdf3c7d591fae02081159b594d", + "0x361122b4b1d18ab577f2aeb6632c690713456a66a5670649ceb2c0a31e43ab46", + "0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0", + "0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0", + "0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2", + "0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9", + "0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377", + "0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652", + "0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef", + "0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d", + "0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0", + "0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e", + "0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e", + "0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322", + "0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735", + "0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9" + ], + "smt_proof_rollup_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol new file mode 100644 index 0000000000..5ba23cbfdf --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; + +/** + * @title ClaimAssetTestVectors + * @notice Test contract that generates comprehensive test vectors for verifying + * compatibility between Solidity's claimAsset and Miden's implementation. + * + * Generates vectors for both LeafData and ProofData from a real transaction. + * + * Run with: forge test -vv --match-contract ClaimAssetTestVectors + * + * The output can be compared against the Rust ClaimNoteStorage implementation. + */ +contract ClaimAssetTestVectors is Test, DepositContractV2 { + /** + * @notice Generates claim asset test vectors from real Katana transaction and saves to JSON. + * Uses real transaction data from Katana explorer: + * https://katanascan.com/tx/0x685f6437c4a54f5d6c59ea33de74fe51bc2401fea65dc3d72a976def859309bf + * + * Output file: test-vectors/claim_asset_vectors.json + */ + function test_generateClaimAssetVectors() public { + string memory obj = "root"; + + // ====== PROOF DATA ====== + // Scoped block keeps stack usage under Solidity limits. + { + // SMT proof for local exit root (32 nodes) + bytes32[32] memory smtProofLocalExitRoot = [ + bytes32(0x0000000000000000000000000000000000000000000000000000000000000000), + bytes32(0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5), + bytes32(0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30), + bytes32(0xe37d456460231cf80063f57ee83a02f70d810c568b3bfb71156d52445f7a885a), + bytes32(0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344), + bytes32(0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d), + bytes32(0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968), + bytes32(0x3236bf576fca1adf85917ec7888c4b89cce988564b6028f7d66807763aaa7b04), + bytes32(0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af), + bytes32(0x054ba828046324ff4794fce22adefb23b3ce749cd4df75ade2dc9f41dd327c31), + bytes32(0x4e9220076c344bf223c7e7cb2d47c9f0096c48def6a9056e41568de4f01d2716), + bytes32(0xca6369acd49a7515892f5936227037cc978a75853409b20f1145f1d44ceb7622), + bytes32(0x5a925caf7bfdf31344037ba5b42657130d049f7cb9e87877317e79fce2543a0c), + bytes32(0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb), + bytes32(0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc), + bytes32(0x4111a1a05cc06ad682bb0f213170d7d57049920d20fc4e0f7556a21b283a7e2a), + bytes32(0x77a0f8b0e0b4e5a57f5e381b3892bb41a0bcdbfdf3c7d591fae02081159b594d), + bytes32(0x361122b4b1d18ab577f2aeb6632c690713456a66a5670649ceb2c0a31e43ab46), + bytes32(0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0), + bytes32(0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0), + bytes32(0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2), + bytes32(0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9), + bytes32(0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377), + bytes32(0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652), + bytes32(0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef), + bytes32(0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d), + bytes32(0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0), + bytes32(0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e), + bytes32(0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e), + bytes32(0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322), + bytes32(0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735), + bytes32(0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9) + ]; + + // forge-std JSON serialization supports `bytes32[]` but not `bytes32[32]`. + bytes32[] memory smtProofLocalExitRootDyn = new bytes32[](32); + for (uint i = 0; i < 32; i++) { + smtProofLocalExitRootDyn[i] = smtProofLocalExitRoot[i]; + } + + // SMT proof for rollup exit root (32 nodes - all zeros for this rollup claim). + bytes32[] memory smtProofRollupExitRootDyn = new bytes32[](32); + + // Global index (uint256) - encodes rollup_id and deposit_count. + uint256 globalIndex = 18446744073709788808; + + // Exit roots + bytes32 mainnetExitRoot = 0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c; + bytes32 rollupExitRoot = 0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858; + + // Compute global exit root: keccak256(mainnetExitRoot || rollupExitRoot) + bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoot, rollupExitRoot); + + vm.serializeBytes32(obj, "smt_proof_local_exit_root", smtProofLocalExitRootDyn); + vm.serializeBytes32(obj, "smt_proof_rollup_exit_root", smtProofRollupExitRootDyn); + vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); + vm.serializeBytes32(obj, "mainnet_exit_root", mainnetExitRoot); + vm.serializeBytes32(obj, "rollup_exit_root", rollupExitRoot); + vm.serializeBytes32(obj, "global_exit_root", globalExitRoot); + } + + // ====== LEAF DATA ====== + // Scoped block keeps stack usage under Solidity limits. + { + uint8 leafType = 0; // 0 for ERC20/ETH transfer + uint32 originNetwork = 0; + address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; + uint32 destinationNetwork = 20; + address destinationAddress = 0x00000000b0E79c68cafC54802726C6F102Cca300; + uint256 amount = 100000000000000; // 1e14 (0.0001 vbETH) + + // Original metadata from the transaction (ABI encoded: name, symbol, decimals) + // name = "Vault Bridge ETH", symbol = "vbETH", decimals = 18 + bytes memory metadata = hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000105661756c7420427269646765204554480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000057662455448000000000000000000000000000000000000000000000000000000"; + bytes32 metadataHash = keccak256(metadata); + + // Compute the leaf value using the official DepositContractV2 implementation + bytes32 leafValue = getLeafValue( + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + vm.serializeUint(obj, "leaf_type", leafType); + vm.serializeUint(obj, "origin_network", originNetwork); + vm.serializeAddress(obj, "origin_token_address", originTokenAddress); + vm.serializeUint(obj, "destination_network", destinationNetwork); + vm.serializeAddress(obj, "destination_address", destinationAddress); + vm.serializeBytes32(obj, "amount", bytes32(amount)); + vm.serializeBytes32(obj, "metadata_hash", metadataHash); + string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); + + // Save to file + string memory outputPath = "test-vectors/claim_asset_vectors.json"; + vm.writeJson(json, outputPath); + } + } +} diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 9eb906b117..bc1108f74d 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -44,6 +44,18 @@ impl Keccak256Output { pub fn to_elements(&self) -> Vec { bytes_to_packed_u32_felts(&self.0) } + + /// Converts the Keccak256 output to two [`Word`]s: `[lo, hi]`. + /// + /// - `lo` contains the first 4 u32-packed felts (bytes 0..16). + /// - `hi` contains the last 4 u32-packed felts (bytes 16..32). + #[cfg(any(test, feature = "testing"))] + pub fn to_words(&self) -> [Word; 2] { + let elements = self.to_elements(); + let lo: [Felt; 4] = elements[0..4].try_into().expect("to_elements returns 8 felts"); + let hi: [Felt; 4] = elements[4..8].try_into().expect("to_elements returns 8 felts"); + [Word::new(lo), Word::new(hi)] + } } impl From<[u8; 32]> for Keccak256Output { @@ -83,25 +95,19 @@ impl SequentialCommit for ProofData { // Convert SMT proof elements to felts (each node is 8 felts) for node in self.smt_proof_local_exit_root.iter() { - let node_felts = node.to_elements(); - elements.extend(node_felts); + elements.extend(node.to_elements()); } for node in self.smt_proof_rollup_exit_root.iter() { - let node_felts = node.to_elements(); - elements.extend(node_felts); + elements.extend(node.to_elements()); } // Global index (uint256 as 32 bytes) elements.extend(self.global_index.to_elements()); - // Mainnet exit root (bytes32 as 8 u32 felts) - let mainnet_exit_root_felts = self.mainnet_exit_root.to_elements(); - elements.extend(mainnet_exit_root_felts); - - // Rollup exit root (bytes32 as 8 u32 felts) - let rollup_exit_root_felts = self.rollup_exit_root.to_elements(); - elements.extend(rollup_exit_root_felts); + // Mainnet and rollup exit roots + elements.extend(self.mainnet_exit_root.to_elements()); + elements.extend(self.rollup_exit_root.to_elements()); elements } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 402c9fbb00..4147119147 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -1,43 +1,33 @@ extern crate alloc; -use core::slice; - -use miden_agglayer::claim_note::{ExitRoot, SmtNode}; use miden_agglayer::{ ClaimNoteStorage, - EthAddressFormat, - EthAmount, - GlobalIndex, - LeafData, - MetadataHash, OutputNoteData, - ProofData, create_claim_note, create_existing_agglayer_faucet, create_existing_bridge_account, }; use miden_protocol::Felt; use miden_protocol::account::Account; -use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::asset::FungibleAsset; use miden_protocol::crypto::rand::FeltRng; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteStorage, - NoteTag, - NoteType, -}; +use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; use miden_standards::account::wallets::BasicWallet; -use miden_standards::note::StandardNote; use miden_testing::{AccountState, Auth, MockChain}; use rand::Rng; -use super::test_utils::claim_note_test_inputs; - -/// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created. +use super::test_utils::real_claim_data; + +/// Tests the bridge-in flow using real claim data: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID +/// note created. +/// +/// This test uses real ProofData and LeafData deserialized from claim_asset_vectors.json. +/// The claim note is processed against the agglayer faucet, which validates the Merkle proof +/// and creates a P2ID note for the destination address. +/// +/// Note: Modifying anything in the test vectors would invalidate the Merkle proof, +/// as the proof was computed for the original leaf_data including the original destination. #[tokio::test] async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -52,7 +42,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- let token_symbol = "AGG"; let decimals = 8u8; - let max_supply = Felt::new(1000000); + let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let agglayer_faucet_seed = builder.rng_mut().draw_word(); let agglayer_faucet = create_existing_agglayer_faucet( @@ -64,93 +54,43 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { ); builder.add_account(agglayer_faucet.clone())?; - // CREATE USER ACCOUNT TO RECEIVE P2ID NOTE + // GET REAL CLAIM DATA FROM JSON // -------------------------------------------------------------------------------------------- - let user_account_builder = + let (proof_data, leaf_data) = real_claim_data(); + + // Get the destination account ID from the leaf data + // This requires the destination_address to be in the embedded Miden AccountId format + // (first 4 bytes must be zero). + let destination_account_id = leaf_data + .destination_address + .to_account_id() + .expect("destination address is not an embedded Miden AccountId"); + + // CREATE SENDER ACCOUNT (for creating the claim note) + // -------------------------------------------------------------------------------------------- + let sender_account_builder = Account::builder(builder.rng_mut().random()).with_component(BasicWallet); - let user_account = builder.add_account_from_builder( + let sender_account = builder.add_account_from_builder( Auth::IncrNonce, - user_account_builder, + sender_account_builder, AccountState::Exists, )?; - // CREATE CLAIM NOTE WITH P2ID OUTPUT NOTE DETAILS + // CREATE CLAIM NOTE WITH REAL PROOF DATA AND LEAF DATA // -------------------------------------------------------------------------------------------- - // Define amount values for the test - let claim_amount = 100u32; - - // Create CLAIM note using the new test inputs function - let ( - smt_proof_local_exit_root, - smt_proof_rollup_exit_root, - global_index, - mainnet_exit_root, - rollup_exit_root, - origin_network, - origin_token_address, - destination_network, - metadata_hash, - ) = claim_note_test_inputs(); - - // Convert AccountId to destination address bytes in the test - let destination_address = EthAddressFormat::from_account_id(user_account.id()).into_bytes(); - // Generate a serial number for the P2ID note let serial_num = builder.rng_mut().draw_word(); - // Convert amount to EthAmount for the LeafData - let mut claim_amount_bytes = [0u8; 32]; - claim_amount_bytes[28..32].copy_from_slice(&claim_amount.to_be_bytes()); - let amount_eth = EthAmount::new(claim_amount_bytes); - - // Convert Vec<[u8; 32]> to [SmtNode; 32] for SMT proofs - let local_proof_array: [SmtNode; 32] = smt_proof_local_exit_root[0..32] - .iter() - .map(|&bytes| SmtNode::from(bytes)) - .collect::>() - .try_into() - .expect("should have exactly 32 elements"); - - let rollup_proof_array: [SmtNode; 32] = smt_proof_rollup_exit_root[0..32] - .iter() - .map(|&bytes| SmtNode::from(bytes)) - .collect::>() - .try_into() - .expect("should have exactly 32 elements"); - - let proof_data = ProofData { - smt_proof_local_exit_root: local_proof_array, - smt_proof_rollup_exit_root: rollup_proof_array, - global_index: GlobalIndex::new(global_index), - mainnet_exit_root: ExitRoot::from(mainnet_exit_root), - rollup_exit_root: ExitRoot::from(rollup_exit_root), - }; - - let leaf_data = LeafData { - origin_network, - origin_token_address: EthAddressFormat::new(origin_token_address), - destination_network, - destination_address: EthAddressFormat::new(destination_address), - amount: amount_eth, - metadata_hash: MetadataHash::new(metadata_hash), - }; - let output_note_data = OutputNoteData { output_p2id_serial_num: serial_num, target_faucet_account_id: agglayer_faucet.id(), - output_note_tag: NoteTag::with_account_target(user_account.id()), + output_note_tag: NoteTag::with_account_target(destination_account_id), }; let claim_inputs = ClaimNoteStorage { proof_data, leaf_data, output_note_data }; - let claim_note = create_claim_note(claim_inputs, user_account.id(), builder.rng_mut())?; - - // Create P2ID note for the user account (similar to network faucet test) - let p2id_script = StandardNote::P2ID.script(); - let p2id_inputs = vec![user_account.id().suffix(), user_account.id().prefix().as_felt()]; - let note_storage = NoteStorage::new(p2id_inputs)?; - let p2id_recipient = NoteRecipient::new(serial_num, p2id_script.clone(), note_storage); + let claim_note = create_claim_note(claim_inputs, sender_account.id(), builder.rng_mut())?; // Add the claim note to the builder before building the mock chain builder.add_output_note(OutputNote::Full(claim_note.clone())); @@ -160,17 +100,6 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let mut mock_chain = builder.clone().build()?; mock_chain.prove_next_block()?; - // CREATE EXPECTED P2ID NOTE FOR VERIFICATION - // -------------------------------------------------------------------------------------------- - let amount_felt = Felt::from(claim_amount); - let mint_asset: Asset = FungibleAsset::new(agglayer_faucet.id(), amount_felt.into())?.into(); - let output_note_tag = NoteTag::with_account_target(user_account.id()); - let expected_p2id_note = Note::new( - NoteAssets::new(vec![mint_asset])?, - NoteMetadata::new(agglayer_faucet.id(), NoteType::Public).with_tag(output_note_tag), - p2id_recipient, - ); - // EXECUTE CLAIM NOTE AGAINST AGGLAYER FAUCET (with FPI to Bridge) // -------------------------------------------------------------------------------------------- let foreign_account_inputs = mock_chain.get_foreign_account_inputs(bridge_account.id())?; @@ -189,44 +118,19 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { assert_eq!(executed_transaction.output_notes().num_notes(), 1); let output_note = executed_transaction.output_notes().get_note(0); - // Verify the output note contains the minted fungible asset - let expected_asset = FungibleAsset::new(agglayer_faucet.id(), claim_amount.into())?; - // Verify note metadata properties assert_eq!(output_note.metadata().sender(), agglayer_faucet.id()); assert_eq!(output_note.metadata().note_type(), NoteType::Public); - assert_eq!(output_note.id(), expected_p2id_note.id()); - - // Extract the full note from the OutputNote enum for detailed verification - let full_note = match output_note { - OutputNote::Full(note) => note, - _ => panic!("Expected OutputNote::Full variant for public note"), - }; - - // Verify note structure and asset content - let expected_asset_obj = Asset::from(expected_asset); - assert_eq!(full_note, &expected_p2id_note); - - assert!(full_note.assets().iter().any(|asset| asset == &expected_asset_obj)); - - // Apply the transaction to the mock chain - mock_chain.add_pending_executed_transaction(&executed_transaction)?; - mock_chain.prove_next_block()?; - - // CONSUME THE OUTPUT NOTE WITH TARGET ACCOUNT - // -------------------------------------------------------------------------------------------- - // Consume the output note with target account - let mut user_account_mut = user_account.clone(); - let consume_tx_context = mock_chain - .build_tx_context(user_account_mut.clone(), &[], slice::from_ref(&expected_p2id_note))? - .build()?; - let consume_executed_transaction = consume_tx_context.execute().await?; - - user_account_mut.apply_delta(consume_executed_transaction.account_delta())?; - // Verify the account's vault now contains the expected fungible asset - let balance = user_account_mut.vault().get_balance(agglayer_faucet.id())?; - assert_eq!(balance, expected_asset.amount()); + // Note: We intentionally do NOT verify the exact note ID or asset amount here because + // the scale_u256_to_native_amount function is currently a TODO stub that doesn't perform + // proper u256-to-native scaling. The test verifies that the bridge-in flow correctly + // validates the Merkle proof using real cryptographic proof data and creates an output note. + // + // TODO: Once scale_u256_to_native_amount is properly implemented, add: + // - Verification that the minted amount matches the expected scaled value + // - Full note ID comparison with the expected P2ID note + // - Asset content verification Ok(()) } diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index b7992887a3..6f8e26a017 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -7,78 +7,23 @@ use alloc::vec::Vec; use anyhow::Context; use miden_agglayer::claim_note::Keccak256Output; use miden_agglayer::utils::felts_to_bytes; -use miden_agglayer::{EthAddressFormat, EthAmount, LeafData, MetadataHash, agglayer_library}; +use miden_agglayer::{ExitRoot, SmtNode, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_crypto::SequentialCommit; -use miden_crypto::hash::keccak::Keccak256Digest; use miden_processor::AdviceInputs; -use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; use miden_tx::utils::hex_to_bytes; -use serde::Deserialize; - -use super::test_utils::{execute_program_with_default_host, keccak_digest_to_word_strings}; - -/// Merkle proof verification vectors JSON embedded at compile time from the Foundry-generated file. -const MERKLE_PROOF_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); - -/// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol -/// Uses parallel arrays for leaves and roots. For each element from leaves/roots there are 32 -/// elements from merkle_paths, which represent the merkle path for that leaf + root. -#[derive(Debug, Deserialize)] -struct MerkleProofVerificationFile { - leaves: Vec, - roots: Vec, - merkle_paths: Vec, -} - -/// Lazily parsed Merkle proof vectors from the JSON file. -static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = LazyLock::new(|| { - serde_json::from_str(MERKLE_PROOF_VECTORS_JSON) - .expect("failed to parse Merkle proof vectors JSON") -}); - -/// Leaf data test vectors JSON embedded at compile time from the Foundry-generated file. -const LEAF_VALUE_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); - -// TEST VECTOR STRUCTURES -// ================================================================================================ -/// Deserialized leaf value test vector from Solidity-generated JSON. -#[derive(Debug, Deserialize)] -struct LeafValueVector { - origin_network: u32, - origin_token_address: String, - destination_network: u32, - destination_address: String, - amount: String, - metadata_hash: String, - #[allow(dead_code)] - leaf_value: String, -} - -impl LeafValueVector { - /// Converts this test vector into a `LeafData` instance. - fn to_leaf_data(&self) -> LeafData { - LeafData { - origin_network: self.origin_network, - origin_token_address: EthAddressFormat::from_hex(&self.origin_token_address) - .expect("valid origin token address hex"), - destination_network: self.destination_network, - destination_address: EthAddressFormat::from_hex(&self.destination_address) - .expect("valid destination address hex"), - amount: EthAmount::new(hex_to_bytes(&self.amount).expect("valid amount hex")), - metadata_hash: MetadataHash::new( - hex_to_bytes(&self.metadata_hash).expect("valid metadata hash hex"), - ), - } - } -} +use super::test_utils::{ + LEAF_VALUE_VECTORS_JSON, + LeafValueVector, + MerkleProofVerificationFile, + SOLIDITY_MERKLE_PROOF_VECTORS, + execute_program_with_default_host, +}; // HELPER FUNCTIONS // ================================================================================================ @@ -99,16 +44,15 @@ fn merkle_proof_verification_code( // generate the code which stores the merkle path to the memory let mut store_path_source = String::new(); for height in 0..32 { - let path_node = - Keccak256Digest::try_from(merkle_paths.merkle_paths[index * 32 + height].as_str()) - .unwrap(); - let (node_hi, node_lo) = keccak_digest_to_word_strings(path_node); + let path_node = merkle_paths.merkle_paths[index * 32 + height].as_str(); + let smt_node = SmtNode::from(hex_to_bytes(path_node).unwrap()); + let [node_lo, node_hi] = smt_node.to_words(); // each iteration (each index in leaf/root vector) we rewrite the merkle path nodes, so the // memory pointers for the merkle path and the expected root never change store_path_source.push_str(&format!( " -\tpush.[{node_hi}] mem_storew_be.{} dropw -\tpush.[{node_lo}] mem_storew_be.{} dropw + \tpush.{node_lo} mem_storew_be.{} dropw + \tpush.{node_hi} mem_storew_be.{} dropw ", height * 8, height * 8 + 4 @@ -116,16 +60,17 @@ fn merkle_proof_verification_code( } // prepare the root for the provided index - let root = Keccak256Digest::try_from(merkle_paths.roots[index].as_str()).unwrap(); - let (root_hi, root_lo) = keccak_digest_to_word_strings(root); + let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); + let [root_lo, root_hi] = root.to_words(); // prepare the leaf for the provided index - let leaf = Keccak256Digest::try_from(merkle_paths.leaves[index].as_str()).unwrap(); - let (leaf_hi, leaf_lo) = keccak_digest_to_word_strings(leaf); + let leaf = Keccak256Output::from(hex_to_bytes(&merkle_paths.leaves[index]).unwrap()); + let [leaf_lo, leaf_hi] = leaf.to_words(); format!( r#" use miden::agglayer::crypto_utils + use miden::core::word begin # store the merkle path to the memory (double word slots from 0 to 248) @@ -133,15 +78,20 @@ fn merkle_proof_verification_code( # => [] # store the root to the memory (double word slot 256) - push.[{root_lo}] mem_storew_be.256 dropw - push.[{root_hi}] mem_storew_be.260 dropw + push.{root_lo} mem_storew_be.256 dropw + push.{root_hi} mem_storew_be.260 dropw # => [] # prepare the stack for the `verify_merkle_proof` procedure push.256 # expected root memory pointer push.{index} # provided leaf index push.0 # Merkle path memory pointer - push.[{leaf_hi}] push.[{leaf_lo}] # provided leaf value + # in practice this is never "pushed" to the stack, but rather an output of `get_leaf_value` + # which returns the leaf value in LE-felt order + push.{leaf_hi} + exec.word::reverse + push.{leaf_lo} + exec.word::reverse # => [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx, expected_root_ptr] exec.crypto_utils::verify_merkle_proof @@ -322,7 +272,6 @@ async fn get_leaf_value() -> anyhow::Result<()> { assert_eq!(computed_leaf_value, expected_leaf_value); Ok(()) } - #[tokio::test] async fn test_solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs index 367d221cc5..a7ddb4dfa7 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -1,15 +1,14 @@ use alloc::format; use alloc::string::ToString; -use miden_agglayer::agglayer_library; +use miden_agglayer::claim_note::SmtNode; +use miden_agglayer::{ExitRoot, agglayer_library}; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; use miden_protocol::utils::sync::LazyLock; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; use serde::Deserialize; -use super::test_utils::keccak_digest_to_word_strings; - // KECCAK MMR FRONTIER // ================================================================================================ @@ -76,7 +75,8 @@ impl KeccakMmrFrontier32 { async fn test_append_and_update_frontier() -> anyhow::Result<()> { let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); - let mut source = "use miden::agglayer::mmr_frontier32_keccak begin".to_string(); + let mut source = + "use miden::agglayer::mmr_frontier32_keccak use miden::core::word begin".to_string(); for round in 0..32 { // construct the leaf from the hex representation of the round number @@ -84,7 +84,11 @@ async fn test_append_and_update_frontier() -> anyhow::Result<()> { let root = mmr_frontier.append_and_update_frontier(leaf); let num_leaves = mmr_frontier.num_leaves; - source.push_str(&leaf_assertion_code(leaf, root, num_leaves)); + source.push_str(&leaf_assertion_code( + SmtNode::new(leaf.into()), + ExitRoot::new(root.into()), + num_leaves, + )); } source.push_str("end"); @@ -108,11 +112,16 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { let zero_31 = *CANONICAL_ZEROS_32.get(31).expect("zeros should have 32 values total"); let empty_mmr_root = Keccak256::merge(&[zero_31, zero_31]); - let mut source = "use miden::agglayer::mmr_frontier32_keccak begin".to_string(); + let mut source = + "use miden::agglayer::mmr_frontier32_keccak use miden::core::word begin".to_string(); for round in 1..=32 { // check that pushing the zero leaves into the MMR doesn't change its root - source.push_str(&leaf_assertion_code(zero_leaf, empty_mmr_root, round)); + source.push_str(&leaf_assertion_code( + SmtNode::new(zero_leaf.into()), + ExitRoot::new(empty_mmr_root.into()), + round, + )); } source.push_str("end"); @@ -222,27 +231,28 @@ fn test_solidity_mmr_frontier_compatibility() { // HELPER FUNCTIONS // ================================================================================================ -fn leaf_assertion_code( - leaf: Keccak256Digest, - expected_root: Keccak256Digest, - num_leaves: u32, -) -> String { - let (leaf_hi, leaf_lo) = keccak_digest_to_word_strings(leaf); - let (root_hi, root_lo) = keccak_digest_to_word_strings(expected_root); +fn leaf_assertion_code(leaf: SmtNode, expected_root: ExitRoot, num_leaves: u32) -> String { + let [leaf_lo, leaf_hi] = leaf.to_words(); + let [root_lo, root_hi] = expected_root.to_words(); format!( r#" - # load the provided leaf onto the stack - push.[{leaf_hi}] - push.[{leaf_lo}] + # load the provided leaf onto the stack and reverse (the leaf value would have been + # reversed by the keccak hash call in a real get_leaf_value) + push.{leaf_hi} + exec.word::reverse + push.{leaf_lo} + exec.word::reverse # add this leaf to the MMR frontier exec.mmr_frontier32_keccak::append_and_update_frontier # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] # assert the root correctness after the first leaf was added - push.[{root_lo}] - push.[{root_hi}] + push.{root_lo} + exec.word::reverse + push.{root_hi} + exec.word::reverse movdnw.3 # => [EXPECTED_ROOT_LO, NEW_ROOT_LO, NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 3cb09e8467..f96326ffa3 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,9 +1,5 @@ pub mod asset_conversion; -// TODO: Uncomment this when https://github.com/0xMiden/miden-base/issues/2397 is ready. -// The mainnet exit root is hardcoded to pass the current test (i.e. we set the expected mainnet -// root to whatever the current implementation computes), and changing any impl. details will break -// the test, forcing us to artificially change the expected root every time. -// mod bridge_in; +mod bridge_in; mod bridge_out; mod crypto_utils; mod global_index; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index d33a7225ef..45813ccfcf 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -1,24 +1,162 @@ extern crate alloc; +use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use miden_agglayer::agglayer_library; +use miden_agglayer::claim_note::{Keccak256Output, ProofData, SmtNode}; +use miden_agglayer::{ + EthAddressFormat, + EthAmount, + GlobalIndex, + LeafData, + MetadataHash, + agglayer_library, +}; use miden_core_lib::CoreLibrary; -use miden_crypto::hash::keccak::Keccak256Digest; use miden_processor::fast::{ExecutionOutput, FastProcessor}; -use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Felt, Program, StackInputs}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; use miden_protocol::transaction::TransactionKernel; +use miden_protocol::utils::sync::LazyLock; +use miden_tx::utils::hex_to_bytes; +use serde::Deserialize; -/// Transforms the `[Keccak256Digest]` into two word strings: (`a, b, c, d`, `e, f, g, h`) -pub fn keccak_digest_to_word_strings(digest: Keccak256Digest) -> (String, String) { - let double_word = (*digest) - .chunks(4) - .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap())).to_string()) - .rev() - .collect::>(); +// TEST VECTOR STRUCTURES +// ================================================================================================ + +/// Claim asset test vectors JSON embedded at compile time - contains both LeafData and ProofData +/// from a real claimAsset transaction. +const CLAIM_ASSET_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json"); + +/// Leaf data test vectors JSON embedded at compile time from the Foundry-generated file. +pub const LEAF_VALUE_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); + +/// Merkle proof verification vectors JSON embedded at compile time from the Foundry-generated file. +pub const MERKLE_PROOF_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); + +/// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol +/// Uses parallel arrays for leaves and roots. For each element from leaves/roots there are 32 +/// elements from merkle_paths, which represent the merkle path for that leaf + root. +#[derive(Debug, Deserialize)] +pub struct MerkleProofVerificationFile { + pub leaves: Vec, + pub roots: Vec, + pub merkle_paths: Vec, +} - (double_word[0..4].join(", "), double_word[4..8].join(", ")) +/// Lazily parsed Merkle proof vectors from the JSON file. +pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = + LazyLock::new(|| { + serde_json::from_str(MERKLE_PROOF_VECTORS_JSON) + .expect("failed to parse Merkle proof vectors JSON") + }); + +/// Deserialized leaf value test vector from Solidity-generated JSON. +#[derive(Debug, Deserialize)] +pub struct LeafValueVector { + pub origin_network: u32, + pub origin_token_address: String, + pub destination_network: u32, + pub destination_address: String, + pub amount: String, + pub metadata_hash: String, + #[allow(dead_code)] + pub leaf_value: String, +} + +impl LeafValueVector { + /// Converts this test vector into a `LeafData` instance. + pub fn to_leaf_data(&self) -> LeafData { + LeafData { + origin_network: self.origin_network, + origin_token_address: EthAddressFormat::from_hex(&self.origin_token_address) + .expect("valid origin token address hex"), + destination_network: self.destination_network, + destination_address: EthAddressFormat::from_hex(&self.destination_address) + .expect("valid destination address hex"), + amount: EthAmount::new(hex_to_bytes(&self.amount).expect("valid amount hex")), + metadata_hash: MetadataHash::new( + hex_to_bytes(&self.metadata_hash).expect("valid metadata hash hex"), + ), + } + } +} + +/// Deserialized proof value test vector from Solidity-generated JSON. +/// Contains SMT proofs, exit roots, global index, and expected global exit root. +#[derive(Debug, Deserialize)] +pub struct ProofValueVector { + pub smt_proof_local_exit_root: Vec, + pub smt_proof_rollup_exit_root: Vec, + pub global_index: String, + pub mainnet_exit_root: String, + pub rollup_exit_root: String, + /// Expected global exit root: keccak256(mainnetExitRoot || rollupExitRoot) + #[allow(dead_code)] + pub global_exit_root: String, +} + +impl ProofValueVector { + /// Converts this test vector into a `ProofData` instance. + pub fn to_proof_data(&self) -> ProofData { + // Parse SMT proofs (32 nodes each) + let smt_proof_local: [SmtNode; 32] = self + .smt_proof_local_exit_root + .iter() + .map(|s| SmtNode::new(hex_to_bytes(s).expect("valid smt proof hex"))) + .collect::>() + .try_into() + .expect("expected 32 SMT proof nodes for local exit root"); + + let smt_proof_rollup: [SmtNode; 32] = self + .smt_proof_rollup_exit_root + .iter() + .map(|s| SmtNode::new(hex_to_bytes(s).expect("valid smt proof hex"))) + .collect::>() + .try_into() + .expect("expected 32 SMT proof nodes for rollup exit root"); + + ProofData { + smt_proof_local_exit_root: smt_proof_local, + smt_proof_rollup_exit_root: smt_proof_rollup, + global_index: GlobalIndex::from_hex(&self.global_index) + .expect("valid global index hex"), + mainnet_exit_root: Keccak256Output::new( + hex_to_bytes(&self.mainnet_exit_root).expect("valid mainnet exit root hex"), + ), + rollup_exit_root: Keccak256Output::new( + hex_to_bytes(&self.rollup_exit_root).expect("valid rollup exit root hex"), + ), + } + } +} + +/// Deserialized claim asset test vector from Solidity-generated JSON. +/// Contains both LeafData and ProofData from a real claimAsset transaction. +#[derive(Debug, Deserialize)] +pub struct ClaimAssetVector { + #[serde(flatten)] + pub proof: ProofValueVector, + + #[serde(flatten)] + pub leaf: LeafValueVector, +} + +/// Lazily parsed claim asset test vector from the JSON file. +pub static CLAIM_ASSET_VECTOR: LazyLock = LazyLock::new(|| { + serde_json::from_str(CLAIM_ASSET_VECTORS_JSON) + .expect("failed to parse claim asset vectors JSON") +}); + +/// Returns real claim data from the claim_asset_vectors.json file. +/// +/// Returns a tuple of (ProofData, LeafData) parsed from the real on-chain claim transaction. +pub fn real_claim_data() -> (ProofData, LeafData) { + let vector = &*CLAIM_ASSET_VECTOR; + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data()) } /// Execute a program with default host and optional advice inputs @@ -48,92 +186,3 @@ pub async fn execute_program_with_default_host( let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); processor.execute(&program, &mut host).await } - -/* -// TODO: Uncomment this when https://github.com/0xMiden/miden-base/issues/2397 is ready. -// The mainnet exit root is hardcoded to pass the current test (i.e. we set the expected mainnet -// root to whatever the current implementation computes), and changing any impl. details will break -// the test, forcing us to artificially change the expected root every time. -// TESTING HELPERS -// ================================================================================================ - -/// Type alias for the complex return type of claim_note_test_inputs. -/// -/// Contains native types for the new ClaimNoteParams structure: -/// - smt_proof_local_exit_root: `Vec<[u8; 32]>` (256 bytes32 values) -/// - smt_proof_rollup_exit_root: `Vec<[u8; 32]>` (256 bytes32 values) -/// - global_index: [u8; 32] -/// - mainnet_exit_root: [u8; 32] -/// - rollup_exit_root: [u8; 32] -/// - origin_network: u32 -/// - origin_token_address: [u8; 20] -/// - destination_network: u32 -/// - metadata: [u8; 32] -pub type ClaimNoteTestInputs = ( - Vec<[u8; 32]>, - Vec<[u8; 32]>, - [u8; 32], - [u8; 32], - [u8; 32], - u32, - [u8; 20], - u32, - [u8; 32], -); - -/// Returns dummy test inputs for creating CLAIM notes with native types. -/// -/// This is a convenience function for testing that provides realistic dummy data -/// for all the agglayer claimAsset function inputs using native types. -/// -/// # Returns -/// A tuple containing native types for the new ClaimNoteParams structure -pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { - // Create SMT proofs with 32 bytes32 values each (SMT path depth) - let smt_proof_local_exit_root = vec![[0u8; 32]; 32]; - let smt_proof_rollup_exit_root = vec![[0u8; 32]; 32]; - // Global index format (32 bytes, big-endian like Solidity uint256): - // - bytes[0..20]: leading zeros (5 limbs) - // - bytes[20..24]: mainnet_flag = 1 (BE u32) - // - bytes[24..28]: rollup_index = 0 (BE u32) - // - bytes[28..32]: leaf_index = 2 (BE u32) - let mut global_index = [0u8; 32]; - global_index[23] = 1; // mainnet flag = 1 (BE: LSB at byte 23) - global_index[31] = 2; // leaf index = 2 (BE: LSB at byte 31) - - let mainnet_exit_root: [u8; 32] = [ - 0x05, 0xc2, 0xbe, 0x9d, 0xd7, 0xf4, 0x7e, 0xc6, 0x29, 0xae, 0x6a, 0xc1, 0x1a, 0x24, 0xb5, - 0x28, 0x59, 0xfd, 0x35, 0x8c, 0x31, 0x39, 0x00, 0xf5, 0x23, 0x1f, 0x84, 0x58, 0x63, 0x22, - 0xb5, 0x06, - ]; - - let rollup_exit_root: [u8; 32] = [ - 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, - 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, - ]; - - let origin_network = 1u32; - - let origin_token_address: [u8; 20] = [ - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xaa, 0xbb, 0xcc, - ]; - - let destination_network = 2u32; - - let metadata_hash: [u8; 32] = [0u8; 32]; - - ( - smt_proof_local_exit_root, - smt_proof_rollup_exit_root, - global_index, - mainnet_exit_root, - rollup_exit_root, - origin_network, - origin_token_address, - destination_network, - metadata_hash, - ) -} -*/ From c3b4497c2a8dd9d1e46adc57f2f11db09729a848 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 16 Feb 2026 10:02:36 +0100 Subject: [PATCH 06/21] chore(Agglayer): refactors to make scaling down easier (#2446) * chore: rename NOTE_INPUTS -> NOTE_STORAGE PTR * chore: explicit stack comment * chore: use constants for mem storage * chore: use mem locals for prefix/suffix * chore: use mem locals for amount * feat: serialize amounts as strings * feat: deserialize amount from json string * chore: amount conversion tests * chore: use UINT256 internally * chore: cleanup --- Cargo.lock | 1 + crates/miden-agglayer/Cargo.toml | 3 ++ .../asm/bridge/agglayer_faucet.masm | 53 ++++++++++++++----- .../test-vectors/claim_asset_vectors.json | 2 +- .../test-vectors/leaf_value_vectors.json | 2 +- .../test/ClaimAssetTestVectors.t.sol | 2 +- .../test/LeafValueTestVectors.t.sol | 3 +- crates/miden-agglayer/src/eth_types/amount.rs | 20 +++++++ crates/miden-agglayer/src/eth_types/mod.rs | 2 +- crates/miden-agglayer/src/lib.rs | 9 +++- .../tests/agglayer/test_utils.rs | 22 +++++++- 11 files changed, 97 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed558bfe4e..83d9bfa7ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1392,6 +1392,7 @@ dependencies = [ "miden-protocol", "miden-standards", "miden-utils-sync", + "primitive-types", "regex", "walkdir", ] diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml index 65b0e3e081..59091a7655 100644 --- a/crates/miden-agglayer/Cargo.toml +++ b/crates/miden-agglayer/Cargo.toml @@ -28,6 +28,9 @@ miden-protocol = { workspace = true } miden-standards = { workspace = true } miden-utils-sync = { workspace = true } +# Third-party dependencies +primitive-types = { workspace = true } + [dev-dependencies] miden-agglayer = { features = ["testing"], path = "." } diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index 2b4ec3a249..cc7702a9f2 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -30,7 +30,7 @@ const LEAF_DATA_KEY_MEM_ADDR = 704 const OUTPUT_NOTE_DATA_MEM_ADDR = 708 const CLAIM_NOTE_DATA_MEM_ADDR = 712 -const OUTPUT_NOTE_INPUTS_MEM_ADDR = 0 +const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 const OUTPUT_NOTE_TAG_MEM_ADDR = 574 const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 549 @@ -48,6 +48,16 @@ const DESTINATION_ADDRESS_2 = 546 const DESTINATION_ADDRESS_3 = 547 const DESTINATION_ADDRESS_4 = 548 +# Memory locals in claim +const CLAIM_PREFIX_MEM_LOC = 5 +const CLAIM_SUFFIX_MEM_LOC = 6 +const CLAIM_AMOUNT_MEM_LOC_0 = 0 +const CLAIM_AMOUNT_MEM_LOC_1 = 4 + +# Memory locals in build_p2id_output_note +const BUILD_P2ID_AMOUNT_MEM_LOC_0 = 0 +const BUILD_P2ID_AMOUNT_MEM_LOC_1 = 4 + # P2ID output note constants const P2ID_NOTE_NUM_STORAGE_ITEMS = 2 const OUTPUT_NOTE_TYPE_PUBLIC = 1 @@ -163,11 +173,19 @@ end #! It reads the destination account ID, amount, and other note parameters from memory to construct #! the output note. #! -#! Inputs: [] +#! Inputs: [prefix, suffix, AMOUNT[0], AMOUNT[1]] #! Outputs: [] #! #! Note: This procedure will be refactored in a follow-up to use leaf data to build the output note. +@locals(8) proc build_p2id_output_note + # write destination account id into memory for use in note::build_recipient + push.OUTPUT_NOTE_STORAGE_MEM_ADDR add.1 mem_store mem_store.OUTPUT_NOTE_STORAGE_MEM_ADDR + + # store amount in memory locals for use in faucets::distribute + loc_storew_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 dropw loc_storew_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 dropw + # => [pad(16)] + # Build P2ID output note procref.::miden::standards::notes::p2id::main # => [SCRIPT_ROOT] @@ -178,15 +196,8 @@ proc build_p2id_output_note push.P2ID_NOTE_NUM_STORAGE_ITEMS # => [note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - push.OUTPUT_NOTE_INPUTS_MEM_ADDR - # => [storage_ptr = 0, note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - - exec.get_destination_account_id_data - # => [prefix, suffix] - - # Write destination account id into memory - mem_store.1 mem_store.0 - # => [] + push.OUTPUT_NOTE_STORAGE_MEM_ADDR + # => [storage_ptr, note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] exec.note::build_recipient # => [RECIPIENT] @@ -197,10 +208,9 @@ proc build_p2id_output_note mem_load.OUTPUT_NOTE_TAG_MEM_ADDR # => [tag, RECIPIENT] - exec.get_raw_claim_amount - # => [AMOUNT[1], AMOUNT[0], tag, note_type, RECIPIENT] - # TODO: implement scale down logic; stubbed out for now + padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 + # => [AMOUNT[0], AMOUNT[1], tag, note_type, RECIPIENT] exec.asset_conversion::scale_u256_to_native_amount # => [amount, tag, note_type, RECIPIENT] @@ -252,11 +262,22 @@ end #! - any of the validations in faucets::distribute fail. #! #! Invocation: call +@locals(10) # 2 for prefix and suffix, 8 for amount pub proc claim # Check AdviceMap values hash to keys & write CLAIM inputs & DATA_KEYs to global memory exec.batch_pipe_double_words # => [pad(16)] + # validate_claim will overwrite memory in-place, so we need to load the account and amount + # before calling validate_claim and store it in memory locals + exec.get_destination_account_id_data + loc_store.CLAIM_PREFIX_MEM_LOC loc_store.CLAIM_SUFFIX_MEM_LOC + # => [pad(16)] + + exec.get_raw_claim_amount + loc_storew_be.CLAIM_AMOUNT_MEM_LOC_0 dropw loc_storew_be.CLAIM_AMOUNT_MEM_LOC_1 dropw + # => [pad(16)] + # VALIDATE CLAIM mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR # => [PROOF_DATA_KEY, pad(12)] @@ -269,6 +290,10 @@ pub proc claim # => [pad(16)] # Create P2ID output note + loc_loadw_be.CLAIM_AMOUNT_MEM_LOC_1 swapw loc_loadw_be.CLAIM_AMOUNT_MEM_LOC_0 + # => [AMOUNT[0], AMOUNT[1], pad(8)] + loc_load.CLAIM_SUFFIX_MEM_LOC loc_load.CLAIM_PREFIX_MEM_LOC + # => [prefix, suffix, AMOUNT[0], AMOUNT[1], pad(8)] exec.build_p2id_output_note # => [pad(16)] end diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json index df09f5a6a5..b0819ea63d 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json @@ -1,5 +1,5 @@ { - "amount": "0x00000000000000000000000000000000000000000000000000005af3107a4000", + "amount": 100000000000000, "destination_address": "0x00000000b0E79c68cafC54802726C6F102Cca300", "destination_network": 20, "global_exit_root": "0xe1cbfbde30bd598ee9aa2ac913b60d53e3297e51ed138bf86c500dd7d2391e7d", diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json index f20fb04025..8d89835c88 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json @@ -1,5 +1,5 @@ { - "amount": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000", + "amount": 2000000000000000000, "destination_address": "0xD9b20Fe633b609B01081aD0428e81f8Dd604F5C5", "destination_network": 7, "leaf_type": 0, diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol index 5ba23cbfdf..4352c232eb 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol @@ -124,7 +124,7 @@ contract ClaimAssetTestVectors is Test, DepositContractV2 { vm.serializeAddress(obj, "origin_token_address", originTokenAddress); vm.serializeUint(obj, "destination_network", destinationNetwork); vm.serializeAddress(obj, "destination_address", destinationAddress); - vm.serializeBytes32(obj, "amount", bytes32(amount)); + vm.serializeUint(obj, "amount", amount); vm.serializeBytes32(obj, "metadata_hash", metadataHash); string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); diff --git a/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol index 18ef6d12ca..ab4fdf8443 100644 --- a/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol @@ -46,14 +46,13 @@ contract LeafValueTestVectors is Test, DepositContractV2 { ); // Serialize to JSON - // Note: amount is serialized as bytes32 (hex string) to properly handle u256 values string memory obj = "root"; vm.serializeUint(obj, "leaf_type", leafType); vm.serializeUint(obj, "origin_network", originNetwork); vm.serializeAddress(obj, "origin_token_address", originTokenAddress); vm.serializeUint(obj, "destination_network", destinationNetwork); vm.serializeAddress(obj, "destination_address", destinationAddress); - vm.serializeBytes32(obj, "amount", bytes32(amount)); + vm.serializeUint(obj, "amount", amount); vm.serializeBytes32(obj, "metadata_hash", metadataHash); string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); diff --git a/crates/miden-agglayer/src/eth_types/amount.rs b/crates/miden-agglayer/src/eth_types/amount.rs index face01feee..09be851fcb 100644 --- a/crates/miden-agglayer/src/eth_types/amount.rs +++ b/crates/miden-agglayer/src/eth_types/amount.rs @@ -1,7 +1,9 @@ +use alloc::string::{String, ToString}; use alloc::vec::Vec; use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::Felt; +use primitive_types::U256; // ================================================================================================ // ETHEREUM AMOUNT @@ -14,12 +16,30 @@ use miden_protocol::Felt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EthAmount([u8; 32]); +/// Error type for parsing an [`EthAmount`] from a decimal string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EthAmountError(String); + impl EthAmount { /// Creates an [`EthAmount`] from a 32-byte array. pub fn new(bytes: [u8; 32]) -> Self { Self(bytes) } + /// Creates an [`EthAmount`] from a decimal (uint) string. + /// + /// The string should contain only ASCII decimal digits (e.g. `"2000000000000000000"`). + /// The value is stored as a 32-byte big-endian array, matching the Solidity uint256 layout. + /// + /// # Errors + /// + /// Returns [`EthAmountError`] if the string is empty, contains non-digit characters, + /// or represents a value that overflows uint256. + pub fn from_uint_str(s: &str) -> Result { + let value = U256::from_dec_str(s).map_err(|e| EthAmountError(e.to_string()))?; + Ok(Self(value.to_big_endian())) + } + /// Converts the amount to a vector of field elements for note storage. /// /// Each u32 value in the amount array is converted to a [`Felt`]. diff --git a/crates/miden-agglayer/src/eth_types/mod.rs b/crates/miden-agglayer/src/eth_types/mod.rs index c17753970a..3bee167e5d 100644 --- a/crates/miden-agglayer/src/eth_types/mod.rs +++ b/crates/miden-agglayer/src/eth_types/mod.rs @@ -4,6 +4,6 @@ pub mod global_index; pub mod metadata_hash; pub use address::EthAddressFormat; -pub use amount::EthAmount; +pub use amount::{EthAmount, EthAmountError}; pub use global_index::{GlobalIndex, GlobalIndexError}; pub use metadata_hash::MetadataHash; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 120a721239..f9d1ebfb68 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -42,7 +42,14 @@ pub use claim_note::{ SmtNode, create_claim_note, }; -pub use eth_types::{EthAddressFormat, EthAmount, GlobalIndex, GlobalIndexError, MetadataHash}; +pub use eth_types::{ + EthAddressFormat, + EthAmount, + EthAmountError, + GlobalIndex, + GlobalIndexError, + MetadataHash, +}; pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 45813ccfcf..8695638485 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -21,6 +21,25 @@ use miden_protocol::utils::sync::LazyLock; use miden_tx::utils::hex_to_bytes; use serde::Deserialize; +// SERDE HELPERS +// ================================================================================================ + +/// Deserializes a JSON value that may be either a number or a string into a `String`. +/// +/// Foundry's `vm.serializeUint` outputs JSON numbers for uint256 values. +/// This deserializer accepts both `"100"` (string) and `100` (number) forms. +fn deserialize_uint_to_string<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::String(s) => Ok(s), + serde_json::Value::Number(n) => Ok(n.to_string()), + _ => Err(serde::de::Error::custom("expected a number or string for amount")), + } +} + // TEST VECTOR STRUCTURES // ================================================================================================ @@ -61,6 +80,7 @@ pub struct LeafValueVector { pub origin_token_address: String, pub destination_network: u32, pub destination_address: String, + #[serde(deserialize_with = "deserialize_uint_to_string")] pub amount: String, pub metadata_hash: String, #[allow(dead_code)] @@ -77,7 +97,7 @@ impl LeafValueVector { destination_network: self.destination_network, destination_address: EthAddressFormat::from_hex(&self.destination_address) .expect("valid destination address hex"), - amount: EthAmount::new(hex_to_bytes(&self.amount).expect("valid amount hex")), + amount: EthAmount::from_uint_str(&self.amount).expect("valid amount uint string"), metadata_hash: MetadataHash::new( hex_to_bytes(&self.metadata_hash).expect("valid metadata hash hex"), ), From 0ff8f8f19a15f296500a0afd50e6874c8cead7a7 Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:33:59 +0300 Subject: [PATCH 07/21] feat: u256 to felt scaling procedure (#2331) * wip: u256 to felt scaling procedure * refactor: pass quotient via stack * refactor: procedure comments * refactor: modify tests & use assertz * refactor: cleanup test helper inputs * refactor: cleanup tests * feat: add inline sanity check test * refactor: cleanup test * refator: simplify and rename to verify_u256_to_native_amount_conversion * refactor: import FUNGIBLE_ASSET_MAX_AMOUNT & use word::eqz * revert: rename to try_to_u64 * refactor: use Felt::MODULUS * refactor: simplify EthAmount * refactor: rename EthAmountError variants * refactor: renamed functions and refactored errors & conversion * fix: taplo fmt * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * chore: clean up EthAmountError enum and improve docs * refactor: cleanup doc comments and test case * refactor: use existing error messages * fix: update global_index to use little-endian format * fix: clean up test_utils and crypto_utils files with agglayer-new changes * fix: rm unused function * chore: merge agglayer-new * feat: encorporate endianness reveral procedure * feat: clean up tests & add fuzzing test * refactor: add byte ordering comment & add testing attribute for from_u256 * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * Update crates/miden-testing/tests/agglayer/asset_conversion.rs Co-authored-by: Marti * refactor: cleanup fuzzing test & address comments * chore: use EthAmount directly in tests (#2455) --------- Co-authored-by: Marti --- Cargo.lock | 1 + crates/miden-agglayer/Cargo.toml | 1 + .../asm/bridge/agglayer_faucet.masm | 2 +- .../asm/bridge/asset_conversion.masm | 253 +++++++++++- crates/miden-agglayer/src/errors/agglayer.rs | 12 + crates/miden-agglayer/src/eth_types/amount.rs | 104 ++++- .../tests/agglayer/asset_conversion.rs | 390 +++++++++++++----- .../tests/agglayer/test_utils.rs | 30 ++ 8 files changed, 668 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83d9bfa7ad..c5db704828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1394,6 +1394,7 @@ dependencies = [ "miden-utils-sync", "primitive-types", "regex", + "thiserror", "walkdir", ] diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml index 59091a7655..70ca24bc7a 100644 --- a/crates/miden-agglayer/Cargo.toml +++ b/crates/miden-agglayer/Cargo.toml @@ -30,6 +30,7 @@ miden-utils-sync = { workspace = true } # Third-party dependencies primitive-types = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] miden-agglayer = { features = ["testing"], path = "." } diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index cc7702a9f2..f271cfc998 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -211,7 +211,7 @@ proc build_p2id_output_note # TODO: implement scale down logic; stubbed out for now padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 # => [AMOUNT[0], AMOUNT[1], tag, note_type, RECIPIENT] - exec.asset_conversion::scale_u256_to_native_amount + exec.asset_conversion::verify_u256_to_native_amount_conversion_stubbed # => [amount, tag, note_type, RECIPIENT] exec.faucets::distribute diff --git a/crates/miden-agglayer/asm/bridge/asset_conversion.masm b/crates/miden-agglayer/asm/bridge/asset_conversion.masm index e4f59f17d4..c3e6201f03 100644 --- a/crates/miden-agglayer/asm/bridge/asset_conversion.masm +++ b/crates/miden-agglayer/asm/bridge/asset_conversion.masm @@ -1,14 +1,22 @@ use miden::core::math::u64 use miden::core::word +use miden::agglayer::utils +use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT # CONSTANTS # ================================================================================================= const MAX_SCALING_FACTOR=18 -# ERRORS +# ERRORS # ================================================================================================= + const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT="maximum scaling factor is 18" +const ERR_X_TOO_LARGE="x must fit into 128 bits (x4..x7 must be 0)" +const ERR_UNDERFLOW="x < y*10^s (underflow detected)" +const ERR_REMAINDER_TOO_LARGE="remainder z must be < 10^s" + +const ERR_Y_TOO_LARGE="y exceeds max fungible token amount" #! Calculate 10^scale where scale is a u8 exponent. #! @@ -105,10 +113,241 @@ pub proc scale_native_amount_to_u256 # => [RESULT_U256[0], RESULT_U256[1]] end -#! TODO: implement scaling down -#! -#! Inputs: [U256[0], U256[1]] -#! Outputs: [amount] -pub proc scale_u256_to_native_amount - repeat.7 drop end +#! Reverse the limbs and change the byte endianness of the result. +pub proc reverse_limbs_and_change_byte_endianness + # reverse the felts within each word + # [a, b, c, d, e, f, g, h] -> [h, g, f, e, d, c, b, a] + exec.word::reverse + swapw + exec.word::reverse + + # change the byte endianness of each felt + repeat.8 + exec.utils::swap_u32_bytes + movdn.7 + end + + # => [RESULT_U256[0], RESULT_U256[1]] end + +#! Subtract two 128-bit integers (little-endian u32 limbs) and assert no underflow. +#! +#! Computes: +#! z = x - y +#! with the constraint: +#! y <= x +#! +#! Inputs: [y0, y1, y2, y3, x0, x1, x2, x3] +#! Outputs: [z0, z1, z2, z3] +#! +#! Panics if: +#! - y > x (ERR_UNDERFLOW) +proc u128_sub_no_underflow + # Put x-word on top for easier access. + swapw + # => [x0, x1, x2, x3, y0, y1, y2, y3] + + # --------------------------------------------------------------------------------------------- + # Low 64 bits: (x1,x0) - (y1,y0) + # Arrange args for u64::overflowing_sub as: + # [y1, y0, x1, x0] + # --------------------------------------------------------------------------------------------- + swap + # => [x1, x0, x2, x3, y0, y1, y2, y3] + + movup.5 + movup.5 + swap + # => [y1, y0, x1, x0, x2, x3, y2, y3] + + exec.u64::overflowing_sub + # => [borrow_low, z1, z0, x2, x3, y2, y3] + + # --------------------------------------------------------------------------------------------- + # High 64 bits (raw): (x3,x2) - (y3,y2) + # Arrange args as: + # [y3, y2, x3, x2] + # --------------------------------------------------------------------------------------------- + movup.3 + movup.4 + # => [x3, x2, borrow_low, z1, z0, y2, y3] + + movup.6 + movup.6 + swap + # => [y3, y2, x3, x2, borrow_low, z1, z0] + + exec.u64::overflowing_sub + # => [underflow_high_raw, t_hi, t_lo, borrow_low, z1, z0] + + # --------------------------------------------------------------------------------------------- + # Apply propagated borrow from low 64-bit subtraction: + # (t_hi,t_lo) - borrow_low + # --------------------------------------------------------------------------------------------- + swap.3 + push.0 + exec.u64::overflowing_sub + # => [underflow_high_borrow, z3, z2, underflow_high_raw, z1, z0] + + # Underflow iff either high-half step underflowed. + movup.3 or + assertz.err=ERR_UNDERFLOW + # => [z3, z2, z1, z0] + + # Return little-endian limbs. + exec.word::reverse + # => [z0, z1, z2, z3] +end + +#! Verify conversion from an AggLayer U256 amount to a Miden native amount (Felt) +#! +#! Specification: +#! Verify that a provided y is the quotient of dividing x by 10^scale_exp: +#! y = floor(x / 10^scale_exp) +#! +#! This procedure does NOT perform division. It proves the quotient is correct by checking: +#! 1) y is within the allowed fungible token amount range +#! 2) y_scaled = y * 10^scale_exp (computed via scale_native_amount_to_u256) +#! 3) z = x - y_scaled (must not underflow, i.e. y_scaled <= x) +#! 4) z fits in 64 bits (upper 192 bits are zero) +#! 5) (z1, z0) < 10^scale_exp (remainder bound) +#! +#! These conditions prove: +#! x = y_scaled + z, with 0 <= z < 10^scale_exp +#! which uniquely implies: +#! y = floor(x / 10^scale_exp) +#! +#! Example (ETH -> Miden base 1e8): +#! - EVM amount: 100 ETH = 100 * 10^18 +#! - Miden amount: 100 ETH = 100 * 10^8 +#! - Therefore the scale-down factor is: +#! scale = 10^(18 - 8) = 10^10 +#! scale_exp = 10 +#! - Inputs/expected values: +#! x = 100 * 10^18 +#! y = floor(x / 10^10) = 100 * 10^8 +#! y_scaled = y * 10^10 = 100 * 10^18 +#! z = x - y_scaled = 0 +#! +#! NOTE: For efficiency, this verifier enforces x < 2^128 by requiring x4..x7 == 0. +#! +#! Inputs: [x7, x6, x5, x4, x3, x2, x1, x0, scale_exp, y] +#! Where x is encoded as 8 u32 limbs in big-endian order. +#! (x7 is most significant limb and is at the top of the stack) +#! Each limb is expected to contain little-endian bytes. +#! Outputs: [y] +#! +#! Where: +#! - x: The original AggLayer amount as an unsigned 256-bit integer (U256). +#! It is provided on the operand stack as 8 big-endian u32 limbs: +#! x = x0 + x1·2^32 + x2·2^64 + x3·2^96 + x4·2^128 + x5·2^160 + x6·2^192 + x7·2^224 +#! - x0..x7: 32-bit limbs of x in big-endian order (x0 is least significant). +#! - scale_exp: The base-10 exponent used for scaling down (an integer in [0, 18]). +#! - y: The provided quotient (Miden native amount) as a Felt interpreted as an unsigned u64. +#! - y_scaled: The 256-bit value y * 10^scale_exp represented as 8 u32 limbs (big-endian). +#! - z: The remainder-like difference z = x - y_scaled (essentially dust that is lost in the +#! conversion due to precision differences). This verifier requires z < 10^scale_exp. +#! +#! Panics if: +#! - scale_exp > 18 (asserted in pow10 via scale_native_amount_to_u256) +#! - y exceeds the max fungible token amount +#! - x does not fit into 128 bits (x4..x7 are not all zero) +#! - x < y * 10^scale_exp (underflow) +#! - z does not fit in 64 bits +#! - (z1, z0) >= 10^scale_exp (remainder too large) +pub proc verify_u256_to_native_amount_conversion + + # reverse limbs and byte endianness + exec.reverse_limbs_and_change_byte_endianness + # => [x0, x1, x2, x3, x4, x5, x6, x7, scale_exp, y] + + # ============================================================================================= + # Step 0: Enforce x < 2^128 + # Constraint: x4 == x5 == x6 == x7 == 0 + # ============================================================================================= + swapw + exec.word::eqz + assert.err=ERR_X_TOO_LARGE + # => [x0, x1, x2, x3, scale_exp, y] + + # ============================================================================================= + # Step 1: Enforce y <= MAX_FUNGIBLE_TOKEN_AMOUNT + # Constraint: y <= MAX_FUNGIBLE_TOKEN_AMOUNT + # ============================================================================================= + dup.5 + push.FUNGIBLE_ASSET_MAX_AMOUNT + lte + # => [is_lte, x0, x1, x2, x3, scale_exp, y] + + assert.err=ERR_Y_TOO_LARGE + # => [x0, x1, x2, x3, scale_exp, y] + + # ============================================================================================= + # Step 2: Compute y_scaled = y * 10^scale_exp + # + # Call: + # scale_native_amount_to_u256(amount=y, target_scale=scale_exp) + # ============================================================================================= + movup.4 + movup.5 + # => [y, scale_exp, x0, x1, x2, x3] + + dup.1 dup.1 + # => [y, scale_exp, y, scale_exp, x0, x1, x2, x3] + + exec.scale_native_amount_to_u256 + # => [y_scaled0..y_scaled7, y, scale_exp, x0, x1, x2, x3] + + # Drop the upper word as it's guaranteed to be zero since y_scaled will fit in 123 bits + # (amount: 63 bits, 10^target_scale: 60 bits). + swapw dropw + # => [y_scaled0, y_scaled1, y_scaled2, y_scaled3, y, scale_exp, x0, x1, x2, x3] + + # ============================================================================================= + # Step 3: Compute z = x - y_scaled and prove no underflow + # z := x - y_scaled + # Constraint: y_scaled <= x + # ============================================================================================= + movup.5 movup.5 + # => [y, scale_exp, y_scaled0, y_scaled1, y_scaled2, y_scaled3, x0, x1, x2, x3] + + movdn.9 movdn.9 + # => [y_scaled0, y_scaled1, y_scaled2, y_scaled3, x0, x1, x2, x3, y, scale_exp] + + exec.u128_sub_no_underflow + # => [z0, z1, z2, z3, y, scale_exp] + + # ============================================================================================= + # Step 4: Enforce z < 10^scale_exp (remainder bound) + # + # We compare z against 10^scale_exp using a u64 comparison on (z1, z0). + # To make that comparison complete, we must first prove z fits into 64 bits, i.e. z2 == z3 == 0. + # + # This is justified because scale_exp <= 18, so 10^scale_exp <= 10^18 < 2^60. + # Therefore any valid remainder z < 10^scale_exp must be < 2^60 and thus must have z2 == z3 == 0. + # ============================================================================================= + exec.word::reverse + # => [z3, z2, z1, z0, y, scale_exp] + + assertz.err=ERR_REMAINDER_TOO_LARGE # z3 == 0 + assertz.err=ERR_REMAINDER_TOO_LARGE # z2 == 0 + # => [z1, z0, y, scale_exp] + + movup.3 + exec.pow10 + # => [scale, z1, z0, y] + + u32split + # => [scale_hi, scale_lo, z1, z0, y] + + exec.u64::lt + # => [is_lt, y] + + assert.err=ERR_REMAINDER_TOO_LARGE + # => [y] +end + +# TODO: Rm & use verify_u256_to_native_amount_conversion +pub proc verify_u256_to_native_amount_conversion_stubbed + repeat.7 drop end +end \ No newline at end of file diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index f5b26c15c7..e86d5336f4 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -40,6 +40,9 @@ pub const ERR_MSB_NONZERO: MasmError = MasmError::from_static_str("most-signific /// Error Message: "address limb is not u32" pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); +/// Error Message: "remainder z must be < 10^s" +pub const ERR_REMAINDER_TOO_LARGE: MasmError = MasmError::from_static_str("remainder z must be < 10^s"); + /// Error Message: "rollup index must be zero for a mainnet deposit" pub const ERR_ROLLUP_INDEX_NON_ZERO: MasmError = MasmError::from_static_str("rollup index must be zero for a mainnet deposit"); @@ -49,7 +52,16 @@ pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_st /// Error Message: "merkle proof verification failed: provided SMT root does not match the computed root" pub const ERR_SMT_ROOT_VERIFICATION_FAILED: MasmError = MasmError::from_static_str("merkle proof verification failed: provided SMT root does not match the computed root"); +/// Error Message: "x < y*10^s (underflow detected)" +pub const ERR_UNDERFLOW: MasmError = MasmError::from_static_str("x < y*10^s (underflow detected)"); + /// Error Message: "UPDATE_GER note attachment target account does not match consuming account" pub const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("UPDATE_GER note attachment target account does not match consuming account"); /// Error Message: "UPDATE_GER script expects exactly 8 note storage items" pub const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("UPDATE_GER script expects exactly 8 note storage items"); + +/// Error Message: "x must fit into 128 bits (x4..x7 must be 0)" +pub const ERR_X_TOO_LARGE: MasmError = MasmError::from_static_str("x must fit into 128 bits (x4..x7 must be 0)"); + +/// Error Message: "y exceeds max fungible token amount" +pub const ERR_Y_TOO_LARGE: MasmError = MasmError::from_static_str("y exceeds max fungible token amount"); diff --git a/crates/miden-agglayer/src/eth_types/amount.rs b/crates/miden-agglayer/src/eth_types/amount.rs index 09be851fcb..3a74d2a8af 100644 --- a/crates/miden-agglayer/src/eth_types/amount.rs +++ b/crates/miden-agglayer/src/eth_types/amount.rs @@ -1,9 +1,31 @@ -use alloc::string::{String, ToString}; use alloc::vec::Vec; use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::Felt; +use miden_protocol::asset::FungibleAsset; use primitive_types::U256; +use thiserror::Error; + +// ================================================================================================ +// ETHEREUM AMOUNT ERROR +// ================================================================================================ + +/// Error type for Ethereum amount conversions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum EthAmountError { + /// The amount doesn't fit in the target type. + #[error("amount overflow: value doesn't fit in target type")] + Overflow, + /// The scaling factor is too large (> 18). + #[error("scaling factor too large: maximum is 18")] + ScaleTooLarge, + /// The scaled-down value doesn't fit in a u64. + #[error("scaled value doesn't fit in u64")] + ScaledValueDoesNotFitU64, + /// The scaled-down value exceeds the maximum fungible token amount. + #[error("scaled value exceeds the maximum fungible token amount")] + ScaledValueExceedsMaxFungibleAmount, +} // ================================================================================================ // ETHEREUM AMOUNT @@ -16,10 +38,6 @@ use primitive_types::U256; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EthAmount([u8; 32]); -/// Error type for parsing an [`EthAmount`] from a decimal string. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EthAmountError(String); - impl EthAmount { /// Creates an [`EthAmount`] from a 32-byte array. pub fn new(bytes: [u8; 32]) -> Self { @@ -36,10 +54,23 @@ impl EthAmount { /// Returns [`EthAmountError`] if the string is empty, contains non-digit characters, /// or represents a value that overflows uint256. pub fn from_uint_str(s: &str) -> Result { - let value = U256::from_dec_str(s).map_err(|e| EthAmountError(e.to_string()))?; + let value = U256::from_dec_str(s).map_err(|_| EthAmountError::Overflow)?; Ok(Self(value.to_big_endian())) } + /// Converts the EthAmount to a U256 for easier arithmetic operations. + pub fn to_u256(&self) -> U256 { + U256::from_big_endian(&self.0) + } + + /// Creates an EthAmount from a U256 value. + /// + /// This constructor is only available in test code to make test arithmetic easier. + #[cfg(any(test, feature = "testing"))] + pub fn from_u256(value: U256) -> Self { + Self(value.to_big_endian()) + } + /// Converts the amount to a vector of field elements for note storage. /// /// Each u32 value in the amount array is converted to a [`Felt`]. @@ -52,3 +83,64 @@ impl EthAmount { &self.0 } } + +// ================================================================================================ +// U256 SCALING DOWN HELPERS +// ================================================================================================ + +/// Maximum scaling factor for decimal conversions +const MAX_SCALING_FACTOR: u32 = 18; + +/// Calculate 10^scale where scale is a u32 exponent. +/// +/// # Errors +/// Returns [`EthAmountError::ScaleTooLarge`] if scale > 18. +fn pow10_u64(scale: u32) -> Result { + if scale > MAX_SCALING_FACTOR { + return Err(EthAmountError::ScaleTooLarge); + } + Ok(10_u64.pow(scale)) +} + +impl EthAmount { + /// Converts a U256 amount to a Miden Felt by scaling down by 10^scale_exp. + /// + /// This is the deterministic reference implementation that computes: + /// - `y = floor(x / 10^scale_exp)` (the Miden amount as a Felt) + /// + /// # Arguments + /// * `scale_exp` - The scaling exponent (0-18) + /// + /// # Returns + /// The scaled-down Miden amount as a Felt + /// + /// # Errors + /// - [`EthAmountError::ScaleTooLarge`] if scale_exp > 18 + /// - [`EthAmountError::ScaledValueDoesNotFitU64`] if the result doesn't fit in a u64 + /// - [`EthAmountError::ScaledValueExceedsMaxFungibleAmount`] if the scaled value exceeds the + /// maximum fungible token amount + /// + /// # Example + /// ```ignore + /// let eth_amount = EthAmount::from_u64(1_000_000_000_000_000_000); // 1 ETH in wei + /// let miden_amount = eth_amount.scale_to_token_amount(12)?; + /// // Result: 1_000_000 (1e6, Miden representation) + /// ``` + pub fn scale_to_token_amount(&self, scale_exp: u32) -> Result { + let x = self.to_u256(); + let scale = U256::from(pow10_u64(scale_exp)?); + + let y_u256 = x / scale; + + // y must fit into u64; canonical Felt is guaranteed by max amount bound + let y_u64: u64 = y_u256.try_into().map_err(|_| EthAmountError::ScaledValueDoesNotFitU64)?; + + if y_u64 > FungibleAsset::MAX_AMOUNT { + return Err(EthAmountError::ScaledValueExceedsMaxFungibleAmount); + } + + // Safe because FungibleAsset::MAX_AMOUNT < Felt modulus + let y_felt = Felt::try_from(y_u64).expect("scaled value must fit into canonical Felt"); + Ok(y_felt) + } +} diff --git a/crates/miden-testing/tests/agglayer/asset_conversion.rs b/crates/miden-testing/tests/agglayer/asset_conversion.rs index 1e3c91cbd1..7a7565c743 100644 --- a/crates/miden-testing/tests/agglayer/asset_conversion.rs +++ b/crates/miden-testing/tests/agglayer/asset_conversion.rs @@ -1,40 +1,32 @@ extern crate alloc; -use alloc::sync::Arc; - -use miden_agglayer::{agglayer_library, utils}; -use miden_assembly::{Assembler, DefaultSourceManager}; -use miden_core_lib::CoreLibrary; -use miden_processor::fast::ExecutionOutput; +use miden_agglayer::errors::{ + ERR_REMAINDER_TOO_LARGE, + ERR_SCALE_AMOUNT_EXCEEDED_LIMIT, + ERR_UNDERFLOW, + ERR_X_TOO_LARGE, +}; +use miden_agglayer::eth_types::amount::EthAmount; +use miden_agglayer::utils; use miden_protocol::Felt; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::errors::MasmError; use primitive_types::U256; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; -use super::test_utils::execute_program_with_default_host; - -/// Convert a Vec to a U256 -fn felts_to_u256(felts: Vec) -> U256 { - assert_eq!(felts.len(), 8, "expected exactly 8 felts"); - let array: [Felt; 8] = - [felts[0], felts[1], felts[2], felts[3], felts[4], felts[5], felts[6], felts[7]]; - let bytes = utils::felts_to_bytes(&array); - U256::from_little_endian(&bytes) -} +use super::test_utils::{assert_execution_fails_with, execute_masm_script}; -/// Convert the top 8 u32 values from the execution stack to a U256 -fn stack_to_u256(exec_output: &ExecutionOutput) -> U256 { - let felts: Vec = exec_output.stack[0..8].to_vec(); - felts_to_u256(felts) -} +// ================================================================================================ +// SCALE UP TESTS (Felt -> U256) +// ================================================================================================ -/// Helper function to test convert_felt_to_u256_scaled with given parameters -async fn test_convert_to_u256_helper( +/// Helper function to test scale_native_amount_to_u256 with given parameters +async fn test_scale_up_helper( miden_amount: Felt, scale_exponent: Felt, - expected_result_array: [u32; 8], - expected_result_u256: U256, + expected_result: EthAmount, ) -> anyhow::Result<()> { - let asset_conversion_lib = agglayer_library(); - let script_code = format!( " use miden::core::sys @@ -49,53 +41,35 @@ async fn test_convert_to_u256_helper( scale_exponent, miden_amount, ); - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(asset_conversion_lib.clone()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); - - let exec_output = execute_program_with_default_host(program, None).await?; - - // Extract the first 8 u32 values from the stack (the U256 representation) - let actual_result: [u32; 8] = [ - exec_output.stack[0].as_int() as u32, - exec_output.stack[1].as_int() as u32, - exec_output.stack[2].as_int() as u32, - exec_output.stack[3].as_int() as u32, - exec_output.stack[4].as_int() as u32, - exec_output.stack[5].as_int() as u32, - exec_output.stack[6].as_int() as u32, - exec_output.stack[7].as_int() as u32, - ]; + let exec_output = execute_masm_script(&script_code).await?; + let actual_felts: Vec = exec_output.stack[0..8].to_vec(); - let actual_result_u256 = stack_to_u256(&exec_output); + // to_elements() returns big-endian limb order with each limb byte-swapped (LE-interpreted + // from BE source bytes). The scale-up output is native u32 limbs in LE limb order, so we + // reverse the limbs and swap bytes within each u32 to match. + let expected_felts: Vec = expected_result + .to_elements() + .into_iter() + .rev() + .map(|f| Felt::new((f.as_int() as u32).swap_bytes() as u64)) + .collect(); - assert_eq!(actual_result, expected_result_array); - assert_eq!(actual_result_u256, expected_result_u256); + assert_eq!(actual_felts, expected_felts); Ok(()) } #[tokio::test] -async fn test_convert_to_u256_basic_examples() -> anyhow::Result<()> { +async fn test_scale_up_basic_examples() -> anyhow::Result<()> { // Test case 1: amount=1, no scaling (scale_exponent=0) - test_convert_to_u256_helper( - Felt::new(1), - Felt::new(0), - [1, 0, 0, 0, 0, 0, 0, 0], - U256::from(1u64), - ) - .await?; + test_scale_up_helper(Felt::new(1), Felt::new(0), EthAmount::from_uint_str("1").unwrap()) + .await?; // Test case 2: amount=1, scale to 1e18 (scale_exponent=18) - test_convert_to_u256_helper( + test_scale_up_helper( Felt::new(1), Felt::new(18), - [2808348672, 232830643, 0, 0, 0, 0, 0, 0], - U256::from_dec_str("1000000000000000000").unwrap(), + EthAmount::from_uint_str("1000000000000000000").unwrap(), ) .await?; @@ -103,86 +77,280 @@ async fn test_convert_to_u256_basic_examples() -> anyhow::Result<()> { } #[tokio::test] -async fn test_convert_to_u256_scaled_eth() -> anyhow::Result<()> { - // 100 units base 1e6 - let miden_amount = Felt::new(100_000_000); +async fn test_scale_up_realistic_amounts() -> anyhow::Result<()> { + // 100 units base 1e6, scale to 1e18 + test_scale_up_helper( + Felt::new(100_000_000), + Felt::new(12), + EthAmount::from_uint_str("100000000000000000000").unwrap(), + ) + .await?; - // scale to 1e18 - let target_scale = Felt::new(12); + // Large amount: 1e18 units scaled by 8 + test_scale_up_helper( + Felt::new(1000000000000000000), + Felt::new(8), + EthAmount::from_uint_str("100000000000000000000000000").unwrap(), + ) + .await?; - let asset_conversion_lib = agglayer_library(); + Ok(()) +} - let script_code = format!( - " +#[tokio::test] +async fn test_scale_up_exceeds_max_scale() { + // scale_exp = 19 should fail + let script_code = " use miden::core::sys use miden::agglayer::asset_conversion begin - push.{}.{} + push.19.1 exec.asset_conversion::scale_native_amount_to_u256 exec.sys::truncate_stack end - ", - target_scale, miden_amount, - ); + "; - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(asset_conversion_lib.clone()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); + assert_execution_fails_with(script_code, "maximum scaling factor is 18").await; +} - let exec_output = execute_program_with_default_host(program, None).await?; +// ================================================================================================ +// SCALE DOWN TESTS (U256 -> Felt) +// ================================================================================================ - let expected_result = U256::from_dec_str("100000000000000000000").unwrap(); - let actual_result = stack_to_u256(&exec_output); +/// Build MASM script for verify_u256_to_native_amount_conversion +fn build_scale_down_script(x: EthAmount, scale_exp: u32, y: u64) -> String { + let x_felts = x.to_elements(); + format!( + r#" + use miden::core::sys + use miden::agglayer::asset_conversion + + begin + push.{}.{}.{}.{}.{}.{}.{}.{}.{}.{} + exec.asset_conversion::verify_u256_to_native_amount_conversion + exec.sys::truncate_stack + end + "#, + y, + scale_exp, + x_felts[7].as_int(), + x_felts[6].as_int(), + x_felts[5].as_int(), + x_felts[4].as_int(), + x_felts[3].as_int(), + x_felts[2].as_int(), + x_felts[1].as_int(), + x_felts[0].as_int(), + ) +} - assert_eq!(actual_result, expected_result); +/// Assert that scaling down succeeds with the correct result +async fn assert_scale_down_ok(x: EthAmount, scale: u32) -> anyhow::Result { + let y = x.scale_to_token_amount(scale).unwrap().as_int(); + let script = build_scale_down_script(x, scale, y); + let out = execute_masm_script(&script).await?; + assert_eq!(out.stack[0].as_int(), y); + Ok(y) +} +/// Assert that scaling down fails with the given y and expected error +async fn assert_scale_down_fails(x: EthAmount, scale: u32, y: u64, expected_error: MasmError) { + let script = build_scale_down_script(x, scale, y); + assert_execution_fails_with(&script, expected_error.message()).await; +} + +/// Test that y-1 and y+1 both fail appropriately +async fn assert_y_plus_minus_one_behavior(x: EthAmount, scale: u32) -> anyhow::Result<()> { + let y = assert_scale_down_ok(x, scale).await?; + if y > 0 { + assert_scale_down_fails(x, scale, y - 1, ERR_REMAINDER_TOO_LARGE).await; + } + assert_scale_down_fails(x, scale, y + 1, ERR_UNDERFLOW).await; + Ok(()) +} + +#[tokio::test] +async fn test_scale_down_basic_examples() -> anyhow::Result<()> { + let cases = [ + (EthAmount::from_uint_str("1000000000000000000").unwrap(), 10u32), + (EthAmount::from_uint_str("1000").unwrap(), 0u32), + (EthAmount::from_uint_str("10000000000000000000").unwrap(), 18u32), + ]; + + for (x, s) in cases { + assert_scale_down_ok(x, s).await?; + } Ok(()) } +// ================================================================================================ +// FUZZING TESTS +// ================================================================================================ + +// Fuzz test that validates verify_u256_to_native_amount_conversion (U256 → Felt) +// with random realistic amounts for all scale exponents (0..=18). #[tokio::test] -async fn test_convert_to_u256_scaled_large_amount() -> anyhow::Result<()> { - // 100,000,000 units (base 1e10) - let miden_amount = Felt::new(1000000000000000000); +async fn test_scale_down_realistic_scenarios_fuzzing() -> anyhow::Result<()> { + const CASES_PER_SCALE: usize = 2; + const MAX_SCALE: u32 = 18; + + let mut rng = StdRng::seed_from_u64(42); - // scale to base 1e18 - let scale_exponent = Felt::new(8); + let min_x = U256::from(10_000_000_000_000u64); // 1e13 + let desired_max_x = U256::from_dec_str("1000000000000000000000000").unwrap(); // 1e24 + let max_y = U256::from(FungibleAsset::MAX_AMOUNT); // 2^63 - 2^31 - let asset_conversion_lib = agglayer_library(); + for scale in 0..=MAX_SCALE { + let scale_factor = U256::from(10u64).pow(U256::from(scale)); + // Ensure x always scales down into a y that fits the fungible-token bound. + let max_x = desired_max_x.min(max_y * scale_factor); + + assert!(max_x > min_x, "max_x must exceed min_x for scale={scale}"); + + // Sample x uniformly from [min_x, max_x). + let span: u128 = (max_x - min_x).try_into().expect("span fits in u128"); + + for _ in 0..CASES_PER_SCALE { + let offset: u128 = rng.random_range(0..span); + let x = EthAmount::from_u256(min_x + U256::from(offset)); + assert_scale_down_ok(x, scale).await?; + } + } + + Ok(()) +} + +// ================================================================================================ +// NEGATIVE TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_scale_down_wrong_y_clean_case() -> anyhow::Result<()> { + let x = EthAmount::from_uint_str("10000000000000000000").unwrap(); + assert_y_plus_minus_one_behavior(x, 18).await +} + +#[tokio::test] +async fn test_scale_down_wrong_y_with_remainder() -> anyhow::Result<()> { + let x = EthAmount::from_uint_str("1500000000000000000").unwrap(); + assert_y_plus_minus_one_behavior(x, 18).await +} + +// ================================================================================================ +// NEGATIVE TESTS - BOUNDS +// ================================================================================================ + +#[tokio::test] +async fn test_scale_down_exceeds_max_scale() { + let x = EthAmount::from_uint_str("1000").unwrap(); + let s = 19u32; + let y = 1u64; + assert_scale_down_fails(x, s, y, ERR_SCALE_AMOUNT_EXCEEDED_LIMIT).await; +} + +#[tokio::test] +async fn test_scale_down_x_too_large() { + // Construct x with upper limbs non-zero (>= 2^128) + let x = EthAmount::from_u256(U256::from(1u64) << 128); + let s = 0u32; + let y = 0u64; + assert_scale_down_fails(x, s, y, ERR_X_TOO_LARGE).await; +} + +// ================================================================================================ +// REMAINDER EDGE TEST +// ================================================================================================ + +#[tokio::test] +async fn test_scale_down_remainder_edge() -> anyhow::Result<()> { + // Force z = scale - 1: pick y=5, s=10, so scale=10^10 + // Set x = y*scale + (scale-1) = 5*10^10 + (10^10 - 1) = 59999999999 + let scale_exp = 10u32; + let scale = 10u64.pow(scale_exp); + let x_val = 5u64 * scale + (scale - 1); + let x = EthAmount::from_u256(U256::from(x_val)); + + assert_scale_down_ok(x, scale_exp).await?; + Ok(()) +} + +#[tokio::test] +async fn test_scale_down_remainder_exactly_scale_fails() { + // If remainder z = scale, it should fail + // Pick s=10, x = 6*scale (where scale = 10^10) + // The correct y should be 6, so providing y=5 should fail + let scale_exp = 10u32; + let scale = 10u64.pow(scale_exp); + let x = EthAmount::from_u256(U256::from(6u64 * scale)); + + // Calculate the correct y using scale_to_token_amount + let correct_y = x.scale_to_token_amount(scale_exp).unwrap().as_int(); + assert_eq!(correct_y, 6); + + // Providing wrong_y = correct_y - 1 should fail with ERR_REMAINDER_TOO_LARGE + let wrong_y = correct_y - 1; + assert_scale_down_fails(x, scale_exp, wrong_y, ERR_REMAINDER_TOO_LARGE).await; +} + +// ================================================================================================ +// INLINE SCALE DOWN TEST +// ================================================================================================ + +#[tokio::test] +async fn test_verify_scale_down_inline() -> anyhow::Result<()> { + // Test: Take 100 * 1e18 and scale to base 1e8 + // This means we divide by 1e10 (scale_exp = 10) + // x = 100 * 1e18 = 100000000000000000000 + // y = x / 1e10 = 10000000000 (100 * 1e8) + let x = EthAmount::from_uint_str("100000000000000000000").unwrap(); + let scale_exp = 10u32; + let y = x.scale_to_token_amount(scale_exp).unwrap().as_int(); + + let x_felts = x.to_elements(); + + // Build the MASM script inline let script_code = format!( - " + r#" use miden::core::sys use miden::agglayer::asset_conversion - + begin - push.{}.{} - - exec.asset_conversion::scale_native_amount_to_u256 + # Push y (expected quotient) + push.{} + + # Push scale_exp + push.{} + + # Push x as 8 u32 limbs (little-endian, x0 at top) + push.{}.{}.{}.{}.{}.{}.{}.{} + + # Call the scale down procedure + exec.asset_conversion::verify_u256_to_native_amount_conversion + + # Truncate stack to just return y exec.sys::truncate_stack end - ", - scale_exponent, miden_amount, + "#, + y, + scale_exp, + x_felts[7].as_int(), + x_felts[6].as_int(), + x_felts[5].as_int(), + x_felts[4].as_int(), + x_felts[3].as_int(), + x_felts[2].as_int(), + x_felts[1].as_int(), + x_felts[0].as_int(), ); - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(asset_conversion_lib.clone()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); - - let exec_output = execute_program_with_default_host(program, None).await?; - - let expected_result = U256::from_dec_str("100000000000000000000000000").unwrap(); - let actual_result = stack_to_u256(&exec_output); + // Execute the script + let exec_output = execute_masm_script(&script_code).await?; - assert_eq!(actual_result, expected_result); + // Verify the result + let result = exec_output.stack[0].as_int(); + assert_eq!(result, y); Ok(()) } diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 8695638485..9da02110d1 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -1,6 +1,7 @@ extern crate alloc; use alloc::string::String; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; @@ -13,6 +14,7 @@ use miden_agglayer::{ MetadataHash, agglayer_library, }; +use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_processor::fast::{ExecutionOutput, FastProcessor}; use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; @@ -206,3 +208,31 @@ pub async fn execute_program_with_default_host( let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); processor.execute(&program, &mut host).await } + +/// Execute a MASM script with the default host +pub async fn execute_masm_script(script_code: &str) -> Result { + let agglayer_lib = agglayer_library(); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib) + .unwrap() + .assemble_program(script_code) + .unwrap(); + + execute_program_with_default_host(program, None).await +} + +/// Helper to assert execution fails with a specific error message +pub async fn assert_execution_fails_with(script_code: &str, expected_error: &str) { + let result = execute_masm_script(script_code).await; + assert!(result.is_err(), "Expected execution to fail but it succeeded"); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains(expected_error), + "Expected error containing '{}', got: {}", + expected_error, + error_msg + ); +} From 3b4bd70e507e8e8f5fe4895465eb8174ac92c67a Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 18 Feb 2026 11:23:23 +0100 Subject: [PATCH 08/21] feat(AggLayer): process bridging-out data and load `LeafData` to memory (#2425) * chore: unify b2agg note storage layout with claim note * chore: clarify destinationAddress in CLAIM note * feat: mem_store_double_word_unaligned * feat: compute leaf value for bridging out * chore: update comment about origin network ID * fix: write_address_to_memory had incorrect address arithmetic The add.N values should all be add.1 (incrementing the running pointer by 1 each time), not add.1, add.2, add.3, add.4 which caused addresses to skip (P, P+1, P+3, P+6, P+10 instead of P, P+1, P+2, P+3, P+4). Also includes byte-order fixes (u256_le_to_abi, swap_u32_bytes for originNetwork and leafType) and test infrastructure for verifying the root against Solidity vectors. Status: the MMR root computation via call is verified correct (test_mmr_append_via_call passes). The write_address_to_memory fix changed the bridge_out root. Still investigating a remaining mismatch related to u256_le_to_abi + mem_store_double_word_unaligned interaction. Co-authored-by: marti * fix: handling of dest address and ID in B2AGG * feat: reverse_limbs_and_change_byte_endianness * chore: integrate byte and limb swapping to bridging * fix: swap origin net ID endianness; rearrange mem writes Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: pad mem with zeros after metadata --------- Co-authored-by: Cursor Agent Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../miden-agglayer/asm/bridge/bridge_out.masm | 262 +++++++++++++----- crates/miden-agglayer/asm/bridge/utils.masm | 28 ++ .../asm/note_scripts/B2AGG.masm | 31 ++- .../asm/note_scripts/CLAIM.masm | 2 +- 4 files changed, 235 insertions(+), 88 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index e78e2983e9..efb5a81f52 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -5,16 +5,200 @@ use miden::protocol::output_note use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 use miden::core::word -use miden::agglayer::local_exit_tree +use miden::agglayer::utils +use miden::agglayer::asset_conversion +use miden::agglayer::crypto_utils + + +# TYPE ALIASES +# ================================================================================================= + +type EthereumAddressFormat = struct @bigendian { a: felt, b: felt, c: felt, d: felt, e: felt } +type MemoryAddress = u32 # CONSTANTS # ================================================================================================= -const MMR_PTR=42 -const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") +const BURN_ASSET_MEM_PTR=24 +const LEAF_DATA_START_PTR=44 + +const LEAF_TYPE_OFFSET=0 +const ORIGIN_NETWORK_OFFSET=1 +const ORIGIN_TOKEN_ADDRESS_OFFSET=2 +const DESTINATION_NETWORK_OFFSET=7 +const DESTINATION_ADDRESS_OFFSET=8 +const AMOUNT_OFFSET=13 +const METADATA_HASH_OFFSET=21 +const PADDING_OFFSET=29 const PUBLIC_NOTE=1 const BURN_NOTE_NUM_STORAGE_ITEMS=0 -const BURN_ASSET_MEM_PTR=24 + +const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") + +const LEAF_TYPE_ASSET=0 +# TBD once we have an AggLayer-specific network ID +const ORIGIN_NETWORK_ID=64 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Bridges an asset out via the AggLayer +#! +#! This procedure handles the complete bridge-out operation, including: +#! - Converting asset data to u32 format +#! - Computing Keccak hash of the data +#! - Adding the hash to the MMR frontier +#! - Storing the updated MMR root in account storage +#! - Creating a BURN note with the bridged out asset +#! +#! Inputs: [ASSET, dest_network_id, dest_address(5)] +#! Outputs: [] +#! +#! Where: +#! - ASSET is the asset to be bridged out. +#! - dest_network_id is the u32 destination network/chain ID. +#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. +#! +#! Invocation: call +pub proc bridge_out + # TODO: make building bridge message a separate procedure + + mem_storew_be.BURN_ASSET_MEM_PTR + # => [ASSET, dest_network_id, dest_address(5)] + + # the amount is the last element of the ASSET + drop drop drop + # => [native_amount, dest_network_id, dest_address(5)] + + # TODO: look up asset faucet id in asset registry and return scaling factor + # for now we use a hardcoded scaling factor of 0 + push.0 swap + # => [native_amount, 0, dest_network_id, dest_address(5)] + + exec.asset_conversion::scale_native_amount_to_u256 + # TODO integrate into scale_native_amount_to_u256 + exec.asset_conversion::reverse_limbs_and_change_byte_endianness + # => [AMOUNT[8], dest_network_id, dest_address(5)] + + push.LEAF_DATA_START_PTR push.AMOUNT_OFFSET add + movdn.8 + # => [AMOUNT[8], amount_ptr, dest_network_id, dest_address(5)] + exec.utils::mem_store_double_word_unaligned + # => [dest_network_id, dest_address(5)] + + push.LEAF_DATA_START_PTR push.DESTINATION_NETWORK_OFFSET add + mem_store + # => [dest_address(5)] + + push.LEAF_DATA_START_PTR push.DESTINATION_ADDRESS_OFFSET add + exec.write_address_to_memory + + # TODO construct metadata hash + padw padw + # => [METADATA_HASH[8]] + push.LEAF_DATA_START_PTR push.METADATA_HASH_OFFSET add + movdn.8 + # => [METADATA_HASH[8], metadata_hash_ptr] + exec.utils::mem_store_double_word_unaligned + + # Explicitly zero the 3 padding felts after METADATA_HASH for + # crypto_utils::pack_leaf_data + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add + mem_store + + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.1 add + mem_store + + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.2 add + mem_store + + # TODO get the origin token address from the ASSET and map via faucet registry + push.0.0.0.0.0 + push.LEAF_DATA_START_PTR push.ORIGIN_TOKEN_ADDRESS_OFFSET add + # => [origin_token_address_ptr, origin_token_address(5)] + exec.write_address_to_memory + + # TODO origin network ID should come from the faucet registry + push.ORIGIN_NETWORK_ID + exec.utils::swap_u32_bytes + push.LEAF_DATA_START_PTR push.ORIGIN_NETWORK_OFFSET add + # => [origin_network_ptr, origin_network_id] + mem_store + + push.LEAF_TYPE_ASSET + exec.utils::swap_u32_bytes + push.LEAF_DATA_START_PTR push.LEAF_TYPE_OFFSET add + # => [leaf_type_ptr, leaf_type] + mem_store + + # all the leaf data is now written to memory, compute the leaf value and add to MMR frontier + push.LEAF_DATA_START_PTR + # => [leaf_data_start_ptr] + exec.add_leaf_bridge + # => [] + + # creating BURN output note for ASSET + mem_loadw_be.BURN_ASSET_MEM_PTR + # => [ASSET] + + exec.create_burn_note + # => [] +end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Add Leaf Bridge +#! +#! Inputs: [leaf_data_start_ptr] +#! Outputs: [] +#! Memory layout (starting at leaf_data_start_ptr): +#! [ +#! leafType[1], +#! originNetwork[1], +#! originTokenAddress[5], +#! destinationNetwork[1], +#! destinationAddress[5], +#! amount[8], +#! metadataHash[8], +#! padding[3], +#! ] +#! +#! Invocation: exec +proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress) + exec.crypto_utils::compute_leaf_value + # => [LEAF_VALUE[8]] + + # TODO: Add LEAF_VALUE to MMR frontier + dropw dropw +end + +#! Writes the destination address to memory. +#! +#! Inputs: [mem_ptr, address(5)] +#! Outputs: [] +#! +#! Invocation: exec +proc write_address_to_memory(mem_ptr: MemoryAddress, address: EthereumAddressFormat) + # => [mem_ptr, address(5)] + + dup movdn.6 mem_store movup.4 add.1 + # => [mem_ptr+1, address(4)] + + dup movdn.5 mem_store movup.3 add.1 + # => [mem_ptr+2, address(3)] + + dup movdn.4 mem_store movup.2 add.1 + # => [mem_ptr+3, address(2)] + + dup movdn.3 mem_store swap add.1 + # => [mem_ptr+4, address(1)] + + mem_store +end #! Computes the SERIAL_NUM of the outputted BURN note. #! @@ -87,73 +271,3 @@ proc create_burn_note exec.output_note::add_asset # => [] end - -#! Bridges an asset out via the AggLayer -#! -#! This procedure handles the complete bridge-out operation, including: -#! - Converting asset data to u32 format -#! - Computing Keccak hash of the data -#! - Adding the hash to the MMR frontier -#! - Storing the updated MMR root in account storage -#! - Creating a BURN note with the bridged out asset -#! -#! Inputs: [ASSET, dest_network, dest_address(5)] -#! Outputs: [] -#! -#! Where: -#! - ASSET is the asset to be bridged out. -#! - dest_network is the u32 destination network/chain ID. -#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. -#! -#! Invocation: call -pub proc bridge_out - mem_storew_be.BURN_ASSET_MEM_PTR - # => [ASSET, dest_network, dest_address(5)] - - # @dev TODO: Look up asset faucet id in asset registry - # -> return scaling factor - - # @dev TODO: Convert ASSET amount to EVM amount using scaling factor - # -> return amount from here: https://github.com/0xMiden/miden-base/pull/2141 - - # Converting SCALED_ASSET, dest_network, dest_address(5) to u32 representation - # in preparation for keccak256 hashing - - # keccak256 inputs: - # => [ASSET, dest_network, dest_address(5)] - # TODO we should convert Miden->Ethereum asset values, incl. amount conversion etc. - - # TODO: make building bridge message a separate procedure - # TODO: match Agglayer addLeafBridge logic - # TODO: convert Miden asset amount to Ethereum amount - # Store ASSET as u32 limbs in memory starting at address 0 - push.0 movdn.4 exec.word::store_word_u32s_le - # => [dest_network, dest_address(5)] - - # Store [dest_network, dest_address[0..3]] as u32 limbs in memory starting at address 8 - push.8 movdn.4 exec.word::store_word_u32s_le - # => [dest_address(2), 0, 0] - - # Store [dest_address[3..5], 0, 0] as u32 limbs in memory starting at address 16 - push.16 movdn.4 exec.word::store_word_u32s_le - # => [] - - # 1 u32 = 4 bytes - # 10 u32 values = 40 bytes - push.40 push.0 - # => [ptr, len_bytes] - - exec.keccak256::hash_bytes - # => [DIGEST_U32[8]] - - # adding DIGEST_U32 double word leaf to mmr frontier - exec.local_exit_tree::add_asset_message - # => [] - - # creating BURN output note for ASSET - mem_loadw_be.BURN_ASSET_MEM_PTR - # => [ASSET] - - exec.create_burn_note - # => [] -end diff --git a/crates/miden-agglayer/asm/bridge/utils.masm b/crates/miden-agglayer/asm/bridge/utils.masm index 85ee284019..6a17598b2c 100644 --- a/crates/miden-agglayer/asm/bridge/utils.masm +++ b/crates/miden-agglayer/asm/bridge/utils.masm @@ -53,6 +53,34 @@ pub proc mem_store_double_word( # => [WORD_1, WORD_2, ptr] end +#! Stores two words to the provided unaligned (not a multiple of 4) memory address. +#! +#! Inputs: [WORD_1, WORD_2, ptr] +#! Outputs: [] +pub proc mem_store_double_word_unaligned( + double_word_to_store: DoubleWord, + mem_ptr: MemoryAddress +) + # bring ptr to the top of the stack + dup.8 + # => [ptr, WORD_1, WORD_2, ptr] + + # store each element individually at consecutive addresses + mem_store dup.7 add.1 + mem_store dup.6 add.2 + mem_store dup.5 add.3 + mem_store + # => [WORD_2, ptr] + + dup.4 add.4 + mem_store dup.3 add.5 + mem_store dup.2 add.6 + mem_store dup.1 add.7 + mem_store + drop + # => [] +end + #! Loads two words from the provided global memory address. #! #! Inputs: [ptr] diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index 9523160e9c..31afc1d83c 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -11,6 +11,10 @@ use miden::standards::wallets::basic->basic_wallet const B2AGG_NOTE_NUM_STORAGE_ITEMS=6 +const STORAGE_START_PTR=8 +const STORAGE_END_PTR=STORAGE_START_PTR + 8 +const ASSET_PTR=0 + # ERRORS # ================================================================================================= const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS="B2AGG script requires exactly 1 note asset" @@ -30,14 +34,14 @@ const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account do #! Inputs: [] #! Outputs: [] #! -#! Note storage is assumed to be as follows: -#! - destination_network: u32 value representing the target chain ID -#! - destination_address: split into 5 u32 values representing a 20-byte Ethereum address: -#! - destination_address_0: bytes 0-3 -#! - destination_address_1: bytes 4-7 -#! - destination_address_2: bytes 8-11 -#! - destination_address_3: bytes 12-15 -#! - destination_address_4: bytes 16-19 +#! Note storage layout (6 felts total): +#! - destination_network [0] : 1 felt +#! - destination_address [1..5] : 5 felts +#! +#! Where: +#! - destination_network: Destination network identifier (uint32) +#! - destination_address: 20-byte Ethereum address as 5 u32 felts +#! #! Note attachment is constructed from a NetworkAccountTarget standard: #! - [0, exec_hint_tag, target_id_prefix, target_id_suffix] #! @@ -71,14 +75,14 @@ begin # => [pad(16)] # Store note storage -> mem[8..14] - push.8 exec.active_note::get_storage + push.STORAGE_START_PTR exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] push.B2AGG_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS drop # => [pad(16)] # Store note assets -> mem[0..4] - push.0 exec.active_note::get_assets + push.ASSET_PTR exec.active_note::get_assets # => [num_assets, ptr, pad(16)] # Must be exactly 1 asset @@ -86,11 +90,12 @@ begin # => [pad(16)] # load the 6 B2AGG felts from note storage as two words - mem_loadw_be.12 swapw.2 mem_loadw_be.8 swapw - # => [EMPTY_WORD, dest_network, dest_address(5), pad(6)] + push.STORAGE_START_PTR add.4 mem_loadw_le swapw mem_loadw_le.STORAGE_START_PTR + # => [dest_network, dest_address(5), pad(10)] # Load ASSET onto the stack - mem_loadw_be.0 + movupw.2 + mem_loadw_be.ASSET_PTR # => [ASSET, dest_network, dest_address(5), pad(6)] call.bridge_out::bridge_out diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index 1aa0a2a1ea..c682993550 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -180,7 +180,7 @@ end #! - originNetwork: Origin network identifier (uint32) #! - originTokenAddress: Origin token address (address as 5 u32 felts) #! - destinationNetwork: Destination network identifier (uint32) -#! - destinationAddress: Destination address (address as 5 u32 felts) +#! - destinationAddress: 20-byte Ethereum address decodable into a Miden AccountId (5 u32 felts) #! - amount: Amount of tokens (uint256 as 8 u32 felts) #! - metadata: ABI encoded metadata (fixed size) #! - padding (3 felts) From 245f7b0cb20353f728b04cbc2d06dc4eb3faba5e Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 18 Feb 2026 13:47:05 +0100 Subject: [PATCH 09/21] feat(AggLayer): Store `hash(GER)` in the bridge storage and implement `assert_valid_ger` (#2388) * chore: unit test for compute_ger * fix: use LE byte<>felt conversion * chore: inline bytes32_to_felts into callers * fixup! fix: use LE byte<>felt conversion * Update crates/miden-testing/tests/agglayer/update_ger.rs * chore: unify underlying repr, use type aliases * feat: store GER hash in a map * feat: check GER in storage * chore: rename to GER_KNOWN_FLAG * Apply suggestions from code review * chore: reduce inline comments * changelog * chore: use constants on Felt * fix: reverse felt order within word after hashing * chore: generate GER test vectors * chore: integrate GER vectors to compute_ger test * chore: add expected GER in bridge-in test * Revert "fix: reverse felt order within word after hashing" This reverts commit 61852a923311b2ebf789ce238c1ffb452d75a0fc. * fix: reverse felt order before Hasher::merge * Update crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol * chore: add comments re: rpo256:merge stack ordering --- CHANGELOG.md | 2 + .../miden-agglayer/asm/bridge/bridge_in.masm | 49 ++++-- .../asm/note_scripts/UPDATE_GER.masm | 4 +- .../test-vectors/exit_roots.json | 14 ++ .../solidity-compat/test/ExitRoots.t.sol | 61 ++++++++ crates/miden-agglayer/src/errors/agglayer.rs | 3 + crates/miden-agglayer/src/lib.rs | 10 +- .../miden-testing/tests/agglayer/bridge_in.rs | 18 ++- .../tests/agglayer/test_utils.rs | 8 +- .../tests/agglayer/update_ger.rs | 146 ++++++++++++++++-- 10 files changed, 274 insertions(+), 41 deletions(-) create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json create mode 100644 crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2bfb7026..0c905babe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Implemented verification of AggLayer deposits (claims) against GER ([#2295](https://github.com/0xMiden/miden-base/pull/2295), [#2288](https://github.com/0xMiden/miden-base/pull/2288)). - Added `SignedBlock` struct ([#2355](https://github.com/0xMiden/miden-base/pull/2235)). - Added `PackageKind` and `ProcedureExport` ([#2358](https://github.com/0xMiden/miden-base/pull/2358)). +- Changed GER storage to a map ([#2388](https://github.com/0xMiden/miden-base/pull/2388)). +- Implemented `assert_valid_ger` procedure for verifying GER against storage ([#2388](https://github.com/0xMiden/miden-base/pull/2388)). - [BREAKING] Added `get_asset` and `get_initial_asset` kernel procedures and removed `get_balance`, `get_initial_balance` and `has_non_fungible_asset` kernel procedures ([#2369](https://github.com/0xMiden/miden-base/pull/2369)). - Introduced `TokenMetadata` type to encapsulate fungible faucet metadata ([#2344](https://github.com/0xMiden/miden-base/issues/2344)). - Added `StandardNote::from_script_root()` and `StandardNote::name()` methods, and exposed `NoteType` `PUBLIC`/`PRIVATE` masks as public constants ([#2411](https://github.com/0xMiden/miden-base/pull/2411)). diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index af1cc28201..50ef55aae6 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -1,6 +1,7 @@ use miden::agglayer::crypto_utils use miden::agglayer::utils use miden::core::crypto::hashes::keccak256 +use miden::core::crypto::hashes::rpo256 use miden::core::mem use miden::protocol::active_account use miden::protocol::native_account @@ -9,6 +10,7 @@ use miden::protocol::native_account # ================================================================================================= const ERR_BRIDGE_NOT_MAINNET = "bridge not mainnet" +const ERR_GER_NOT_FOUND = "GER not found in storage" const ERR_LEADING_BITS_NON_ZERO = "leading bits of global index must be zero" const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet deposit" const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: provided SMT root does not match the computed root" @@ -23,34 +25,39 @@ const GLOBAL_INDEX_PTR = PROOF_DATA_PTR + 2 * 256 # 512 const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 # 520 const MAINNET_EXIT_ROOT_PTR = EXIT_ROOTS_PTR # it's the first exit root -const GER_UPPER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_upper") -const GER_LOWER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_lower") +const GER_STORAGE_SLOT=word("miden::agglayer::bridge::ger") +const GER_KNOWN_FLAG=1 # PUBLIC INTERFACE # ================================================================================================= #! 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. +#! #! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)] #! Outputs: [pad(16)] #! #! Invocation: call pub proc update_ger - push.GER_LOWER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_LOWER[4], GER_UPPER[4], pad(8)] + # compute hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) + # inputs: [B, A] => output: hash(A || B) + exec.rpo256::merge + # => [GER_HASH, pad(12)] - exec.native_account::set_item - # => [OLD_VALUE, GER_UPPER[4], pad(8)] + # 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)] - dropw - # => [GER_UPPER[4], pad(12)] + swapw + # => [GER_HASH, VALUE, pad(12)] - push.GER_UPPER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_UPPER[4], pad(12)] + push.GER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_HASH, VALUE, pad(12)] - exec.native_account::set_item + exec.native_account::set_map_item # => [OLD_VALUE, pad(12)] - dropw # => [pad(16)] end @@ -117,8 +124,22 @@ end #! #! Invocation: exec proc assert_valid_ger - # TODO verify that GER is in storage - dropw dropw + # compute hash(GER) + exec.rpo256::merge + # => [GER_HASH] + + push.GER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_HASH] + + exec.active_account::get_map_item + # => [VALUE] + + # 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_eqw.err=ERR_GER_NOT_FOUND + # => [] end #! Assert the global index is valid for a mainnet deposit. diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index 1ca3d1ab9d..87a29aaef9 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -55,10 +55,10 @@ begin # => [pad(16)] # Load GER_LOWER and GER_UPPER from note storage - mem_loadw_be.STORAGE_PTR_GER_UPPER + mem_loadw_le.STORAGE_PTR_GER_UPPER # => [GER_UPPER[4], pad(12)] swapw - mem_loadw_be.STORAGE_PTR_GER_LOWER + mem_loadw_le.STORAGE_PTR_GER_LOWER # => [GER_LOWER[4], GER_UPPER[4], pad(8)] call.bridge_in::update_ger diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json b/crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json new file mode 100644 index 0000000000..b04fbdf844 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json @@ -0,0 +1,14 @@ +{ + "global_exit_roots": [ + "0x207f0b7db488bbc423fc3d12db21b97e574453e12b49ca21205181af677d7b04", + "0x8e10e03b7db5ffe76edbea651052f8045289ece97947297de6279ce9f6730252" + ], + "mainnet_exit_roots": [ + "0x98c911b6dcface93fd0bb490d09390f2f7f9fcf36fc208cbb36528a229298326", + "0xbb71d991caf89fe64878259a61ae8d0b4310c176e66d90fd2370b02573e80c90" + ], + "rollup_exit_roots": [ + "0x6a2533a24cc2a3feecf5c09b6a270bbb24a5e2ce02c18c0e26cd54c3dddc2d70", + "0xd9b546933b59acd388dc0c6520cbf2d4dbb9bac66f74f167ba70f221d82a440c" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol b/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol new file mode 100644 index 0000000000..9224231a00 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; + +/** + * @title ExitRootsTestVectors + * @notice Test contract that generates global exit root test vectors from + * mainnet-rollup exit root pairs. + * + * Run with: forge test -vv --match-contract ExitRootsTestVectors + * + * The output can be compared against Rust implementations that compute + * the global exit root as keccak256(mainnetExitRoot || rollupExitRoot). + */ +contract ExitRootsTestVectors is Test { + + /** + * @notice Generates global exit root vectors from mainnet-rollup pairs + * and saves to JSON file. + * + * Output file: test-vectors/exit_roots.json + */ + function test_generateExitRootVectors() public { + // Input: pairs of (mainnetExitRoot, rollupExitRoot) from mainnet transactions + // Source transaction hashes from https://explorer.lumia.org/: + // TX 1: 0xe1a20811d757c48eba534f63041f58cd39eec762bfb6e4496dccf4e675fd5619 + // TX 2: 0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188 + bytes32[] memory mainnetExitRoots = new bytes32[](2); + bytes32[] memory rollupExitRoots = new bytes32[](2); + + // Pair 1 (TX: 0xe1a20811d757c48eba534f63041f58cd39eec762bfb6e4496dccf4e675fd5619) + mainnetExitRoots[0] = bytes32(0x98c911b6dcface93fd0bb490d09390f2f7f9fcf36fc208cbb36528a229298326); + rollupExitRoots[0] = bytes32(0x6a2533a24cc2a3feecf5c09b6a270bbb24a5e2ce02c18c0e26cd54c3dddc2d70); + + // Pair 2 (TX: 0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188) + mainnetExitRoots[1] = bytes32(0xbb71d991caf89fe64878259a61ae8d0b4310c176e66d90fd2370b02573e80c90); + rollupExitRoots[1] = bytes32(0xd9b546933b59acd388dc0c6520cbf2d4dbb9bac66f74f167ba70f221d82a440c); + + // Compute global exit roots + bytes32[] memory globalExitRoots = new bytes32[](mainnetExitRoots.length); + for (uint256 i = 0; i < mainnetExitRoots.length; i++) { + globalExitRoots[i] = GlobalExitRootLib.calculateGlobalExitRoot( + mainnetExitRoots[i], + rollupExitRoots[i] + ); + } + + // Serialize parallel arrays to JSON + string memory obj = "root"; + vm.serializeBytes32(obj, "mainnet_exit_roots", mainnetExitRoots); + vm.serializeBytes32(obj, "rollup_exit_roots", rollupExitRoots); + string memory json = vm.serializeBytes32(obj, "global_exit_roots", globalExitRoots); + + // Save to file + string memory outputPath = "test-vectors/exit_roots.json"; + vm.writeJson(json, outputPath); + console.log("Saved exit root vectors to:", outputPath); + } +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index e86d5336f4..997ef62715 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -25,6 +25,9 @@ pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str /// Error Message: "combined u64 doesn't fit in field" pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combined u64 doesn't fit in field"); +/// Error Message: "GER not found in storage" +pub const ERR_GER_NOT_FOUND: MasmError = MasmError::from_static_str("GER not found in storage"); + /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index f9d1ebfb68..414bc25261 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -267,15 +267,9 @@ pub fn create_agglayer_faucet_component( /// Creates a complete bridge account builder with the standard configuration. pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { - // Create the "bridge_in" component - let ger_upper_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_upper") + let ger_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger") .expect("Bridge storage slot name should be valid"); - let ger_lower_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_lower") - .expect("Bridge storage slot name should be valid"); - let bridge_in_storage_slots = vec![ - StorageSlot::with_value(ger_upper_storage_slot_name, Word::empty()), - StorageSlot::with_value(ger_lower_storage_slot_name, Word::empty()), - ]; + let bridge_in_storage_slots = vec![StorageSlot::with_empty_map(ger_storage_slot_name)]; let bridge_in_component = bridge_in_component(bridge_in_storage_slots); diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 4147119147..ffe578b1cb 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -3,6 +3,7 @@ extern crate alloc; use miden_agglayer::{ ClaimNoteStorage, OutputNoteData, + UpdateGerNote, create_claim_note, create_existing_agglayer_faucet, create_existing_bridge_account, @@ -56,7 +57,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // GET REAL CLAIM DATA FROM JSON // -------------------------------------------------------------------------------------------- - let (proof_data, leaf_data) = real_claim_data(); + let (proof_data, leaf_data, ger) = real_claim_data(); // Get the destination account ID from the leaf data // This requires the destination_address to be in the embedded Miden AccountId format @@ -95,9 +96,24 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // Add the claim note to the builder before building the mock chain builder.add_output_note(OutputNote::Full(claim_note.clone())); + // CREATE UPDATE_GER NOTE WITH GLOBAL EXIT ROOT + // -------------------------------------------------------------------------------------------- + let update_ger_note = + UpdateGerNote::create(ger, sender_account.id(), bridge_account.id(), builder.rng_mut())?; + builder.add_output_note(OutputNote::Full(update_ger_note.clone())); + // BUILD MOCK CHAIN WITH ALL ACCOUNTS // -------------------------------------------------------------------------------------------- let mut mock_chain = builder.clone().build()?; + + // EXECUTE UPDATE_GER NOTE TO STORE GER IN BRIDGE ACCOUNT + // -------------------------------------------------------------------------------------------- + let update_ger_tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? + .build()?; + let update_ger_executed = update_ger_tx_context.execute().await?; + + mock_chain.add_pending_executed_transaction(&update_ger_executed)?; mock_chain.prove_next_block()?; // EXECUTE CLAIM NOTE AGAINST AGGLAYER FAUCET (with FPI to Bridge) diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 9da02110d1..ca3493a5c2 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -9,6 +9,7 @@ use miden_agglayer::claim_note::{Keccak256Output, ProofData, SmtNode}; use miden_agglayer::{ EthAddressFormat, EthAmount, + ExitRoot, GlobalIndex, LeafData, MetadataHash, @@ -176,9 +177,12 @@ pub static CLAIM_ASSET_VECTOR: LazyLock = LazyLock::new(|| { /// Returns real claim data from the claim_asset_vectors.json file. /// /// Returns a tuple of (ProofData, LeafData) parsed from the real on-chain claim transaction. -pub fn real_claim_data() -> (ProofData, LeafData) { +pub fn real_claim_data() -> (ProofData, LeafData, ExitRoot) { let vector = &*CLAIM_ASSET_VECTOR; - (vector.proof.to_proof_data(), vector.leaf.to_leaf_data()) + let ger = ExitRoot::new( + hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), + ); + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) } /// Execute a program with default host and optional advice inputs diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 6014650545..3b66f15dcb 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -10,16 +10,43 @@ use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_core_lib::handlers::keccak256::KeccakPreimage; +use miden_crypto::hash::rpo::Rpo256 as Hasher; +use miden_crypto::{Felt, FieldElement}; +use miden_protocol::Word; use miden_protocol::account::StorageSlotName; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; -use miden_protocol::{Felt, Word}; +use miden_protocol::utils::sync::LazyLock; use miden_testing::{Auth, MockChain}; +use miden_tx::utils::hex_to_bytes; +use serde::Deserialize; use super::test_utils::execute_program_with_default_host; +// EXIT ROOT TEST VECTORS +// ================================================================================================ +// Test vectors generated from Solidity's GlobalExitRootLib.calculateGlobalExitRoot +// Run `forge test --match-contract ExitRootsTestVectors` to regenerate. + +/// Exit roots JSON embedded at compile time from the Foundry-generated file. +const EXIT_ROOTS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/exit_roots.json"); + +/// Deserialized exit root vectors from Solidity GlobalExitRootLib +#[derive(Debug, Deserialize)] +struct ExitRootsFile { + mainnet_exit_roots: Vec, + rollup_exit_roots: Vec, + global_exit_roots: Vec, +} + +/// Lazily parsed exit root vectors from the JSON file. +static EXIT_ROOTS_VECTORS: LazyLock = LazyLock::new(|| { + serde_json::from_str(EXIT_ROOTS_JSON).expect("Failed to parse exit roots JSON") +}); + #[tokio::test] -async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { +async fn update_ger_note_updates_storage() -> anyhow::Result<()> { let mut builder = MockChain::builder(); // CREATE BRIDGE ACCOUNT @@ -55,23 +82,114 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { .build()?; let executed_transaction = tx_context.execute().await?; - // VERIFY GER WAS UPDATED IN STORAGE + // VERIFY GER HASH WAS STORED IN MAP // -------------------------------------------------------------------------------------------- let mut updated_bridge_account = bridge_account.clone(); updated_bridge_account.apply_delta(executed_transaction.account_delta())?; - let ger_upper = updated_bridge_account - .storage() - .get_item(&StorageSlotName::new("miden::agglayer::bridge::ger_upper")?) - .unwrap(); - let ger_lower = updated_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(); + let mut ger_upper: [Felt; 4] = ger.to_elements()[4..8].try_into().unwrap(); + // Elements are reversed: rpo256::merge treats stack as if loaded BE from memory + // The following will produce matching hashes: + // Rust + // Hasher::merge(&[a, b, c, d], &[e, f, g, h]) + // MASM + // rpo256::merge(h, g, f, e, d, c, b, a) + ger_lower.reverse(); + ger_upper.reverse(); + + let ger_hash = Hasher::merge(&[ger_upper.into(), ger_lower.into()]); + // Look up the GER hash in the map storage + let ger_storage_slot = StorageSlotName::new("miden::agglayer::bridge::ger")?; + let stored_value = updated_bridge_account .storage() - .get_item(&StorageSlotName::new("miden::agglayer::bridge::ger_lower")?) - .unwrap(); - let expected_lower: Word = ger.to_elements()[0..4].try_into().unwrap(); - let expected_upper: Word = ger.to_elements()[4..8].try_into().unwrap(); - assert_eq!(ger_upper, expected_upper); - assert_eq!(ger_lower, expected_lower); + .get_map_item(&ger_storage_slot, ger_hash) + .expect("GER hash should be stored in the map"); + + // The stored value should be [GER_KNOWN_FLAG, 0, 0, 0] = [1, 0, 0, 0] + let expected_value: Word = [Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO].into(); + assert_eq!(stored_value, expected_value, "GER hash should map to [1, 0, 0, 0]"); + + Ok(()) +} + +/// Tests compute_ger with known mainnet and rollup exit roots. +/// +/// The GER (Global Exit Root) is computed as keccak256(mainnet_exit_root || rollup_exit_root). +#[tokio::test] +async fn compute_ger() -> anyhow::Result<()> { + let agglayer_lib = agglayer_library(); + let vectors = &*EXIT_ROOTS_VECTORS; + + for i in 0..vectors.mainnet_exit_roots.len() { + let mainnet_exit_root_bytes = + hex_to_bytes(vectors.mainnet_exit_roots[i].as_str()).expect("Invalid hex string"); + let rollup_exit_root_bytes = + hex_to_bytes(vectors.rollup_exit_roots[i].as_str()).expect("Invalid hex string"); + let expected_ger_bytes = + hex_to_bytes(vectors.global_exit_roots[i].as_str()).expect("Invalid hex string"); + + // Convert expected GER to felts for comparison + let expected_ger_exit_root = ExitRoot::from(expected_ger_bytes); + let expected_ger_felts = expected_ger_exit_root.to_elements(); + + // Computed GER using keccak256 + let ger_preimage: Vec = + [mainnet_exit_root_bytes.as_ref(), rollup_exit_root_bytes.as_ref()].concat(); + let ger_preimage = KeccakPreimage::new(ger_preimage); + let computed_ger_felts: Vec = ger_preimage.digest().as_ref().to_vec(); + + assert_eq!( + computed_ger_felts, expected_ger_felts, + "Computed GER mismatch for test vector {}", + i + ); + + // Convert exit roots to packed u32 felts for memory initialization + let mainnet_felts = ExitRoot::from(mainnet_exit_root_bytes).to_elements(); + let rollup_felts = ExitRoot::from(rollup_exit_root_bytes).to_elements(); + + // Build memory initialization: mainnet at ptr 0, rollup at ptr 8 + let mem_init: Vec = mainnet_felts + .iter() + .chain(rollup_felts.iter()) + .enumerate() + .map(|(idx, f)| format!("push.{} mem_store.{}", f.as_int(), idx)) + .collect(); + let mem_init_code = mem_init.join("\n"); + + let source = format!( + r#" + use miden::core::sys + use miden::agglayer::crypto_utils + + begin + # Initialize memory with exit roots + {mem_init_code} + + # Call compute_ger with pointer to exit roots + push.0 + exec.crypto_utils::compute_ger + exec.sys::truncate_stack + end + "# + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib.clone()) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, None).await?; + + let result_digest: Vec = exec_output.stack[0..8].to_vec(); + + assert_eq!(result_digest, expected_ger_felts, "GER mismatch for test vector {}", i); + } Ok(()) } From b1c483b81278bb69df1ac3fcdda2d639b7bef2b6 Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 18 Feb 2026 14:06:32 +0100 Subject: [PATCH 10/21] feat(AggLayer): store Local Exit Tree in `AggLayerBridge` contract and update it upon `B2AGG` consumption (#2424) * chore: unify b2agg note storage layout with claim note * chore: clarify destinationAddress in CLAIM note * feat: mem_store_double_word_unaligned * feat: compute leaf value for bridging out * chore: update comment about origin network ID * fix: write_address_to_memory had incorrect address arithmetic The add.N values should all be add.1 (incrementing the running pointer by 1 each time), not add.1, add.2, add.3, add.4 which caused addresses to skip (P, P+1, P+3, P+6, P+10 instead of P, P+1, P+2, P+3, P+4). Also includes byte-order fixes (u256_le_to_abi, swap_u32_bytes for originNetwork and leafType) and test infrastructure for verifying the root against Solidity vectors. Status: the MMR root computation via call is verified correct (test_mmr_append_via_call passes). The write_address_to_memory fix changed the bridge_out root. Still investigating a remaining mismatch related to u256_le_to_abi + mem_store_double_word_unaligned interaction. Co-authored-by: marti * fix: handling of dest address and ID in B2AGG * feat: reverse_limbs_and_change_byte_endianness * chore: integrate byte and limb swapping to bridging * fix: swap origin net ID endianness; rearrange mem writes Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: pad mem with zeros after metadata * feat: store LET frontier in double-word array * feat: frontier test generation from param"d leaves * feat: verify computed LER against expected root * write leaf"s amounts to JSON * fix: compare LET by elements, rev elements per word * chore: Replace `unwrap()` with `expect()` for StorageSlotName parsing (#2451) * Initial plan * chore: replace unwrap() with expect() for storage slot names Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> * Update crates/miden-testing/tests/agglayer/bridge_out.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: move (de)ser logic to test_utils * chore: organize test_utils better Signed-off-by: Marti * chore: serialize amounts as strings in mmr test vecs Signed-off-by: Marti * test bridge_out frontier persistence across two consumes Co-authored-by: marti Signed-off-by: Marti * fix bridge_out frontier reload num_leaves endianness Co-authored-by: marti Signed-off-by: Marti * test bridge_out with shared multi-note helper and 32-leaf case Co-authored-by: marti Signed-off-by: Marti * refactor bridge_out coverage to direct 32-leaf burn flow Co-authored-by: marti Signed-off-by: Marti * refactor MMR vectors to seeded per-leaf destinations Co-authored-by: marti Signed-off-by: Marti * chore: cleanup tests Signed-off-by: Marti --------- Signed-off-by: Marti Co-authored-by: Cursor Agent Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- .../miden-agglayer/asm/bridge/bridge_out.masm | 151 +++++++++- .../miden-agglayer/solidity-compat/README.md | 11 +- .../solidity-compat/foundry.lock | 6 + .../test-vectors/mmr_frontier_vectors.json | 232 +++++++++++---- .../solidity-compat/test/MMRTestVectors.t.sol | 73 ++++- crates/miden-agglayer/src/lib.rs | 21 +- .../tests/agglayer/bridge_out.rs | 280 +++++++++++------- .../tests/agglayer/mmr_frontier.rs | 36 +-- .../tests/agglayer/test_utils.rs | 134 ++++++--- 9 files changed, 683 insertions(+), 261 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index efb5a81f52..04f0a6e189 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -1,6 +1,9 @@ use miden::protocol::active_note +use miden::protocol::active_account +use miden::protocol::native_account use miden::protocol::note use miden::standards::note_tag +use miden::standards::data_structures::double_word_array use miden::protocol::output_note use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 @@ -8,6 +11,7 @@ use miden::core::word use miden::agglayer::utils use miden::agglayer::asset_conversion use miden::agglayer::crypto_utils +use miden::agglayer::mmr_frontier32_keccak # TYPE ALIASES @@ -21,6 +25,11 @@ type MemoryAddress = u32 const BURN_ASSET_MEM_PTR=24 const LEAF_DATA_START_PTR=44 +# Memory pointer for loading the LET (Local Exit Tree) frontier into memory. +# The memory layout at this address matches what append_and_update_frontier expects: +# [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +const LET_FRONTIER_MEM_PTR=100 + const LEAF_TYPE_OFFSET=0 const ORIGIN_NETWORK_OFFSET=1 const ORIGIN_TOKEN_ADDRESS_OFFSET=2 @@ -33,7 +42,13 @@ const PADDING_OFFSET=29 const PUBLIC_NOTE=1 const BURN_NOTE_NUM_STORAGE_ITEMS=0 +# Storage slot constants for the LET (Local Exit Tree). +# The frontier is stored as a double-word array in a map slot. +# The root and num_leaves are stored in separate value slots. const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") +const LET_ROOT_LO_SLOT=word("miden::agglayer::let::root_lo") +const LET_ROOT_HI_SLOT=word("miden::agglayer::let::root_hi") +const LET_NUM_LEAVES_SLOT=word("miden::agglayer::let::num_leaves") const LEAF_TYPE_ASSET=0 # TBD once we have an AggLayer-specific network ID @@ -170,10 +185,140 @@ end #! Invocation: exec proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress) exec.crypto_utils::compute_leaf_value - # => [LEAF_VALUE[8]] + # => [LEAF_VALUE_LO, LEAF_VALUE_HI] + + # Load the LET frontier from storage into memory at LET_FRONTIER_MEM_PTR + exec.load_let_frontier_to_memory + # => [LEAF_VALUE_LO, LEAF_VALUE_HI] + + # Push frontier pointer below the leaf value + push.LET_FRONTIER_MEM_PTR movdn.8 + # => [LEAF_VALUE_LO, LEAF_VALUE_HI, let_frontier_ptr] + + # Append the leaf to the frontier and compute the new root + exec.mmr_frontier32_keccak::append_and_update_frontier + # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] + + # Save the root and num_leaves to their value slots + exec.save_let_root_and_num_leaves + # => [] + + # Write the updated frontier from memory back to the map + exec.save_let_frontier_to_storage + # => [] +end + +#! Loads the LET (Local Exit Tree) frontier from account storage into memory. +#! +#! The num_leaves is read from its dedicated value slot, and the 32 frontier entries are read +#! from the LET map slot (double-word array, indices 0..31). The data is placed into memory at +#! LET_FRONTIER_MEM_PTR, matching the layout expected by append_and_update_frontier: +#! [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +#! +#! Empty (uninitialized) map entries return zeros, which is the correct initial state for the +#! frontier when there are no leaves. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +proc load_let_frontier_to_memory + # 1. Load num_leaves from its value slot + push.LET_NUM_LEAVES_SLOT[0..2] + exec.active_account::get_item + # => [num_leaves_word] + + push.LET_FRONTIER_MEM_PTR mem_storew_be dropw + # => [] + + # 2. Load 32 frontier double-word entries from the map via double_word_array::get + push.0 + # => [h=0] + + repeat.32 + # => [h] + + # Read frontier[h] as a double word from the map + dup push.LOCAL_EXIT_TREE_SLOT[0..2] + exec.double_word_array::get + # => [VALUE_0, VALUE_1, h] + + # Compute memory address and store the double word + dup.8 mul.8 add.LET_FRONTIER_MEM_PTR add.4 movdn.8 + # => [VALUE_0, VALUE_1, mem_addr, h] + exec.utils::mem_store_double_word + dropw dropw drop + # => [h] - # TODO: Add LEAF_VALUE to MMR frontier - dropw dropw + add.1 + # => [h+1] + end + + drop + # => [] +end + +#! Saves the Local Exit Root and num_leaves to their dedicated value slots. +#! +#! Inputs: [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] +#! Outputs: [] +#! +#! Invocation: exec +proc save_let_root_and_num_leaves + # 1. Save root lo word to its value slot + push.LET_ROOT_LO_SLOT[0..2] + exec.native_account::set_item + dropw + # => [NEW_ROOT_HI, new_leaf_count] + + # 2. Save root hi word to its value slot + push.LET_ROOT_HI_SLOT[0..2] + exec.native_account::set_item + dropw + # => [new_leaf_count] + + # 3. Save new_leaf_count to its value slot as [new_leaf_count, 0, 0, 0] + push.0.0.0 + # => [0, 0, 0, new_leaf_count] + push.LET_NUM_LEAVES_SLOT[0..2] + exec.native_account::set_item + dropw + # => [] +end + +#! Writes the 32 frontier entries from memory back to the LET map slot. +#! +#! Each frontier entry is a double word (Keccak256 digest) stored at +#! LET_FRONTIER_MEM_PTR + 4 + h * 8, and is written to the map at double_word_array index h. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +proc save_let_frontier_to_storage + push.0 + # => [h=0] + + repeat.32 + # => [h] + + # Load frontier[h] double word from memory + dup mul.8 add.LET_FRONTIER_MEM_PTR add.4 + exec.utils::mem_load_double_word + # => [VALUE_0, VALUE_1, h] + + # Write it back to the map at index h + dup.8 push.LOCAL_EXIT_TREE_SLOT[0..2] + exec.double_word_array::set + dropw dropw + # => [h] + + add.1 + # => [h+1] + end + + drop + # => [] end #! Writes the destination address to memory. diff --git a/crates/miden-agglayer/solidity-compat/README.md b/crates/miden-agglayer/solidity-compat/README.md index f93f83b5bc..b45b6edced 100644 --- a/crates/miden-agglayer/solidity-compat/README.md +++ b/crates/miden-agglayer/solidity-compat/README.md @@ -46,5 +46,12 @@ The canonical zeros should match the constants in: ### MMR Frontier Vectors -The `test_generateVectors` adds leaves `0, 1, 2, ...` (as left-padded 32-byte values) -and outputs the root after each addition. +The `test_generateVectors` adds 32 leaves and outputs the root after each addition. +Each leaf uses: + +- `amounts[i] = i + 1` +- `destination_networks[i]` and `destination_addresses[i]` generated deterministically from + a fixed seed in `MMRTestVectors.t.sol` + +This gives reproducible "random-looking" destination parameters while keeping vector generation +stable across machines and reruns. diff --git a/crates/miden-agglayer/solidity-compat/foundry.lock b/crates/miden-agglayer/solidity-compat/foundry.lock index 8aa165ad75..196c826d70 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.lock +++ b/crates/miden-agglayer/solidity-compat/foundry.lock @@ -7,5 +7,11 @@ "name": "v1.14.0", "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" } + }, + "lib/openzeppelin-contracts-upgradeable": { + "branch": { + "name": "release-v4.9", + "rev": "2d081f24cac1a867f6f73d512f2022e1fa987854" + } } } \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json index e51ea4e4e9..e2b1c37ab8 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json @@ -1,4 +1,38 @@ { + "amounts": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32 + ], "counts": [ 1, 2, @@ -33,72 +67,140 @@ 31, 32 ], + "destination_networks": [ + 1538671592, + 1271685039, + 2812858243, + 1717044446, + 1618236512, + 1846799397, + 1114625417, + 1980472020, + 3445581035, + 1216050355, + 1334555263, + 1595653741, + 1406956437, + 2339872987, + 1591634953, + 2036330440, + 948554316, + 1629580568, + 4209912969, + 3528172732, + 4197496357, + 2020389543, + 1365501531, + 2591126838, + 273689805, + 543018504, + 3291055054, + 2685286074, + 3030491074, + 4166649488, + 1541470110, + 1181416010 + ], + "destination_addresses": [ + "0xb48074703337bef6e94a9e2e1fffe71632b42d56", + "0xba60cd3cbd12619e6983b5d0e1cbcf2f4fed9d7b", + "0x89510362d6edeb958f059727c9ed0f99298aafa4", + "0xd62cf6356e0a48e2014b71cf942bebbbfb00f7d7", + "0xfa5eacb9668731d74f2bb5ad5bfb319f5a91c87d", + "0x90dd6647e5c91f9104a548876868a54795696b34", + "0x0e76f5f993a9a7f961e06397bc71d15c278a0b6c", + "0xe022226d1ffccf12ac0e84d0ab9430f3fd56c613", + "0x1f9ecff77e28bca8ef18434b842a30579bfd4eaa", + "0xe51d207b549db157bee9faebd51c35ab47d180ef", + "0x9f30d6d0335e91e0593f13a567e4fee661e1259f", + "0xe8f13da1bdb719ba364a890a623454040a932ecf", + "0xb6ee19bf265563aa76dbe202e8dc71f8f42a58b1", + "0xf62d45e4d0dc57259b4557b5d79ea23f67d0e381", + "0xaa94f5480ad0c906044e5e7da8bb6bc4395aa498", + "0x060ddd9f6e6cf285004e33c30b46710ad75918dd", + "0x8b743c166e1da1444781ad2b5fe2291578abceb1", + "0x8b08d9a773273df976fb7448d38feeeb15dc34f8", + "0xbe931f6f189e6f8da14f7b67eb2e67b5d7f71c1d", + "0x2f891c182b23d1422d8fddd9cc30b25bb849bd5f", + "0x93fd7ded75058aba1b76c35c4ac4e9355e596edc", + "0x25b9ebc8d7d48a6b0e71e82aa66832acc9419e3a", + "0xbb086ecac1316b81107e3ca591ef645831094e5a", + "0x08c7a5db749def9280108ec5e0354d4957cb17cf", + "0x0da76aa44116fad143f778f25907046e52f8c4d3", + "0xcfd0a3bfa35e771aad88c64ef0a310eff6730cda", + "0xa7439b51638f31f054c93ec869c8c7e982699bac", + "0x5c9a97f096cb18903994c44ddc07ffd921490b2c", + "0x0e52786af0b48d764a255f6506c9c297d5ba2dc3", + "0x5c2093921171f2c2d657eaa681d463fe36c965d1", + "0xf8de801f1ba2a676d96eb1f1ccb0b0cadfcbbe9e", + "0x31d230fabad05777bb3e1a062e781446bc422b80" + ], "leaves": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000007", - "0x0000000000000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000009", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b", - "0x000000000000000000000000000000000000000000000000000000000000000c", - "0x000000000000000000000000000000000000000000000000000000000000000d", - "0x000000000000000000000000000000000000000000000000000000000000000e", - "0x000000000000000000000000000000000000000000000000000000000000000f", - "0x0000000000000000000000000000000000000000000000000000000000000010", - "0x0000000000000000000000000000000000000000000000000000000000000011", - "0x0000000000000000000000000000000000000000000000000000000000000012", - "0x0000000000000000000000000000000000000000000000000000000000000013", - "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x0000000000000000000000000000000000000000000000000000000000000015", - "0x0000000000000000000000000000000000000000000000000000000000000016", - "0x0000000000000000000000000000000000000000000000000000000000000017", - "0x0000000000000000000000000000000000000000000000000000000000000018", - "0x0000000000000000000000000000000000000000000000000000000000000019", - "0x000000000000000000000000000000000000000000000000000000000000001a", - "0x000000000000000000000000000000000000000000000000000000000000001b", - "0x000000000000000000000000000000000000000000000000000000000000001c", - "0x000000000000000000000000000000000000000000000000000000000000001d", - "0x000000000000000000000000000000000000000000000000000000000000001e", - "0x000000000000000000000000000000000000000000000000000000000000001f" + "0xa2619da6f9de228ce3f0ad996c8cf37ad67d723718b591674a69e5661033ced4", + "0xf09c0dd27341e504f89c98d7d6ff7016919dc4f9721f58818cb19ce79914a95f", + "0xef2de724b0d0623903af391edcad11aa66738852994b876078a159525f6d6c4e", + "0xdec801cf38770ff0e8d39afea8ee2278ded6ba535394acdca83738b7c8969785", + "0xb4c6a4c31eb77b8e4d3be761340ba7e88429d9239c977aa5e5698cf03e543f5a", + "0x87835487f0f18c051967219757b01d5eb62c5d1a9f1afe4b5441d7bafc0a2074", + "0x7ff7d9645e0cad3fe9acd19c2dfc3da19536dff68e9e27db1f93d0f39cde1051", + "0x19728855d407f2b38c28bedca25c9d2be86c8eaf4eb8b70def028dfe837e005e", + "0xc99573c6e9f75b23a386fa5e5853540675edfcdaa5ad200f456fb9d76e2cdf74", + "0x21b7bf0de4e5d31ebb5e3023d1c6d344dc181669169f7b46009e3316abc6c769", + "0x2ea2c7805fb6bc45d2354c5df10243b4d41307ce3150527470ab3b175f2b087c", + "0xd8a4915d02943529bcd9ca6eea84a9035bc67b8e2f6e4a676cba7d00e72b05d9", + "0x3a430b2240dc5bd86c39b48f9f1a28e66230e02dabc4882a2706d08cadb1c644", + "0x403a632a93dd10731a3b1f95dcaf74380d7d8f87fa9804b5b1771f68d8b5e77e", + "0x0cfc60836a624120490cb0a0545b00b7e722174084aebf6e51e4a75cf59611d4", + "0xc5da75f1681a8a3c78d1aada32051090ec539d9b58b33a67e158d6103e20cfcc", + "0x16993f767beaf731359840fcbbea261456e2c13b5e6d90ebb4edfd8cb2a34c12", + "0x2b4388f707db1c673a241028232b08ff88fbae3bd7b3e4785951fb6ab8534781", + "0x0f94921debee580f6ce2d7372320accc1af2830eb05c9e2598c19f70dfa559a4", + "0x2dbb3f7d7aac347c130bd6f1ad396ba1a0bf9133128f1fa076b93810fe854efd", + "0xffd960a69fef871723a9dd196af598021ccbd63ff914ed9c68ce84d61bd6bc75", + "0x17e72d621cf42e7627815312f9581aacac54e1c44f41bfab6c52e29852545dd4", + "0x549a0ee3a3481975d1988252f823fc7260c620af0b489b7abc96a22e250c449c", + "0x1856904839c7f70794a93b4655050f8c2ebf462a39f0e7ed3edc2bd23e7f1f12", + "0xae819f3cf3b174421fc8e4dc1ee99f21ea14b34d5ad759dc9fa34394b293d476", + "0x3343b5bd07d4ff4108be1600ff4b6f5386cab4d388c60aec335b4a0634a82dd6", + "0x9c95427ecc2c9fc49fcccdc9e15bb847c9959994c57c5c919795e1ba74e8b10c", + "0xf7f19507ee3cf3292abff7d1e6e30aec7820be6d30ad552ef30be84dc6757da4", + "0x4f6f9a4370464bb10ce1bc38af0116d194deb28e1af95785816b764a5b7c2864", + "0xa52443afcd89dc39d1188b0413aa858dafaa1164772718eeed9569f99ae616da", + "0x08396f083f72d32ead958fa7fdb538a04d7492025f9ad7fa2b95a81d5e63184d", + "0x0f8b47e9beded26a7702189b2b180a5000c3362148befd44bb032633fb7aeeec" ], "roots": [ - "0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757", - "0x4a90a2c108a29b7755a0a915b9bb950233ce71f8a01859350d7b73cc56f57a62", - "0x2757cc260a62cc7c7708c387ea99f2a6bb5f034ed00da845734bec4d3fa3abfe", - "0xcb305ccda4331eb3fd9e17b81a5a0b336fb37a33f927698e9fb0604e534c6a01", - "0xa377a6262d3bae7be0ce09c2cc9f767b0f31848c268a4bdc12b63a451bb97281", - "0x440213f4dff167e3f5c655fbb6a3327af3512affed50ce3c1a3f139458a8a6d1", - "0xdd716d2905f2881005341ff1046ced5ee15cc63139716f56ed6be1d075c3f4a7", - "0xd6ebf96fcc3344fa755057b148162f95a93491bc6e8be756d06ec64df4df90fc", - "0x8b3bf2c95f3d0f941c109adfc3b652fadfeaf6f34be52524360a001cb151b5c9", - "0x74a5712654eccd015c44aca31817fd8bee8da400ada986a78384ef3594f2d459", - "0x95dd1209b92cce04311dfc8670b03428408c4ff62beb389e71847971f73702fa", - "0x0a83f3b2a75e19b7255b1de379ea9a71aef9716a3aef20a86abe625f088bbebf", - "0x601ba73b45858be76c8d02799fd70a5e1713e04031aa3be6746f95a17c343173", - "0x93d741c47aa73e36d3c7697758843d6af02b10ed38785f367d1602c8638adb0d", - "0x578f0d0a9b8ed5a4f86181b7e479da7ad72576ba7d3f36a1b72516aa0900c8ac", - "0x995c30e6b58c6e00e06faf4b5c94a21eb820b9db7ad30703f8e3370c2af10c11", - "0x49fb7257be1e954c377dc2557f5ca3f6fc7002d213f2772ab6899000e465236c", - "0x06fee72550896c50e28b894c60a3132bfe670e5c7a77ab4bb6a8ffb4abcf9446", - "0xbba3a807e79d33c6506cd5ecb5d50417360f8be58139f6dbe2f02c92e4d82491", - "0x1243fbd4d21287dbdaa542fa18a6a172b60d1af2c517b242914bdf8d82a98293", - "0x02b7b57e407fbccb506ed3199922d6d9bd0f703a1919d388c76867399ed44286", - "0xa15e7890d8f860a2ef391f9f58602dec7027c19e8f380980f140bbb92a3e00ba", - "0x2cb7eff4deb9bf6bbb906792bc152f1e63759b30e7829bfb5f3257ee600303f5", - "0xb1b034b4784411dc6858a0da771acef31be60216be0520a7950d29f66aee1fc5", - "0x3b17098f521ca0719e144a12bb79fdc51a3bc70385b5c2ee46b5762aae741f4f", - "0xd3e054489aa750d41938143011666a83e5e6b1477cce5ad612447059c2d8b939", - "0x6d15443ab2f39cce7fbe131843cdad6f27400eb179efb866569dd48baaf3ed4d", - "0xf9386ef40320c369185e48132f8fbf2f3e78d9598495dd342bcf4f41388d460d", - "0xb618ebe1f7675ef246a8cbb93519469076d5caacd4656330801537933e27b172", - "0x6c8c90b5aa967c98061a2dd09ea74dfb61fd9e86e308f14453e9e0ae991116de", - "0x06f51cfc733d71220d6e5b70a6b33a8d47a1ab55ac045fac75f26c762d7b29c9", - "0x82d1ddf8c6d986dee7fc6fa2d7120592d1dc5026b1bb349fcc9d5c73ac026f56" + "0x8cc758e5c6fa8345fd3b0a0181caa5dc6e77f423c580b8e87c03012584a73692", + "0x98b61870eb76d5e6edbc50e020d75bb1fcb4b7b401c7c4b2a1f89a80d1c09a79", + "0x880ca4f47ba1927e7fcdcbec8ab116b56b60da9cd004ca1d5face545643aa08a", + "0xb20bc203ee04e2a7090e16b13e87729056a68fb3ab59de412bee1f0e59bc0c9c", + "0xcfa71ba9de0f8f07725ec7d219498f6795a6244c2d8acf123fb16e5b2481dd20", + "0x48d2645722a03a1e88614ac5b824f11e5b86499121b0716cfcaee5e488c90c59", + "0x383a9831eecd5de9f16fd42cc9e37e3dc192c927250f3e9b0ba841f9ff47a10e", + "0x3108b99fcba1929e52ae6ac3efb50fbb369b3f28d9dfd3545fb5daa512c0094a", + "0xcaa8fcf88451b7003616d53bfad0aad284444f54e630fd5fa29f382d8d909a9b", + "0x45c0ab378292e4ec9fd445a17e596252f1d469d82cc5fae0dd2aea5272c7353a", + "0xbac28ff6568a8373cfa4fed0451fe1ff681e2ce723056b0ce9cf7f478800227c", + "0xfcb39eb0b04a6eaa4e9245774766ed4e573f00e44d295885ee1eccce2409ca0a", + "0x26bde93e570491ca04e1756a80327f351c25597653bf60297cd450b34f885490", + "0xafdf583feab737f2d6ac97e1b49269512f556ecd8498fa0b08d31be1f4537680", + "0xc7bdfe4f0e89a31dbfc7efbb2e405d258d0ddc349acaa5bc06901d2e2fbbb68f", + "0x251b6bab8a7f95560f0390a2c5833c5fb9492e91d98e90ad6d490ec0bd36a4ad", + "0xa72ae26e10e8dd0aaa536d2403c35fb509affcbe534cef0faa4186ad4bbfcf20", + "0xbcaa37a71be0cf0a4db6ee7dcfaf2323ba3795f6ec7c227e8f9f91d9675d2609", + "0xb29fedb04c912e66b81048f7c17519197e5e61b386b0ef26e43892cc285a1b6b", + "0xcd3e646ad3f7915eb598e639b503d884f6806127d16aa0c76f1cec85148f9de1", + "0xef2853e40b2e0f9f699651245d7773a2c4a9d935cc6bdf1b5e46b71f101ba888", + "0x227e25d9d9f79839aab39e181bd03c9989e4a388308a666a600c17f3fef4de8e", + "0x1d45946e28e961ae6e7bd971b4ae8e7607bc58ba5a99c31743bc34be94f4ad1b", + "0x61560eb989331073660926c8261458e2188c45f2ce02434f89fe3841a73a8c17", + "0x35935bf0c2325d8ca58b6446937abd310ea12d9e6641662c1f2ee05d250c4a8a", + "0x4bfb9be91a0f8bc7184dfefbc6d79b7b24e5ac59ad06a0f29e4992a2802f5e79", + "0x6c1f77384e835b1395286931fd4d5c12959bc9cc90d7050f5de05619991f02b4", + "0x56cda6d58d7dd69c3b48d81b038cda89bfe1cd1a5b8fa5dffdc8fdd0eec3ed77", + "0x1641df206eddddc851fde5b92ddaab17cd268f60a93f4c129828b59ef1e9d6c7", + "0x4a893765b186f2b595e1a4bec1926ac92d9b3e7b3273678edf5889efb1d780b0", + "0x2a3a74b27e9559e8e5d6c31c1a9cedd791bb0462305beac1b3204b55f943a5d9", + "0x23395138bd9267d5fa9ad3d3c55d2a324703664ea7a2c378f0e0463aae5ea98e" ] -} \ No newline at end of file +} diff --git a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol index 2e5b016232..84213c994d 100644 --- a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol @@ -2,20 +2,63 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "@agglayer/v2/lib/DepositContractBase.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; /** * @title MMRTestVectors * @notice Test contract that generates test vectors for verifying compatibility * between Solidity's DepositContractBase and Miden's MMR Frontier implementation. + * + * Leaves are constructed via getLeafValue using the same hardcoded fields that + * bridge_out.masm uses (leafType=0, originNetwork=64, originTokenAddress=0, + * metadataHash=0), parametrised by amount (i+1) and deterministic per-leaf + * destination network/address values derived from a fixed seed. * * Run with: forge test -vv --match-contract MMRTestVectors * * The output can be compared against the Rust KeccakMmrFrontier32 implementation * in crates/miden-testing/tests/agglayer/mmr_frontier.rs */ -contract MMRTestVectors is Test, DepositContractBase { - +contract MMRTestVectors is Test, DepositContractV2 { + + // Constants matching bridge_out.masm hardcoded values + uint8 constant LEAF_TYPE = 0; + uint32 constant ORIGIN_NETWORK = 64; + address constant ORIGIN_TOKEN_ADDR = address(0); + bytes32 constant METADATA_HASH = bytes32(0); + + // Fixed seed for deterministic "random" destination vectors. + // Keeping this constant ensures everyone regenerates the exact same JSON vectors. + uint256 constant VECTOR_SEED = uint256(keccak256("miden::agglayer::mmr_frontier_vectors::v2")); + + /** + * @notice Builds a leaf hash identical to what bridge_out.masm would produce for the + * given amount. + */ + function _createLeaf( + uint256 amount, + uint32 destinationNetwork, + address destinationAddress + ) internal pure returns (bytes32) { + return getLeafValue( + LEAF_TYPE, + ORIGIN_NETWORK, + ORIGIN_TOKEN_ADDR, + destinationNetwork, + destinationAddress, + amount, + METADATA_HASH + ); + } + + function _destinationNetworkAt(uint256 idx) internal pure returns (uint32) { + return uint32(uint256(keccak256(abi.encodePacked(VECTOR_SEED, bytes1(0x01), idx)))); + } + + function _destinationAddressAt(uint256 idx) internal pure returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(VECTOR_SEED, bytes1(0x02), idx))))); + } + /** * @notice Generates the canonical zeros and saves to JSON file. * ZERO_0 = 0x0...0 (32 zero bytes) @@ -43,28 +86,46 @@ contract MMRTestVectors is Test, DepositContractBase { /** * @notice Generates MMR frontier vectors (leaf-root pairs) and saves to JSON file. - * Uses parallel arrays instead of array of objects for cleaner serialization. + * Each leaf is created via _createLeaf(i+1, network[i], address[i]) so that: + * - amounts are 1..32 + * - destination networks/addresses are deterministic per index from VECTOR_SEED + * + * The destination vectors are also written to JSON so the Rust bridge_out test + * can construct matching B2AGG notes. + * * Output file: test-vectors/mmr_frontier_vectors.json */ function test_generateVectors() public { bytes32[] memory leaves = new bytes32[](32); bytes32[] memory roots = new bytes32[](32); uint256[] memory counts = new uint256[](32); + uint256[] memory amounts = new uint256[](32); + uint256[] memory destinationNetworks = new uint256[](32); + address[] memory destinationAddresses = new address[](32); for (uint256 i = 0; i < 32; i++) { - bytes32 leaf = bytes32(i); + uint256 amount = i + 1; + uint32 destinationNetwork = _destinationNetworkAt(i); + address destinationAddress = _destinationAddressAt(i); + bytes32 leaf = _createLeaf(amount, destinationNetwork, destinationAddress); _addLeaf(leaf); leaves[i] = leaf; roots[i] = getRoot(); counts[i] = depositCount; + amounts[i] = amount; + destinationNetworks[i] = destinationNetwork; + destinationAddresses[i] = destinationAddress; } // Serialize parallel arrays to JSON string memory obj = "root"; vm.serializeBytes32(obj, "leaves", leaves); vm.serializeBytes32(obj, "roots", roots); - string memory json = vm.serializeUint(obj, "counts", counts); + vm.serializeUint(obj, "counts", counts); + vm.serializeUint(obj, "amounts", amounts); + vm.serializeUint(obj, "destination_networks", destinationNetworks); + string memory json = vm.serializeAddress(obj, "destination_addresses", destinationAddresses); // Save to file string memory outputPath = "test-vectors/mmr_frontier_vectors.json"; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 414bc25261..af51d606ec 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -273,9 +273,24 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { let bridge_in_component = bridge_in_component(bridge_in_storage_slots); - // Create the "bridge_out" component - let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap(); - let bridge_out_storage_slots = vec![StorageSlot::with_empty_map(let_storage_slot_name)]; + // Create the "bridge_out" component. + // - The map slot stores the frontier as a double-word array (absent keys return zeros, which is + // the correct initial state for a frontier with no leaves). + // - The root and num_leaves each get their own value slots. + let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let") + .expect("LET storage slot name should be valid"); + let let_root_lo_slot_name = StorageSlotName::new("miden::agglayer::let::root_lo") + .expect("LET root_lo storage slot name should be valid"); + let let_root_hi_slot_name = StorageSlotName::new("miden::agglayer::let::root_hi") + .expect("LET root_hi storage slot name should be valid"); + let let_num_leaves_slot_name = StorageSlotName::new("miden::agglayer::let::num_leaves") + .expect("LET num_leaves storage slot name should be valid"); + let bridge_out_storage_slots = vec![ + StorageSlot::with_empty_map(let_storage_slot_name), + StorageSlot::with_value(let_root_lo_slot_name, Word::empty()), + StorageSlot::with_value(let_root_hi_slot_name, Word::empty()), + StorageSlot::with_value(let_num_leaves_slot_name, Word::empty()), + ]; let bridge_out_component = bridge_out_component(bridge_out_storage_slots); // Combine the components into a single account(builder) diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 23f1663631..c970a3d40c 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,18 +1,68 @@ extern crate alloc; use miden_agglayer::errors::ERR_B2AGG_TARGET_ACCOUNT_MISMATCH; -use miden_agglayer::{B2AggNote, EthAddressFormat, create_existing_bridge_account}; +use miden_agglayer::{B2AggNote, EthAddressFormat, ExitRoot, create_existing_bridge_account}; use miden_crypto::rand::FeltRng; use miden_protocol::Felt; -use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; +use miden_protocol::account::{ + Account, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, + StorageSlotName, +}; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::note::{NoteAssets, NoteTag, NoteType}; +use miden_protocol::note::NoteAssets; use miden_protocol::transaction::OutputNote; use miden_standards::account::faucets::TokenMetadata; -use miden_standards::note::StandardNote; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; +use miden_tx::utils::hex_to_bytes; -/// Tests the B2AGG (Bridge to AggLayer) note script with bridge_out account component. +use super::test_utils::SOLIDITY_MMR_FRONTIER_VECTORS; + +/// Reads the Local Exit Root (double-word) from the bridge account's storage. +/// +/// The Local Exit Root is stored in two dedicated value slots: +/// - `"miden::agglayer::let::root_lo"` — low word of the root +/// - `"miden::agglayer::let::root_hi"` — high word of the root +/// +/// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo` (in +/// reverse of their storage order), followed by the 4 elements of `root_hi` (also in +/// reverse of their storage order). For an empty/uninitialized tree, all elements are +/// zeros. +fn read_local_exit_root(account: &Account) -> Vec { + let root_lo_slot = + StorageSlotName::new("miden::agglayer::let::root_lo").expect("slot name should be valid"); + let root_hi_slot = + StorageSlotName::new("miden::agglayer::let::root_hi").expect("slot name should be valid"); + + let root_lo = account + .storage() + .get_item(&root_lo_slot) + .expect("should be able to read LET root lo"); + let root_hi = account + .storage() + .get_item(&root_hi_slot) + .expect("should be able to read LET root hi"); + + let mut root = Vec::with_capacity(8); + root.extend(root_lo.to_vec().into_iter().rev()); + root.extend(root_hi.to_vec().into_iter().rev()); + root +} + +fn read_let_num_leaves(account: &Account) -> u64 { + let num_leaves_slot = StorageSlotName::new("miden::agglayer::let::num_leaves") + .expect("slot name should be valid"); + let value = account + .storage() + .get_item(&num_leaves_slot) + .expect("should be able to read LET num leaves"); + value.to_vec()[0].as_int() +} + +/// Tests that 32 sequential B2AGG note consumptions match all 32 Solidity MMR roots. /// /// This test flow: /// 1. Creates a network faucet to provide assets @@ -21,10 +71,23 @@ use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; /// 4. Executes the B2AGG note consumption via network transaction /// 5. Consumes the BURN note #[tokio::test] -async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); +async fn bridge_out_consecutive() -> anyhow::Result<()> { + let vectors = &*SOLIDITY_MMR_FRONTIER_VECTORS; + let note_count = 32usize; + assert_eq!(vectors.amounts.len(), note_count, "amount vectors should contain 32 entries"); + assert_eq!(vectors.roots.len(), note_count, "root vectors should contain 32 entries"); + assert_eq!( + vectors.destination_networks.len(), + note_count, + "destination network vectors should contain 32 entries" + ); + assert_eq!( + vectors.destination_addresses.len(), + note_count, + "destination address vectors should contain 32 entries" + ); - // Create a network faucet owner account + let mut builder = MockChain::builder(); let faucet_owner_account_id = AccountId::dummy( [1; 15], AccountIdVersion::Version0, @@ -32,124 +95,113 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { AccountStorageMode::Private, ); - // Create a network faucet to provide assets for the B2AGG note - let faucet = - builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + // We burn all 32 produced burn notes at the end; initial supply must cover their total amount. + let faucet = builder.add_existing_network_faucet( + "AGG", + 10_000, + faucet_owner_account_id, + Some(10_000), + )?; - // Create a bridge account (includes a `bridge_out` component tested here) let mut bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); builder.add_account(bridge_account.clone())?; - // CREATE B2AGG NOTE WITH ASSETS - // -------------------------------------------------------------------------------------------- - - let amount = Felt::new(100); - let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - - // Create note storage with destination network and address - let destination_network = 1u32; // Example network ID - let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = - EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); - - let assets = NoteAssets::new(vec![bridge_asset])?; + let mut notes = Vec::with_capacity(note_count); + let mut expected_amounts = Vec::with_capacity(note_count); + for i in 0..note_count { + let amount: u64 = vectors.amounts[i].parse().expect("valid amount decimal string"); + expected_amounts.push(amount); + let destination_network = vectors.destination_networks[i]; + let eth_address = EthAddressFormat::from_hex(&vectors.destination_addresses[i]) + .expect("valid destination address"); + + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount).unwrap().into(); + let note = B2AggNote::create( + destination_network, + eth_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + faucet.id(), + builder.rng_mut(), + )?; + builder.add_output_note(OutputNote::Full(note.clone())); + notes.push(note); + } - // Create the B2AGG note using the helper - let b2agg_note = B2AggNote::create( - destination_network, - eth_address, - assets, - bridge_account.id(), - faucet.id(), - builder.rng_mut(), - )?; - - // Add the B2AGG note to the mock chain - builder.add_output_note(OutputNote::Full(b2agg_note.clone())); let mut mock_chain = builder.build()?; + let mut burn_note_ids = Vec::with_capacity(note_count); + + for (i, note) in notes.iter().enumerate() { + let executed_tx = mock_chain + .build_tx_context(bridge_account.id(), &[note.id()], &[])? + .build()? + .execute() + .await?; + + assert_eq!( + executed_tx.output_notes().num_notes(), + 1, + "Expected one BURN note after consume #{}", + i + 1 + ); + let burn_note = match executed_tx.output_notes().get_note(0) { + OutputNote::Full(note) => note, + _ => panic!("Expected OutputNote::Full variant for BURN note"), + }; + burn_note_ids.push(burn_note.id()); + + let expected_asset = Asset::from(FungibleAsset::new(faucet.id(), expected_amounts[i])?); + assert!( + burn_note.assets().iter().any(|asset| asset == &expected_asset), + "BURN note after consume #{} should contain the bridged asset", + i + 1 + ); + + bridge_account.apply_delta(executed_tx.account_delta())?; + assert_eq!( + read_let_num_leaves(&bridge_account), + (i + 1) as u64, + "LET leaf count should match consumed notes" + ); + + let expected_ler = + ExitRoot::new(hex_to_bytes(&vectors.roots[i]).expect("valid root hex")).to_elements(); + assert_eq!( + read_local_exit_root(&bridge_account), + expected_ler, + "Local Exit Root after {} leaves should match the Solidity-generated root", + i + 1 + ); + + mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.prove_next_block()?; + } - // EXECUTE B2AGG NOTE AGAINST BRIDGE ACCOUNT (NETWORK TRANSACTION) - // -------------------------------------------------------------------------------------------- - let tx_context = mock_chain - .build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])? - .build()?; - let executed_transaction = tx_context.execute().await?; - - // VERIFY PUBLIC BURN NOTE WAS CREATED - // -------------------------------------------------------------------------------------------- - // The bridge_out component should create a PUBLIC BURN note addressed to the faucet - assert_eq!( - executed_transaction.output_notes().num_notes(), - 1, - "Expected one BURN note to be created" - ); - - let output_note = executed_transaction.output_notes().get_note(0); - - // Extract the full note from the OutputNote enum - let burn_note = match output_note { - OutputNote::Full(note) => note, - _ => panic!("Expected OutputNote::Full variant for BURN note"), - }; - - // Verify the BURN note is public - assert_eq!(burn_note.metadata().note_type(), NoteType::Public, "BURN note should be public"); - - // Verify the BURN note contains the bridged asset - let expected_asset = FungibleAsset::new(faucet.id(), amount.into())?; - let expected_asset_obj = Asset::from(expected_asset); - assert!( - burn_note.assets().iter().any(|asset| asset == &expected_asset_obj), - "BURN note should contain the bridged asset" - ); - - assert_eq!( - burn_note.metadata().tag(), - NoteTag::with_account_target(faucet.id()), - "BURN note should have the correct tag" - ); - - // Verify the BURN note uses the correct script - assert_eq!( - burn_note.recipient().script().root(), - StandardNote::BURN.script_root(), - "BURN note should use the BURN script" - ); - - // Apply the delta to the bridge account - bridge_account.apply_delta(executed_transaction.account_delta())?; - - // Apply the transaction to the mock chain - mock_chain.add_pending_executed_transaction(&executed_transaction)?; - mock_chain.prove_next_block()?; - - // CONSUME THE BURN NOTE WITH THE NETWORK FAUCET - // -------------------------------------------------------------------------------------------- - // Check the initial token issuance before burning let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); - assert_eq!(initial_token_supply, Felt::new(100), "Initial issuance should be 100"); - - // Execute the BURN note against the network faucet - let burn_tx_context = - mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?; - let burn_executed_transaction = burn_tx_context.execute().await?; - - // Verify the burn transaction was successful - no output notes should be created - assert_eq!( - burn_executed_transaction.output_notes().num_notes(), - 0, - "Burn transaction should not create output notes" - ); + let total_burned: u64 = expected_amounts.iter().sum(); - // Apply the delta to the faucet account and verify the token issuance decreased let mut faucet = faucet; - faucet.apply_delta(burn_executed_transaction.account_delta())?; + for burn_note_id in burn_note_ids { + let burn_executed_tx = mock_chain + .build_tx_context(faucet.id(), &[burn_note_id], &[])? + .build()? + .execute() + .await?; + assert_eq!( + burn_executed_tx.output_notes().num_notes(), + 0, + "Burn transaction should not create output notes" + ); + faucet.apply_delta(burn_executed_tx.account_delta())?; + mock_chain.add_pending_executed_transaction(&burn_executed_tx)?; + mock_chain.prove_next_block()?; + } let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( final_token_supply, - Felt::new(initial_token_supply.as_int() - amount.as_int()), - "Token issuance should decrease by the burned amount" + Felt::new(initial_token_supply.as_int() - total_burned), + "Token supply should decrease by the sum of 32 bridged amounts" ); Ok(()) @@ -167,7 +219,7 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { /// 4. The same user account consumes the B2AGG note (triggering reclaim branch) /// 5. Verifies that assets are added back to the account and no BURN note is created #[tokio::test] -async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { +async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let mut builder = MockChain::builder(); // Create a network faucet owner account @@ -271,7 +323,7 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { /// 5. Attempts to consume the B2AGG note with the malicious account /// 6. Verifies that the transaction fails with ERR_B2AGG_TARGET_ACCOUNT_MISMATCH #[tokio::test] -async fn test_b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { +async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { let mut builder = MockChain::builder(); // Create a network faucet owner account diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs index a7ddb4dfa7..00bb195e76 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -7,8 +7,6 @@ use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; use miden_protocol::utils::sync::LazyLock; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; -use serde::Deserialize; - // KECCAK MMR FRONTIER // ================================================================================================ @@ -146,39 +144,7 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { // Test vectors generated from: https://github.com/agglayer/agglayer-contracts // Run `make generate-solidity-test-vectors` to regenerate the test vectors. -/// Canonical zeros JSON embedded at compile time from the Foundry-generated file. -const CANONICAL_ZEROS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json"); - -/// MMR frontier vectors JSON embedded at compile time from the Foundry-generated file. -const MMR_FRONTIER_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); - -/// Deserialized canonical zeros from Solidity DepositContractBase.sol -#[derive(Debug, Deserialize)] -struct CanonicalZerosFile { - canonical_zeros: Vec, -} - -/// Deserialized MMR frontier vectors from Solidity DepositContractBase.sol -/// Uses parallel arrays for leaves, roots, and counts instead of array of objects -#[derive(Debug, Deserialize)] -struct MmrFrontierVectorsFile { - leaves: Vec, - roots: Vec, - counts: Vec, -} - -/// Lazily parsed canonical zeros from the JSON file. -static SOLIDITY_CANONICAL_ZEROS: LazyLock = LazyLock::new(|| { - serde_json::from_str(CANONICAL_ZEROS_JSON).expect("Failed to parse canonical zeros JSON") -}); - -/// Lazily parsed MMR frontier vectors from the JSON file. -static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = LazyLock::new(|| { - serde_json::from_str(MMR_FRONTIER_VECTORS_JSON) - .expect("failed to parse MMR frontier vectors JSON") -}); +use super::test_utils::{SOLIDITY_CANONICAL_ZEROS, SOLIDITY_MMR_FRONTIER_VECTORS}; /// Verifies that the Rust KeccakMmrFrontier32 produces the same canonical zeros as Solidity. #[test] diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index ca3493a5c2..6a021047eb 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -24,6 +24,30 @@ use miden_protocol::utils::sync::LazyLock; use miden_tx::utils::hex_to_bytes; use serde::Deserialize; +// EMBEDDED TEST VECTOR JSON FILES +// ================================================================================================ + +/// Claim asset test vectors JSON — contains both LeafData and ProofData from a real claimAsset +/// transaction. +const CLAIM_ASSET_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json"); + +/// Leaf data test vectors JSON from the Foundry-generated file. +pub const LEAF_VALUE_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); + +/// Merkle proof verification vectors JSON from the Foundry-generated file. +pub const MERKLE_PROOF_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); + +/// Canonical zeros JSON from the Foundry-generated file. +pub const CANONICAL_ZEROS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json"); + +/// MMR frontier vectors JSON from the Foundry-generated file. +pub const MMR_FRONTIER_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); + // SERDE HELPERS // ================================================================================================ @@ -43,38 +67,26 @@ where } } -// TEST VECTOR STRUCTURES -// ================================================================================================ - -/// Claim asset test vectors JSON embedded at compile time - contains both LeafData and ProofData -/// from a real claimAsset transaction. -const CLAIM_ASSET_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json"); - -/// Leaf data test vectors JSON embedded at compile time from the Foundry-generated file. -pub const LEAF_VALUE_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); - -/// Merkle proof verification vectors JSON embedded at compile time from the Foundry-generated file. -pub const MERKLE_PROOF_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); - -/// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol -/// Uses parallel arrays for leaves and roots. For each element from leaves/roots there are 32 -/// elements from merkle_paths, which represent the merkle path for that leaf + root. -#[derive(Debug, Deserialize)] -pub struct MerkleProofVerificationFile { - pub leaves: Vec, - pub roots: Vec, - pub merkle_paths: Vec, +/// Deserializes a JSON array of values that may be either numbers or strings into `Vec`. +/// +/// Array-level counterpart of [`deserialize_uint_to_string`]. +fn deserialize_uint_vec_to_strings<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let values = Vec::::deserialize(deserializer)?; + values + .into_iter() + .map(|v| match v { + serde_json::Value::String(s) => Ok(s), + serde_json::Value::Number(n) => Ok(n.to_string()), + _ => Err(serde::de::Error::custom("expected a number or string for amount")), + }) + .collect() } -/// Lazily parsed Merkle proof vectors from the JSON file. -pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = - LazyLock::new(|| { - serde_json::from_str(MERKLE_PROOF_VECTORS_JSON) - .expect("failed to parse Merkle proof vectors JSON") - }); +// TEST VECTOR TYPES +// ================================================================================================ /// Deserialized leaf value test vector from Solidity-generated JSON. #[derive(Debug, Deserialize)] @@ -125,7 +137,6 @@ pub struct ProofValueVector { impl ProofValueVector { /// Converts this test vector into a `ProofData` instance. pub fn to_proof_data(&self) -> ProofData { - // Parse SMT proofs (32 nodes each) let smt_proof_local: [SmtNode; 32] = self .smt_proof_local_exit_root .iter() @@ -168,12 +179,70 @@ pub struct ClaimAssetVector { pub leaf: LeafValueVector, } +/// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol. +/// Uses parallel arrays for leaves and roots. For each element from leaves/roots there are 32 +/// elements from merkle_paths, which represent the merkle path for that leaf + root. +#[derive(Debug, Deserialize)] +pub struct MerkleProofVerificationFile { + pub leaves: Vec, + pub roots: Vec, + pub merkle_paths: Vec, +} + +/// Deserialized canonical zeros from Solidity DepositContractBase.sol. +#[derive(Debug, Deserialize)] +pub struct CanonicalZerosFile { + pub canonical_zeros: Vec, +} + +/// Deserialized MMR frontier vectors from Solidity DepositContractV2. +/// +/// Each leaf is produced by `getLeafValue` using the same hardcoded fields as `bridge_out.masm` +/// (leafType=0, originNetwork=64, originTokenAddress=0, metadataHash=0), parametrised by +/// `amounts[i]` and per-index `destination_networks[i]` / `destination_addresses[i]`. +/// +/// Amounts are serialized as uint256 values (JSON numbers). +#[derive(Debug, Deserialize)] +pub struct MmrFrontierVectorsFile { + pub leaves: Vec, + pub roots: Vec, + pub counts: Vec, + #[serde(deserialize_with = "deserialize_uint_vec_to_strings")] + pub amounts: Vec, + pub destination_networks: Vec, + pub destination_addresses: Vec, +} + +// LAZY-PARSED TEST VECTORS +// ================================================================================================ + /// Lazily parsed claim asset test vector from the JSON file. pub static CLAIM_ASSET_VECTOR: LazyLock = LazyLock::new(|| { serde_json::from_str(CLAIM_ASSET_VECTORS_JSON) .expect("failed to parse claim asset vectors JSON") }); +/// Lazily parsed Merkle proof vectors from the JSON file. +pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = + LazyLock::new(|| { + serde_json::from_str(MERKLE_PROOF_VECTORS_JSON) + .expect("failed to parse Merkle proof vectors JSON") + }); + +/// Lazily parsed canonical zeros from the JSON file. +pub static SOLIDITY_CANONICAL_ZEROS: LazyLock = LazyLock::new(|| { + serde_json::from_str(CANONICAL_ZEROS_JSON).expect("Failed to parse canonical zeros JSON") +}); + +/// Lazily parsed MMR frontier vectors from the JSON file. +pub static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = LazyLock::new(|| { + serde_json::from_str(MMR_FRONTIER_VECTORS_JSON) + .expect("failed to parse MMR frontier vectors JSON") +}); + +// HELPER FUNCTIONS +// ================================================================================================ + /// Returns real claim data from the claim_asset_vectors.json file. /// /// Returns a tuple of (ProofData, LeafData) parsed from the real on-chain claim transaction. @@ -185,7 +254,7 @@ pub fn real_claim_data() -> (ProofData, LeafData, ExitRoot) { (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) } -/// Execute a program with default host and optional advice inputs +/// Execute a program with a default host and optional advice inputs. pub async fn execute_program_with_default_host( program: Program, advice_inputs: Option, @@ -198,7 +267,6 @@ pub async fn execute_program_with_default_host( let std_lib = CoreLibrary::default(); host.load_library(std_lib.mast_forest()).unwrap(); - // Register handlers from std_lib for (event_name, handler) in std_lib.handlers() { host.register_handler(event_name, handler)?; } From be765b0352146231a64c4dfee1cbd9c9db026e55 Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 18 Feb 2026 21:00:43 +0100 Subject: [PATCH 11/21] feat(AggLayer): faucet registry (#2426) * chore: unify b2agg note storage layout with claim note * chore: clarify destinationAddress in CLAIM note * feat: mem_store_double_word_unaligned * feat: compute leaf value for bridging out * chore: update comment about origin network ID * fix: write_address_to_memory had incorrect address arithmetic The add.N values should all be add.1 (incrementing the running pointer by 1 each time), not add.1, add.2, add.3, add.4 which caused addresses to skip (P, P+1, P+3, P+6, P+10 instead of P, P+1, P+2, P+3, P+4). Also includes byte-order fixes (u256_le_to_abi, swap_u32_bytes for originNetwork and leafType) and test infrastructure for verifying the root against Solidity vectors. Status: the MMR root computation via call is verified correct (test_mmr_append_via_call passes). The write_address_to_memory fix changed the bridge_out root. Still investigating a remaining mismatch related to u256_le_to_abi + mem_store_double_word_unaligned interaction. Co-authored-by: marti * fix: handling of dest address and ID in B2AGG * feat: reverse_limbs_and_change_byte_endianness * chore: integrate byte and limb swapping to bridging * fix: swap origin net ID endianness; rearrange mem writes Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: pad mem with zeros after metadata * feat: store LET frontier in double-word array * feat: frontier test generation from param"d leaves * feat: verify computed LER against expected root * write leaf"s amounts to JSON * fix: compare LET by elements, rev elements per word * feat: add faucet registry * chore: CONFIG note docs * chore: simplify creation of existing agg faucet w/supply * lint * chore: TODO on bridge admin validation * chore: simplify stack operations Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> * chore: Replace `unwrap()` with `expect()` for StorageSlotName parsing (#2451) * Initial plan * chore: replace unwrap() with expect() for storage slot names Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> * Update crates/miden-testing/tests/agglayer/bridge_out.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: only truncate at the end * chore: move (de)ser logic to test_utils * chore: organize test_utils better Signed-off-by: Marti * chore: serialize amounts as strings in mmr test vecs Signed-off-by: Marti * test bridge_out frontier persistence across two consumes Co-authored-by: marti Signed-off-by: Marti * fix bridge_out frontier reload num_leaves endianness Co-authored-by: marti Signed-off-by: Marti * test bridge_out with shared multi-note helper and 32-leaf case Co-authored-by: marti Signed-off-by: Marti * refactor bridge_out coverage to direct 32-leaf burn flow Co-authored-by: marti Signed-off-by: Marti * refactor MMR vectors to seeded per-leaf destinations Co-authored-by: marti Signed-off-by: Marti * chore: cleanup tests Signed-off-by: Marti * fix: rustfmt line wrapping + clippy needless_range_loop Co-authored-by: marti * fix: read addr conversion slot in reverse * chore: use non-zero address * chore: rename loc const to distinguish * chore: use loc const in create_burn_note * chore: bring back doc and inline comments --------- Signed-off-by: Marti Co-authored-by: Cursor Agent Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + .../asm/bridge/agglayer_faucet.masm | 122 ++++++++++ .../asm/bridge/bridge_config.masm | 45 ++++ .../miden-agglayer/asm/bridge/bridge_out.masm | 199 ++++++++++----- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 69 ++++++ .../test-vectors/mmr_frontier_vectors.json | 199 +++++++-------- .../solidity-compat/test/MMRTestVectors.t.sol | 5 +- crates/miden-agglayer/src/config_note.rs | 115 +++++++++ crates/miden-agglayer/src/errors/agglayer.rs | 8 + crates/miden-agglayer/src/lib.rs | 148 +++++++++-- .../miden-testing/tests/agglayer/bridge_in.rs | 12 +- .../tests/agglayer/bridge_out.rs | 229 ++++++++++++++---- .../tests/agglayer/config_bridge.rs | 82 +++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + .../tests/agglayer/test_utils.rs | 6 +- 15 files changed, 1017 insertions(+), 224 deletions(-) create mode 100644 crates/miden-agglayer/asm/bridge/bridge_config.masm create mode 100644 crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm create mode 100644 crates/miden-agglayer/src/config_note.rs create mode 100644 crates/miden-testing/tests/agglayer/config_bridge.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c905babe9..ccb779a11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- 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)). - Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)). diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index f271cfc998..7b7bd43e93 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -1,4 +1,6 @@ use miden::agglayer::bridge_in +use miden::core::sys +use miden::agglayer::utils use miden::agglayer::asset_conversion use miden::agglayer::eth_address use miden::protocol::active_account @@ -16,6 +18,12 @@ use miden::core::word # The slot in this component's storage layout where the bridge account ID is stored. const BRIDGE_ID_SLOT = word("miden::agglayer::faucet") +# Storage slots for conversion metadata. +# Slot 1: [addr_felt0, addr_felt1, addr_felt2, addr_felt3] — first 4 felts of origin token address +const CONVERSION_INFO_1_SLOT = word("miden::agglayer::faucet::conversion_info_1") +# Slot 2: [addr_felt4, origin_network, scale, 0] — remaining address felt + origin network + scale +const CONVERSION_INFO_2_SLOT = word("miden::agglayer::faucet::conversion_info_2") + const PROOF_DATA_WORD_LEN = 134 const LEAF_DATA_WORD_LEN = 8 const OUTPUT_NOTE_DATA_WORD_LEN = 2 @@ -70,6 +78,120 @@ const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" +# CONVERSION METADATA HELPERS +# ================================================================================================= + +#! Returns the origin token address (5 felts) from faucet conversion storage. +#! +#! Reads conversion_info_1 (first 4 felts of address) and conversion_info_2 (5th felt) +#! from storage. +#! +#! Inputs: [] +#! Outputs: [addr0, addr1, addr2, addr3, addr4] +#! +#! Invocation: exec +pub proc get_origin_token_address + push.CONVERSION_INFO_1_SLOT[0..2] + exec.active_account::get_item + # => [addr3, addr2, addr1, addr0] + exec.word::reverse + # => [addr0, addr1, addr2, addr3] + + # Read slot 2: [0, scale, origin_network, addr4] + push.CONVERSION_INFO_2_SLOT[0..2] + exec.active_account::get_item + # => [0, scale, origin_network, addr4, addr0, addr1, addr2, addr3] + + # Keep only addr4 from slot 2 and append it after slot 1 limbs + drop drop drop + movdn.4 + # => [addr0, addr1, addr2, addr3, addr4] +end + +#! Returns the origin network identifier from faucet conversion storage. +#! +#! Inputs: [] +#! Outputs: [origin_network] +#! +#! Invocation: exec +pub proc get_origin_network + push.CONVERSION_INFO_2_SLOT[0..2] + exec.active_account::get_item + # => [0, scale, origin_network, addr4] + + drop drop swap drop + # => [origin_network] +end + +#! Returns the scale factor from faucet conversion storage. +#! +#! Inputs: [] +#! Outputs: [scale] +#! +#! Invocation: exec +pub proc get_scale + push.CONVERSION_INFO_2_SLOT[0..2] + exec.active_account::get_item + # => [0, scale, origin_network, addr4] + + drop movdn.2 drop drop + # => [scale] +end + +#! Converts a native Miden asset amount to origin asset data using the stored +#! conversion metadata (origin_token_address, origin_network, and scale). +#! +#! This procedure is intended to be called via FPI from the bridge account. +#! It reads the faucet's conversion metadata from storage, scales the native amount +#! to U256 format, and returns the result along with origin token address and network. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [AMOUNT_U256[0], AMOUNT_U256[1], addr0, addr1, addr2, addr3, addr4, origin_network, pad(2)] +#! +#! Where: +#! - amount: The native Miden asset amount +#! - AMOUNT_U256: The scaled amount as 8 u32 limbs (little-endian U256) +#! - addr0..addr4: Origin token address (5 felts, u32 limbs) +#! - origin_network: Origin network identifier +#! +#! Invocation: call +pub proc asset_to_origin_asset + # => [amount, pad(15)] + + # Step 1: Get scale from storage + exec.get_scale + # => [scale, amount, pad(15)] + swap + # => [amount, scale, pad(15)] + + # Step 2: Scale amount to U256 + exec.asset_conversion::scale_native_amount_to_u256 + exec.asset_conversion::reverse_limbs_and_change_byte_endianness + # => [U256_LO(4), U256_HI(4), pad(15)] + + # Step 3: Get origin token address + exec.get_origin_token_address + # => [addr0, addr1, addr2, addr3, addr4, U256_LO(4), U256_HI(4), pad(15)] + + # Move address below the U256 amount + repeat.5 movdn.12 end + # => [U256_LO(4), U256_HI(4), addr0, addr1, addr2, addr3, addr4, pad(15)] + + # Step 4: Get origin network + exec.get_origin_network + exec.utils::swap_u32_bytes + # => [origin_network, U256_LO(4), U256_HI(4), addr0..addr4, pad(15)] + + # Move origin_network after the address fields + movdn.13 + # => [U256_LO(4), U256_HI(4), addr0, addr1, addr2, addr3, addr4, origin_network, pad(15)] + + exec.sys::truncate_stack +end + +# CLAIM PROCEDURES +# ================================================================================================= + #! Inputs: [LEAF_DATA_KEY, PROOF_DATA_KEY] #! Outputs: [] #! diff --git a/crates/miden-agglayer/asm/bridge/bridge_config.masm b/crates/miden-agglayer/asm/bridge/bridge_config.masm new file mode 100644 index 0000000000..38cb530767 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/bridge_config.masm @@ -0,0 +1,45 @@ +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================= + +const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") +const IS_REGISTERED_FLAG=1 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Registers a faucet in the bridge's faucet registry. +#! +#! Writes `KEY -> [1, 0, 0, 0]` into the `faucet_registry` map, where +#! `KEY = [faucet_id_prefix, faucet_id_suffix, 0, 0]`. +#! +#! The sentinel value `[1, 0, 0, 0]` distinguishes registered faucets from +#! non-existent entries (SMTs return EMPTY_WORD for missing keys). +#! +#! TODO: Currently, no sender validation is performed — anyone can register a faucet. +#! Tracked in https://github.com/0xMiden/miden-base/issues/2450 +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix, pad(14)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +pub proc register_faucet + # => [faucet_id_prefix, faucet_id_suffix, pad(14)] + + # set_map_item expects [slot_id(2), KEY(4), VALUE(4)] and returns [OLD_VALUE(4)]. + push.IS_REGISTERED_FLAG + # => [IS_REGISTERED_FLAG, slot_id_prefix, slot_id_suffix, pad(14)] + + movdn.7 + # => [[slot_id_prefix, slot_id_suffix, 0, 0], [0, 0, 0, IS_REGISTERED_FLAG], pad(9)] + + # Place slot ID on top + push.FAUCET_REGISTRY_SLOT[0..2] + # Stack: [slot0, slot1, [prefix, suffix, 0, 0], [0, 0, 0, 1], pad(9)] + + exec.native_account::set_map_item + # => [OLD_VALUE(4), pad(9)] + + dropw +end diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index 04f0a6e189..74f3319fdc 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -2,6 +2,7 @@ use miden::protocol::active_note use miden::protocol::active_account use miden::protocol::native_account use miden::protocol::note +use miden::protocol::tx use miden::standards::note_tag use miden::standards::data_structures::double_word_array use miden::protocol::output_note @@ -9,7 +10,7 @@ use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 use miden::core::word use miden::agglayer::utils -use miden::agglayer::asset_conversion +use miden::agglayer::agglayer_faucet use miden::agglayer::crypto_utils use miden::agglayer::mmr_frontier32_keccak @@ -22,7 +23,20 @@ type MemoryAddress = u32 # CONSTANTS # ================================================================================================= -const BURN_ASSET_MEM_PTR=24 + +# bridge_out memory locals +const BRIDGE_OUT_BURN_ASSET_LOC=0 +const DESTINATION_ADDRESS_0_LOC=4 +const DESTINATION_ADDRESS_1_LOC=5 +const DESTINATION_ADDRESS_2_LOC=6 +const DESTINATION_ADDRESS_3_LOC=7 +const DESTINATION_ADDRESS_4_LOC=8 +const DESTINATION_NETWORK_LOC=9 + +# create_burn_note memory locals +const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 +const NETWORK_FAUCET_TAG_LOC=5 + const LEAF_DATA_START_PTR=44 # Memory pointer for loading the LET (Local Exit Tree) frontier into memory. @@ -49,64 +63,86 @@ const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") const LET_ROOT_LO_SLOT=word("miden::agglayer::let::root_lo") const LET_ROOT_HI_SLOT=word("miden::agglayer::let::root_hi") const LET_NUM_LEAVES_SLOT=word("miden::agglayer::let::num_leaves") +const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") const LEAF_TYPE_ASSET=0 -# TBD once we have an AggLayer-specific network ID -const ORIGIN_NETWORK_ID=64 + +# ERRORS +# ================================================================================================= + +const ERR_FAUCET_NOT_REGISTERED="faucet is not registered in the bridge's faucet registry" # PUBLIC INTERFACE # ================================================================================================= -#! Bridges an asset out via the AggLayer +#! Bridges an asset out via the AggLayer. #! -#! This procedure handles the complete bridge-out operation, including: -#! - Converting asset data to u32 format -#! - Computing Keccak hash of the data -#! - Adding the hash to the MMR frontier -#! - Storing the updated MMR root in account storage -#! - Creating a BURN note with the bridged out asset +#! This procedure handles the complete bridge-out operation: +#! 1. Validates the asset's faucet is registered in the bridge's faucet registry +#! 2. Queries the faucet for origin asset conversion data via FPI +#! 3. Builds the leaf data (origin token, destination, amount, metadata) +#! 4. Computes Keccak hash and adds it to the MMR frontier +#! 5. Creates a BURN note with the bridged out asset #! -#! Inputs: [ASSET, dest_network_id, dest_address(5)] +#! Inputs: [ASSET, dest_network_id, dest_address(5), pad(4)] #! Outputs: [] #! #! Where: -#! - ASSET is the asset to be bridged out. +#! - ASSET is the asset to be bridged out (layout: [faucet_id_prefix, faucet_id_suffix, 0, amount]). #! - dest_network_id is the u32 destination network/chain ID. #! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. #! #! Invocation: call +@locals(10) pub proc bridge_out - # TODO: make building bridge message a separate procedure - - mem_storew_be.BURN_ASSET_MEM_PTR - # => [ASSET, dest_network_id, dest_address(5)] - - # the amount is the last element of the ASSET - drop drop drop - # => [native_amount, dest_network_id, dest_address(5)] + # Save ASSET to memory for later BURN note creation + loc_storew_be.BRIDGE_OUT_BURN_ASSET_LOC dropw + loc_store.DESTINATION_NETWORK_LOC + loc_store.DESTINATION_ADDRESS_0_LOC + loc_store.DESTINATION_ADDRESS_1_LOC + loc_store.DESTINATION_ADDRESS_2_LOC + loc_store.DESTINATION_ADDRESS_3_LOC + loc_store.DESTINATION_ADDRESS_4_LOC + # => [] - # TODO: look up asset faucet id in asset registry and return scaling factor - # for now we use a hardcoded scaling factor of 0 - push.0 swap - # => [native_amount, 0, dest_network_id, dest_address(5)] + # --- 1. Validate faucet registration and convert asset via FPI --- + loc_loadw_be.BRIDGE_OUT_BURN_ASSET_LOC + exec.convert_asset + # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network, dest_network_id, dest_address(5)] - exec.asset_conversion::scale_native_amount_to_u256 - # TODO integrate into scale_native_amount_to_u256 - exec.asset_conversion::reverse_limbs_and_change_byte_endianness - # => [AMOUNT[8], dest_network_id, dest_address(5)] + # --- 2. Write all leaf data fields to memory --- + # Store scaled AMOUNT (8 felts) push.LEAF_DATA_START_PTR push.AMOUNT_OFFSET add movdn.8 - # => [AMOUNT[8], amount_ptr, dest_network_id, dest_address(5)] exec.utils::mem_store_double_word_unaligned + # => [origin_addr(5), origin_network, dest_network_id, dest_address(5)] + + # Store origin_token_address (5 felts) + push.LEAF_DATA_START_PTR push.ORIGIN_TOKEN_ADDRESS_OFFSET add + exec.write_address_to_memory + # => [origin_network, dest_network_id, dest_address(5)] + + # Store origin_network + push.LEAF_DATA_START_PTR push.ORIGIN_NETWORK_OFFSET add + mem_store # => [dest_network_id, dest_address(5)] + # Store destination_network + loc_load.DESTINATION_NETWORK_LOC push.LEAF_DATA_START_PTR push.DESTINATION_NETWORK_OFFSET add mem_store # => [dest_address(5)] + # Store destination_address + loc_load.DESTINATION_ADDRESS_4_LOC + loc_load.DESTINATION_ADDRESS_3_LOC + loc_load.DESTINATION_ADDRESS_2_LOC + loc_load.DESTINATION_ADDRESS_1_LOC + loc_load.DESTINATION_ADDRESS_0_LOC push.LEAF_DATA_START_PTR push.DESTINATION_ADDRESS_OFFSET add exec.write_address_to_memory + # => [] # TODO construct metadata hash padw padw @@ -130,46 +166,90 @@ pub proc bridge_out push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.2 add mem_store - # TODO get the origin token address from the ASSET and map via faucet registry - push.0.0.0.0.0 - push.LEAF_DATA_START_PTR push.ORIGIN_TOKEN_ADDRESS_OFFSET add - # => [origin_token_address_ptr, origin_token_address(5)] - exec.write_address_to_memory - - # TODO origin network ID should come from the faucet registry - push.ORIGIN_NETWORK_ID - exec.utils::swap_u32_bytes - push.LEAF_DATA_START_PTR push.ORIGIN_NETWORK_OFFSET add - # => [origin_network_ptr, origin_network_id] - mem_store - + # Leaf type push.LEAF_TYPE_ASSET exec.utils::swap_u32_bytes push.LEAF_DATA_START_PTR push.LEAF_TYPE_OFFSET add # => [leaf_type_ptr, leaf_type] mem_store - # all the leaf data is now written to memory, compute the leaf value and add to MMR frontier + # --- 3. Compute leaf value and add to MMR frontier --- push.LEAF_DATA_START_PTR - # => [leaf_data_start_ptr] exec.add_leaf_bridge - # => [] - - # creating BURN output note for ASSET - mem_loadw_be.BURN_ASSET_MEM_PTR - # => [ASSET] + # --- 4. Create BURN output note for ASSET --- + loc_loadw_be.BRIDGE_OUT_BURN_ASSET_LOC exec.create_burn_note - # => [] end # HELPER PROCEDURES # ================================================================================================= -#! Add Leaf Bridge +#! Validates that a faucet is registered in the bridge's faucet registry, then performs +#! an FPI call to the faucet's `asset_to_origin_asset` procedure to obtain the scaled +#! amount, origin token address, and origin network. +#! +#! Inputs: [ASSET] +#! Outputs: [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network] +#! +#! Where: +#! - ASSET layout: [faucet_id_prefix, faucet_id_suffix, 0, amount] +#! - AMOUNT_U256: scaled amount as 8 u32 limbs (little-endian) +#! - origin_addr: origin token address (5 u32 felts) +#! - origin_network: origin network identifier +#! +#! Panics if: +#! - The faucet is not registered in the faucet registry. +#! - The FPI call to asset_to_origin_asset fails. +#! +#! Invocation: exec +proc convert_asset + # => [faucet_id_prefix, faucet_id_suffix, 0, amount] + + # --- Step 1: Assert faucet is registered --- + + # Build KEY = [faucet_id_prefix, faucet_id_suffix, 0, 0] for the map lookup. + # Duplicate faucet ID onto top of stack, then add zero padding. + dup.1 dup.1 + push.0.0 + movup.3 movup.3 + # => [faucet_id_prefix, faucet_id_suffix, 0, 0, faucet_id_prefix, faucet_id_suffix, 0, amount] + + push.FAUCET_REGISTRY_SLOT[0..2] + exec.active_account::get_map_item + # => [VALUE(4), faucet_id_prefix, faucet_id_suffix, 0, amount] + + # Check flag, the stored word must be [0, 0, 0, 1] for registered faucets + drop drop drop + assert.err=ERR_FAUCET_NOT_REGISTERED + # => [faucet_id_prefix, faucet_id_suffix, 0, amount] + + # --- Step 2: FPI to faucet's asset_to_origin_asset --- + + # Drop the zero padding between faucet_id and amount. + movup.2 drop + # => [faucet_id_prefix, faucet_id_suffix, amount] + + procref.agglayer_faucet::asset_to_origin_asset + # => [PROC_MAST_ROOT(4), faucet_id_prefix, faucet_id_suffix, amount] + + # Move faucet_id above PROC_MAST_ROOT + movup.5 movup.5 + # => [faucet_id_prefix, faucet_id_suffix, PROC_MAST_ROOT(4), amount] + + exec.tx::execute_foreign_procedure + # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network, pad(2)] + + # drop the 2 trailing padding elements + movup.15 drop movup.14 drop + # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network] +end + +#! Computes the leaf value from the leaf data in memory and adds it to the MMR frontier. #! #! Inputs: [leaf_data_start_ptr] #! Outputs: [] +#! #! Memory layout (starting at leaf_data_start_ptr): #! [ #! leafType[1], @@ -321,15 +401,13 @@ proc save_let_frontier_to_storage # => [] end -#! Writes the destination address to memory. +#! Writes an Ethereum address (5 u32 felts) to consecutive memory locations. #! #! Inputs: [mem_ptr, address(5)] #! Outputs: [] #! #! Invocation: exec proc write_address_to_memory(mem_ptr: MemoryAddress, address: EthereumAddressFormat) - # => [mem_ptr, address(5)] - dup movdn.6 mem_store movup.4 add.1 # => [mem_ptr+1, address(4)] @@ -379,7 +457,7 @@ end #! Invocation: exec @locals(8) proc create_burn_note - loc_storew_be.0 dupw + loc_storew_be.CREATE_BURN_NOTE_BURN_ASSET_LOC dupw # => [ASSET, ASSET] movup.2 drop movup.2 drop @@ -388,7 +466,7 @@ proc create_burn_note exec.note_tag::create_account_target # => [network_faucet_tag, ASSET] - loc_store.5 + loc_store.NETWORK_FAUCET_TAG_LOC # => [ASSET] exec.compute_burn_note_serial_num @@ -397,22 +475,21 @@ proc create_burn_note procref.::miden::standards::notes::burn::main swapw # => [SERIAL_NUM, SCRIPT_ROOT] + # BURN note has no storage items, so we can set the pointer to 0 push.BURN_NOTE_NUM_STORAGE_ITEMS push.0 # => [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - exec.note::build_recipient # => [RECIPIENT] push.PUBLIC_NOTE - loc_load.5 + loc_load.NETWORK_FAUCET_TAG_LOC # => [tag, note_type, RECIPIENT] call.output_note::create # => [note_idx] - movdn.4 loc_loadw_be.0 + movdn.4 loc_loadw_be.CREATE_BURN_NOTE_BURN_ASSET_LOC # => [ASSET, note_idx] exec.output_note::add_asset - # => [] end diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm new file mode 100644 index 0000000000..99dcee284f --- /dev/null +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -0,0 +1,69 @@ +use miden::agglayer::bridge_config +use miden::protocol::active_note +use miden::protocol::active_account +use miden::protocol::account_id +use miden::standards::attachments::network_account_target + +# CONSTANTS +# ================================================================================================= + +const STORAGE_START_PTR = 0 +const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 2 + +# ERRORS +# ================================================================================================= + +const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS = "CONFIG_AGG_BRIDGE expects exactly 2 note storage items" +const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" + +#! Registers a faucet in the bridge's faucet registry. +#! +#! This note can only be consumed by the Agglayer Bridge account that is targeted by the note +#! attachment. Upon consumption, it registers the faucet ID from note storage in the bridge's +#! faucet registry. + +#! Note: Currently, there are no sender validation checks, so anyone can register a faucet. +#! +#! Requires that the account exposes: +#! - agglayer::bridge_config::register_faucet procedure. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! NoteStorage layout (2 felts total): +#! - faucet_id_prefix [0]: 1 felt +#! - faucet_id_suffix [1]: 1 felt +#! +#! Where: +#! - faucet_id_prefix: Prefix felt of the faucet account ID to register. +#! - faucet_id_suffix: Suffix felt of the faucet account ID to register. +#! +#! Panics if: +#! - The note attachment target account does not match the consuming bridge account. +#! - The note does not contain exactly 2 storage items. +#! - The account does not expose the register_faucet procedure. +#! +begin + dropw + # => [pad(16)] + + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + + # Load note storage to memory + push.STORAGE_START_PTR exec.active_note::get_storage + # => [num_storage_items, dest_ptr, pad(16)] + + push.CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS assert_eq.err=ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS drop + # => [pad(16)] + + # Load the faucet ID from memory, replacing the top 4 zeros + mem_loadw_le.STORAGE_START_PTR + # => [faucet_id_prefix, faucet_id_suffix, pad(14)] + + # Register the faucet in the bridge's faucet registry + call.bridge_config::register_faucet + # => [pad(16)] +end diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json index e2b1c37ab8..79c76364dc 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json @@ -67,6 +67,40 @@ 31, 32 ], + "destination_addresses": [ + "0xB48074703337bEf6e94A9e2E1FfFe71632B42D56", + "0xBA60cd3cBD12619e6983B5D0E1CbcF2f4fed9d7b", + "0x89510362d6EdeB958F059727C9eD0F99298aAFa4", + "0xD62Cf6356E0a48e2014b71Cf942BEbBbFb00F7d7", + "0xFA5eacb9668731D74F2BB5Ad5bfB319f5A91c87D", + "0x90DD6647e5c91f9104a548876868a54795696B34", + "0x0E76F5f993A9a7f961e06397BC71d15c278A0b6c", + "0xe022226D1fFcCf12ac0e84D0aB9430F3fd56C613", + "0x1F9ecff77E28Bca8Ef18434B842A30579Bfd4EaA", + "0xe51D207B549Db157BeE9faeBd51C35aB47d180EF", + "0x9f30d6d0335E91e0593f13a567E4Fee661e1259F", + "0xE8F13Da1BDb719ba364a890a623454040A932eCf", + "0xb6EE19bf265563aA76dbe202e8dC71F8f42a58B1", + "0xf62d45e4D0DC57259B4557b5d79Ea23F67D0E381", + "0xaa94f5480aD0C906044E5E7Da8BB6BC4395aA498", + "0x060ddd9f6e6CF285004e33C30b46710ad75918Dd", + "0x8B743c166e1dA1444781AD2b5Fe2291578ABCeb1", + "0x8B08d9A773273Df976fb7448D38FeEeB15Dc34F8", + "0xbe931f6F189e6F8Da14f7B67Eb2E67b5D7f71c1d", + "0x2F891C182b23d1422D8Fddd9CC30B25BB849Bd5F", + "0x93fD7DEd75058ABA1B76C35c4Ac4e9355e596EdC", + "0x25B9eBC8D7d48a6B0e71e82Aa66832aCC9419E3A", + "0xbb086ECaC1316B81107e3CA591ef645831094E5a", + "0x08c7a5Db749DEf9280108Ec5e0354d4957CB17cF", + "0x0da76aA44116fad143F778f25907046E52F8c4d3", + "0xcFd0a3bfA35E771aad88C64EF0A310efF6730cDa", + "0xa7439b51638F31f054C93EC869C8c7E982699BAC", + "0x5C9A97f096CB18903994C44ddC07FfD921490B2c", + "0x0e52786aF0b48D764a255f6506C9C297d5BA2Dc3", + "0x5C2093921171F2c2d657eAA681D463Fe36c965d1", + "0xf8de801F1ba2a676d96Eb1F1ccB0B0CADFCbbE9e", + "0x31D230FAbAd05777Bb3E1a062e781446Bc422b80" + ], "destination_networks": [ 1538671592, 1271685039, @@ -101,106 +135,73 @@ 1541470110, 1181416010 ], - "destination_addresses": [ - "0xb48074703337bef6e94a9e2e1fffe71632b42d56", - "0xba60cd3cbd12619e6983b5d0e1cbcf2f4fed9d7b", - "0x89510362d6edeb958f059727c9ed0f99298aafa4", - "0xd62cf6356e0a48e2014b71cf942bebbbfb00f7d7", - "0xfa5eacb9668731d74f2bb5ad5bfb319f5a91c87d", - "0x90dd6647e5c91f9104a548876868a54795696b34", - "0x0e76f5f993a9a7f961e06397bc71d15c278a0b6c", - "0xe022226d1ffccf12ac0e84d0ab9430f3fd56c613", - "0x1f9ecff77e28bca8ef18434b842a30579bfd4eaa", - "0xe51d207b549db157bee9faebd51c35ab47d180ef", - "0x9f30d6d0335e91e0593f13a567e4fee661e1259f", - "0xe8f13da1bdb719ba364a890a623454040a932ecf", - "0xb6ee19bf265563aa76dbe202e8dc71f8f42a58b1", - "0xf62d45e4d0dc57259b4557b5d79ea23f67d0e381", - "0xaa94f5480ad0c906044e5e7da8bb6bc4395aa498", - "0x060ddd9f6e6cf285004e33c30b46710ad75918dd", - "0x8b743c166e1da1444781ad2b5fe2291578abceb1", - "0x8b08d9a773273df976fb7448d38feeeb15dc34f8", - "0xbe931f6f189e6f8da14f7b67eb2e67b5d7f71c1d", - "0x2f891c182b23d1422d8fddd9cc30b25bb849bd5f", - "0x93fd7ded75058aba1b76c35c4ac4e9355e596edc", - "0x25b9ebc8d7d48a6b0e71e82aa66832acc9419e3a", - "0xbb086ecac1316b81107e3ca591ef645831094e5a", - "0x08c7a5db749def9280108ec5e0354d4957cb17cf", - "0x0da76aa44116fad143f778f25907046e52f8c4d3", - "0xcfd0a3bfa35e771aad88c64ef0a310eff6730cda", - "0xa7439b51638f31f054c93ec869c8c7e982699bac", - "0x5c9a97f096cb18903994c44ddc07ffd921490b2c", - "0x0e52786af0b48d764a255f6506c9c297d5ba2dc3", - "0x5c2093921171f2c2d657eaa681d463fe36c965d1", - "0xf8de801f1ba2a676d96eb1f1ccb0b0cadfcbbe9e", - "0x31d230fabad05777bb3e1a062e781446bc422b80" - ], "leaves": [ - "0xa2619da6f9de228ce3f0ad996c8cf37ad67d723718b591674a69e5661033ced4", - "0xf09c0dd27341e504f89c98d7d6ff7016919dc4f9721f58818cb19ce79914a95f", - "0xef2de724b0d0623903af391edcad11aa66738852994b876078a159525f6d6c4e", - "0xdec801cf38770ff0e8d39afea8ee2278ded6ba535394acdca83738b7c8969785", - "0xb4c6a4c31eb77b8e4d3be761340ba7e88429d9239c977aa5e5698cf03e543f5a", - "0x87835487f0f18c051967219757b01d5eb62c5d1a9f1afe4b5441d7bafc0a2074", - "0x7ff7d9645e0cad3fe9acd19c2dfc3da19536dff68e9e27db1f93d0f39cde1051", - "0x19728855d407f2b38c28bedca25c9d2be86c8eaf4eb8b70def028dfe837e005e", - "0xc99573c6e9f75b23a386fa5e5853540675edfcdaa5ad200f456fb9d76e2cdf74", - "0x21b7bf0de4e5d31ebb5e3023d1c6d344dc181669169f7b46009e3316abc6c769", - "0x2ea2c7805fb6bc45d2354c5df10243b4d41307ce3150527470ab3b175f2b087c", - "0xd8a4915d02943529bcd9ca6eea84a9035bc67b8e2f6e4a676cba7d00e72b05d9", - "0x3a430b2240dc5bd86c39b48f9f1a28e66230e02dabc4882a2706d08cadb1c644", - "0x403a632a93dd10731a3b1f95dcaf74380d7d8f87fa9804b5b1771f68d8b5e77e", - "0x0cfc60836a624120490cb0a0545b00b7e722174084aebf6e51e4a75cf59611d4", - "0xc5da75f1681a8a3c78d1aada32051090ec539d9b58b33a67e158d6103e20cfcc", - "0x16993f767beaf731359840fcbbea261456e2c13b5e6d90ebb4edfd8cb2a34c12", - "0x2b4388f707db1c673a241028232b08ff88fbae3bd7b3e4785951fb6ab8534781", - "0x0f94921debee580f6ce2d7372320accc1af2830eb05c9e2598c19f70dfa559a4", - "0x2dbb3f7d7aac347c130bd6f1ad396ba1a0bf9133128f1fa076b93810fe854efd", - "0xffd960a69fef871723a9dd196af598021ccbd63ff914ed9c68ce84d61bd6bc75", - "0x17e72d621cf42e7627815312f9581aacac54e1c44f41bfab6c52e29852545dd4", - "0x549a0ee3a3481975d1988252f823fc7260c620af0b489b7abc96a22e250c449c", - "0x1856904839c7f70794a93b4655050f8c2ebf462a39f0e7ed3edc2bd23e7f1f12", - "0xae819f3cf3b174421fc8e4dc1ee99f21ea14b34d5ad759dc9fa34394b293d476", - "0x3343b5bd07d4ff4108be1600ff4b6f5386cab4d388c60aec335b4a0634a82dd6", - "0x9c95427ecc2c9fc49fcccdc9e15bb847c9959994c57c5c919795e1ba74e8b10c", - "0xf7f19507ee3cf3292abff7d1e6e30aec7820be6d30ad552ef30be84dc6757da4", - "0x4f6f9a4370464bb10ce1bc38af0116d194deb28e1af95785816b764a5b7c2864", - "0xa52443afcd89dc39d1188b0413aa858dafaa1164772718eeed9569f99ae616da", - "0x08396f083f72d32ead958fa7fdb538a04d7492025f9ad7fa2b95a81d5e63184d", - "0x0f8b47e9beded26a7702189b2b180a5000c3362148befd44bb032633fb7aeeec" + "0xe460585d9b2385592b26a34d6908ea58165586cb39e5e6cb365b68246d29d7f8", + "0x5a7295b074b2ffeb07bd8bacbdd97aa97b0b269db43779112ef24b52548a9a2a", + "0xde239e1e8b54de83c9b0e3f32c269b265dd0efcda92c93a2146f44302e604080", + "0x98681050a4c0e39d25f1a44d95b343a05f7139cc882f628c569b3a1ae889f0e6", + "0xd3d70b40cc2a71e9a4996a2afaabcafe93af95ba9de147e3835ccddba2d82fdd", + "0xd46fec5943f6d40c9a68076fbc325daf7763607aaa60ca9be297cade5a1efca5", + "0x4c54e0aab6332cea9a9f867933caee83c6167aa78f663129d10e56cee35aacdd", + "0xf487aba0c467c53aa4fc9a7319817e1448efd774dedb235a1ab95a5dd2592d21", + "0xc734b7fd5abe87f4dff06da98980e19894117e92738e27e8dc0826eb4dee7202", + "0x8bcc65728c792dfaa58c6b63d192c2e37cd3db7c62774e7b40b9b3232597073a", + "0x6dbb052d9082cf78a0464cae809cd6c1be9d5657fc75a0fa1efade46f047aa01", + "0x11ea20a8fb14ed8b5ba47e83935f4dc1c032be3a3a9895a65fabed6e1adbef5c", + "0xd108801a4cfa732a19995a6f930ccdda98e91ce393f55eae7f63781568b44c74", + "0x423b7a7716ba307d27c05a6bbfde03b35c9544dffcf6702f69a205cff40a51da", + "0xed832ce8f80ed861bd13b1104490724dd38ab1c9ff18fd8e02ad13eb287af68f", + "0x6eca57794d8d55ec934427971898952017d87bd2773b64c554629f32f55fc7dc", + "0xc7abf795f5ebe46e9f86ba72d58f38ef535475cc41a11913fa1ec51cf902ee1a", + "0xbffebb2a3584cb6f96af4f8da6f5eea2e64066f0caa4bc6f44abb69b621a2b79", + "0x04de39dc7a9f11eba923271d07b5fda4f6b38012858a9a5a9d8f6557706981bd", + "0xef5e2f249fce6c67f5483b52e87384c6a6f6b5f8f102ecfede50cc9f8dfa78af", + "0x34e1511b36260dd619fcb205311055b87d31bb6440c9fb2a8b272bc1dcb1d699", + "0x0640b605ee9f8d8b38118c8dd1f51ca30f3b3f9037c29e598f39b91326825c46", + "0xfe7de1151f56cc10894b6bd63fc995a741c54d9069ee97247cb28627a4838da8", + "0xf17bb6827fe8873b839ecefe872776f757ca087dd65c2c2882523b71dcd24f05", + "0x7a11106b01c8d98348739c89007dddca673f18e9c38ef2d953315a1a49b23ce0", + "0xa7f0a37834fab9ce2cfbe364ccc4c50c88d48a061f0901889cc4fdc6b088a3ea", + "0xb386fae6a43e096a3d66147212a4fc756f7ed921febb2404f1d060111e4521e8", + "0x98484766860a98231a6834276f1ca84c8cf381e4931d635268b9b7d9db976958", + "0xd5007290e81283abd144a619da55be689e7b3eeb8a8b79f0de5e1f2793b056fe", + "0xac6812ede94056979e789ac4bd7dc5e4e682ea93aaaa1aadb22645ec44e21772", + "0xdc0662d88af437d468ed541ee9088464770bbd149a5ce5b3cdd9e836888c5b9d", + "0x6c8e78ff6214e87c5a791423385e31659921f3bb09376b302dd3933f98f346b4" ], + "origin_token_address": "0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891", "roots": [ - "0x8cc758e5c6fa8345fd3b0a0181caa5dc6e77f423c580b8e87c03012584a73692", - "0x98b61870eb76d5e6edbc50e020d75bb1fcb4b7b401c7c4b2a1f89a80d1c09a79", - "0x880ca4f47ba1927e7fcdcbec8ab116b56b60da9cd004ca1d5face545643aa08a", - "0xb20bc203ee04e2a7090e16b13e87729056a68fb3ab59de412bee1f0e59bc0c9c", - "0xcfa71ba9de0f8f07725ec7d219498f6795a6244c2d8acf123fb16e5b2481dd20", - "0x48d2645722a03a1e88614ac5b824f11e5b86499121b0716cfcaee5e488c90c59", - "0x383a9831eecd5de9f16fd42cc9e37e3dc192c927250f3e9b0ba841f9ff47a10e", - "0x3108b99fcba1929e52ae6ac3efb50fbb369b3f28d9dfd3545fb5daa512c0094a", - "0xcaa8fcf88451b7003616d53bfad0aad284444f54e630fd5fa29f382d8d909a9b", - "0x45c0ab378292e4ec9fd445a17e596252f1d469d82cc5fae0dd2aea5272c7353a", - "0xbac28ff6568a8373cfa4fed0451fe1ff681e2ce723056b0ce9cf7f478800227c", - "0xfcb39eb0b04a6eaa4e9245774766ed4e573f00e44d295885ee1eccce2409ca0a", - "0x26bde93e570491ca04e1756a80327f351c25597653bf60297cd450b34f885490", - "0xafdf583feab737f2d6ac97e1b49269512f556ecd8498fa0b08d31be1f4537680", - "0xc7bdfe4f0e89a31dbfc7efbb2e405d258d0ddc349acaa5bc06901d2e2fbbb68f", - "0x251b6bab8a7f95560f0390a2c5833c5fb9492e91d98e90ad6d490ec0bd36a4ad", - "0xa72ae26e10e8dd0aaa536d2403c35fb509affcbe534cef0faa4186ad4bbfcf20", - "0xbcaa37a71be0cf0a4db6ee7dcfaf2323ba3795f6ec7c227e8f9f91d9675d2609", - "0xb29fedb04c912e66b81048f7c17519197e5e61b386b0ef26e43892cc285a1b6b", - "0xcd3e646ad3f7915eb598e639b503d884f6806127d16aa0c76f1cec85148f9de1", - "0xef2853e40b2e0f9f699651245d7773a2c4a9d935cc6bdf1b5e46b71f101ba888", - "0x227e25d9d9f79839aab39e181bd03c9989e4a388308a666a600c17f3fef4de8e", - "0x1d45946e28e961ae6e7bd971b4ae8e7607bc58ba5a99c31743bc34be94f4ad1b", - "0x61560eb989331073660926c8261458e2188c45f2ce02434f89fe3841a73a8c17", - "0x35935bf0c2325d8ca58b6446937abd310ea12d9e6641662c1f2ee05d250c4a8a", - "0x4bfb9be91a0f8bc7184dfefbc6d79b7b24e5ac59ad06a0f29e4992a2802f5e79", - "0x6c1f77384e835b1395286931fd4d5c12959bc9cc90d7050f5de05619991f02b4", - "0x56cda6d58d7dd69c3b48d81b038cda89bfe1cd1a5b8fa5dffdc8fdd0eec3ed77", - "0x1641df206eddddc851fde5b92ddaab17cd268f60a93f4c129828b59ef1e9d6c7", - "0x4a893765b186f2b595e1a4bec1926ac92d9b3e7b3273678edf5889efb1d780b0", - "0x2a3a74b27e9559e8e5d6c31c1a9cedd791bb0462305beac1b3204b55f943a5d9", - "0x23395138bd9267d5fa9ad3d3c55d2a324703664ea7a2c378f0e0463aae5ea98e" + "0xacd6f8510c036081e605dd2c8749d2b7d3b289913514d10af9538cb4b32b7ded", + "0x2d7b622637d38f862a074a0160bc1e54ad7df147ff3374af82777b37021b22e1", + "0xf9bdf29ab9c4cbd2927b759b9f8ddafa90317bdb91f388b8eee08038ff5ded00", + "0x80134ca84d0d742662f3ec22543f4cf33f02dc0b628f51d1df1c521ef3018395", + "0x21d6f3b63306929d624f01ffdbe216acb822bf080bcf04b7e6021db957e7bee4", + "0x7932d55a970d094161976d0b562805779d55a81b08a501983c2b121a0c989a1e", + "0x43f09c6c8a277ee6fbc0e3f8261ba4570f32d1cbfff06bf662aa8e5feeb742bc", + "0x9ae3a76a5c7fcc2af6e3cb937b7e1a4ba397a46029987b06fec29257ba408564", + "0x007e432139766ea419be4aeda41a59e20114c0b772b61e43b3a344fa1c4e1196", + "0xdf60f37334585bc10d67b107b417a14181158ac9828f56a9337684a81e7405d9", + "0xba49ac55a723278ef6cd8f12193a405bc90cd2b6e88f8791f8d48d69fe952104", + "0x4ab8529bce44bcfb8c8e90c9adebebca9e51f44b0e8a048d99bf7717cb58eae7", + "0xf9313f060db170a5287bcc78443267e893f638731dd48a9131b120f9c5833f88", + "0x49a9e6e504f2a6938bbefba42ec2b4930eed298a04eac403af1e0a6286017960", + "0xe318ce76597523c02da0094bcfd970e88c9544c6393d9bfe17d96e2a17f4856d", + "0x00d4099acc3d2a2cdd76f693fb527b218f369bc8e92af4a39328809738497a9d", + "0xf4db3da65c8fda88ad4a1ad1aca66e9260d5230a962791b57d396948a78fe96e", + "0x6813db5a7b4ac98c11d84412df7d6552941d30c7adb95e7025b13d747cf0f3f7", + "0xf1e93cbb96e5fabaee7cbb44f87f44832c9c290a5f85631d8c86493bab6ba0d5", + "0x654a2e78a6e49c969a0fedad0e4372862950ca371406c122779cf62e16dfe7e7", + "0x1a07ce13254cfb6697256a401063d6c43e5a89b8b1945c90bce62c464da1ba27", + "0xedaf2d835d1e6fdd801555835b2cadcd04517f8668f30658019869d0376c6c36", + "0x82adda5fd38a4718f37b2d4fe9fe99b364cced5de9bdfa4c6bdcd118da42c64c", + "0x2d28e62dd13f99153b5e9eb4d68cc1f99a5bd510375f2d1ed522c0062a2d38d7", + "0xd87e80ebe2f69df6735911707780df6b882189db786b5507310249a26d3db69d", + "0x5406d2fbc12edcccd2b8c755b7063ababc760ce23da62032d500a10d49756994", + "0xce99e7d0f9d77226cae034297dfec349d866f892eb753a8c7f5bba4bec52364c", + "0x4419d0e6c47cac3e4fc917f91d878582ed4496bef8e7df219be4d483e496ff0a", + "0xafe2c2b44e58c34576299a201d4918f47d5a48b6fa7a229eaf59e226120b12ac", + "0x9d2989190f9edb660b043a55f3051412280dd7bb7d4d042e3695d3a2b23f5b8d", + "0x18b772e2e093d5f69151c3b6da00d42a2066d1f5980e5f9210ae902f5a5643ca", + "0x6717e563a6c40e1562235c4cbbc2ba0de5be6be07101715e8d3922361b77d394" ] -} +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol index 84213c994d..1f6ab63d2c 100644 --- a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol @@ -10,7 +10,7 @@ import "@agglayer/v2/lib/DepositContractV2.sol"; * between Solidity's DepositContractBase and Miden's MMR Frontier implementation. * * Leaves are constructed via getLeafValue using the same hardcoded fields that - * bridge_out.masm uses (leafType=0, originNetwork=64, originTokenAddress=0, + * bridge_out.masm uses (leafType=0, originNetwork=64, originTokenAddress=fixed random value, * metadataHash=0), parametrised by amount (i+1) and deterministic per-leaf * destination network/address values derived from a fixed seed. * @@ -24,7 +24,7 @@ contract MMRTestVectors is Test, DepositContractV2 { // Constants matching bridge_out.masm hardcoded values uint8 constant LEAF_TYPE = 0; uint32 constant ORIGIN_NETWORK = 64; - address constant ORIGIN_TOKEN_ADDR = address(0); + address constant ORIGIN_TOKEN_ADDR = 0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891; bytes32 constant METADATA_HASH = bytes32(0); // Fixed seed for deterministic "random" destination vectors. @@ -125,6 +125,7 @@ contract MMRTestVectors is Test, DepositContractV2 { vm.serializeUint(obj, "counts", counts); vm.serializeUint(obj, "amounts", amounts); vm.serializeUint(obj, "destination_networks", destinationNetworks); + vm.serializeAddress(obj, "origin_token_address", ORIGIN_TOKEN_ADDR); string memory json = vm.serializeAddress(obj, "destination_addresses", destinationAddresses); // Save to file diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs new file mode 100644 index 0000000000..c8c259a7b2 --- /dev/null +++ b/crates/miden-agglayer/src/config_note.rs @@ -0,0 +1,115 @@ +//! CONFIG_AGG_BRIDGE note creation utilities. +//! +//! This module provides helpers for creating CONFIG_AGG_BRIDGE notes, +//! which are used to register faucets in the bridge's faucet registry. + +extern crate alloc; + +use alloc::string::ToString; +use alloc::vec; + +use miden_assembly::utils::Deserializable; +use miden_core::{Program, Word}; +use miden_protocol::account::AccountId; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteType, +}; +use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; +use miden_utils_sync::LazyLock; + +// NOTE SCRIPT +// ================================================================================================ + +// Initialize the CONFIG_AGG_BRIDGE note script only once +static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CONFIG_AGG_BRIDGE.masb")); + let program = + Program::read_from_bytes(bytes).expect("Shipped CONFIG_AGG_BRIDGE script is well-formed"); + NoteScript::new(program) +}); + +// CONFIG_AGG_BRIDGE NOTE +// ================================================================================================ + +/// CONFIG_AGG_BRIDGE note. +/// +/// This note is used to register a faucet in the bridge's faucet registry. +/// It carries the faucet account ID and is always public. +pub struct ConfigAggBridgeNote; + +impl ConfigAggBridgeNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for a CONFIG_AGG_BRIDGE note. + pub const NUM_STORAGE_ITEMS: usize = 2; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the CONFIG_AGG_BRIDGE note script. + pub fn script() -> NoteScript { + CONFIG_AGG_BRIDGE_SCRIPT.clone() + } + + /// Returns the CONFIG_AGG_BRIDGE note script root. + pub fn script_root() -> Word { + CONFIG_AGG_BRIDGE_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Creates a CONFIG_AGG_BRIDGE note to register a faucet in the bridge's registry. + /// + /// The note storage contains 2 felts: + /// - `faucet_id_prefix`: The prefix of the faucet account ID + /// - `faucet_id_suffix`: The suffix of the faucet account ID + /// + /// # Parameters + /// - `faucet_account_id`: The account ID of the faucet to register + /// - `sender_account_id`: The account ID of the note creator + /// - `target_account_id`: The bridge account ID that will consume this note + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + faucet_account_id: AccountId, + sender_account_id: AccountId, + target_account_id: AccountId, + rng: &mut R, + ) -> Result { + // Create note storage with 2 felts: [faucet_id_prefix, faucet_id_suffix] + let storage_values = vec![faucet_account_id.prefix().as_felt(), faucet_account_id.suffix()]; + + let note_storage = NoteStorage::new(storage_values)?; + + // Generate a serial number for the note + let serial_num = rng.draw_word(); + + let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage); + + let attachment = NoteAttachment::from( + NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))?, + ); + let metadata = + NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + + // CONFIG_AGG_BRIDGE notes don't carry assets + let assets = NoteAssets::new(vec![])?; + + Ok(Note::new(assets, metadata, recipient)) + } +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 997ef62715..ba836a0cfa 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -22,6 +22,14 @@ pub const ERR_BRIDGE_NOT_MAINNET: MasmError = MasmError::from_static_str("bridge /// Error Message: "CLAIM's target account address and transaction address do not match" pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM's target account address and transaction address do not match"); +/// Error Message: "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" +pub const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE note attachment target account does not match consuming account"); +/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 2 note storage items" +pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 2 note storage items"); + +/// Error Message: "faucet is not registered in the bridge's faucet registry" +pub const ERR_FAUCET_NOT_REGISTERED: MasmError = MasmError::from_static_str("faucet is not registered in the bridge's faucet registry"); + /// Error Message: "combined u64 doesn't fit in field" pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combined u64 doesn't fit in field"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index af51d606ec..5db0c68195 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -27,6 +27,7 @@ use miden_utils_sync::LazyLock; pub mod b2agg_note; pub mod claim_note; +pub mod config_note; pub mod errors; pub mod eth_types; pub mod update_ger_note; @@ -42,6 +43,7 @@ pub use claim_note::{ SmtNode, create_claim_note, }; +pub use config_note::ConfigAggBridgeNote; pub use eth_types::{ EthAddressFormat, EthAmount, @@ -202,6 +204,49 @@ pub fn asset_conversion_component(storage_slots: Vec) -> AccountCom ) } +// FAUCET CONVERSION STORAGE HELPERS +// ================================================================================================ + +/// Builds the two storage slot values for faucet conversion metadata. +/// +/// The conversion metadata is stored in two value storage slots: +/// - Slot 1 (`miden::agglayer::faucet::conversion_info_1`): `[addr0, addr1, addr2, addr3]` — first +/// 4 felts of the origin token address (5 × u32 limbs). +/// - Slot 2 (`miden::agglayer::faucet::conversion_info_2`): `[addr4, origin_network, scale, 0]` — +/// remaining address felt + origin network + scale factor. +/// +/// # Parameters +/// - `origin_token_address`: The EVM token address in Ethereum format +/// - `origin_network`: The origin network/chain ID +/// - `scale`: The decimal scaling factor (exponent for 10^scale) +/// +/// # Returns +/// A tuple of two `Word` values representing the two storage slot contents. +pub fn agglayer_faucet_conversion_slots( + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, +) -> (Word, Word) { + let addr_elements = origin_token_address.to_elements(); + + let slot1 = Word::new([addr_elements[0], addr_elements[1], addr_elements[2], addr_elements[3]]); + + let slot2 = + Word::new([addr_elements[4], Felt::from(origin_network), Felt::from(scale), Felt::ZERO]); + + (slot1, slot2) +} + +// FAUCET REGISTRY HELPERS +// ================================================================================================ + +/// Creates a faucet registry map key from a faucet account ID. +/// +/// The key format is `[faucet_id_prefix, faucet_id_suffix, 0, 0]`. +pub fn faucet_registry_key(faucet_id: AccountId) -> Word { + Word::new([Felt::ZERO, Felt::ZERO, faucet_id.suffix(), faucet_id.prefix().as_felt()]) +} + // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ @@ -222,14 +267,20 @@ pub fn create_bridge_account_component() -> AccountComponent { /// Creates an agglayer faucet account component with the specified configuration. /// /// This function creates all the necessary storage slots for an agglayer faucet: -/// - Network faucet metadata slot (max_supply, decimals, token_symbol) +/// - Network faucet metadata slot (token_supply, max_supply, decimals, token_symbol) /// - Bridge account reference slot for FPI validation +/// - Conversion info slot 1: first 4 felts of origin token address +/// - Conversion info slot 2: 5th address felt + origin network + scale /// /// # Parameters /// - `token_symbol`: The symbol for the fungible token (e.g., "AGG") /// - `decimals`: Number of decimal places for the token /// - `max_supply`: Maximum supply of the token +/// - `token_supply`: Initial outstanding token supply (0 for new faucets) /// - `bridge_account_id`: The account ID of the bridge account for validation +/// - `origin_token_address`: The EVM origin token address +/// - `origin_network`: The origin network/chain ID +/// - `scale`: The decimal scaling factor (exponent for 10^scale) /// /// # Returns /// Returns an [`AccountComponent`] configured for agglayer faucet operations. @@ -240,12 +291,16 @@ pub fn create_agglayer_faucet_component( token_symbol: &str, decimals: u8, max_supply: Felt, + token_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> AccountComponent { - // Create network faucet metadata slot: [0, max_supply, decimals, token_symbol] + // Create network faucet metadata slot: [token_supply, max_supply, decimals, token_symbol] let token_symbol = TokenSymbol::new(token_symbol).expect("Token symbol should be valid"); let metadata_word = - Word::new([FieldElement::ZERO, max_supply, Felt::from(decimals), token_symbol.into()]); + Word::new([token_supply, max_supply, Felt::from(decimals), token_symbol.into()]); let metadata_slot = StorageSlot::with_value(NetworkFungibleFaucet::metadata_slot().clone(), metadata_word); @@ -260,12 +315,28 @@ pub fn create_agglayer_faucet_component( .expect("Agglayer faucet storage slot name should be valid"); let bridge_slot = StorageSlot::with_value(agglayer_storage_slot_name, bridge_account_id_word); + // Create conversion metadata storage slots + let (conversion_slot1_word, conversion_slot2_word) = + agglayer_faucet_conversion_slots(origin_token_address, origin_network, scale); + + let conversion_info_1_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") + .expect("Conversion info 1 storage slot name should be valid"); + let conversion_slot1 = StorageSlot::with_value(conversion_info_1_name, conversion_slot1_word); + + let conversion_info_2_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") + .expect("Conversion info 2 storage slot name should be valid"); + let conversion_slot2 = StorageSlot::with_value(conversion_info_2_name, conversion_slot2_word); + // Combine all storage slots for the agglayer faucet component - let agglayer_storage_slots = vec![metadata_slot, bridge_slot]; + let agglayer_storage_slots = + vec![metadata_slot, bridge_slot, conversion_slot1, conversion_slot2]; agglayer_faucet_component(agglayer_storage_slots) } /// Creates a complete bridge account builder with the standard configuration. +/// +/// The bridge starts with an empty faucet registry. Faucets are registered at runtime +/// via CONFIG_AGG_BRIDGE notes that call `bridge_config::register_faucet`. pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { let ger_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger") .expect("Bridge storage slot name should be valid"); @@ -285,15 +356,21 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { .expect("LET root_hi storage slot name should be valid"); let let_num_leaves_slot_name = StorageSlotName::new("miden::agglayer::let::num_leaves") .expect("LET num_leaves storage slot name should be valid"); + let faucet_registry_slot_name = + StorageSlotName::new("miden::agglayer::bridge::faucet_registry") + .expect("Faucet registry storage slot name should be valid"); let bridge_out_storage_slots = vec![ StorageSlot::with_empty_map(let_storage_slot_name), StorageSlot::with_value(let_root_lo_slot_name, Word::empty()), StorageSlot::with_value(let_root_hi_slot_name, Word::empty()), StorageSlot::with_value(let_num_leaves_slot_name, Word::empty()), + StorageSlot::with_empty_map(faucet_registry_slot_name), ]; let bridge_out_component = bridge_out_component(bridge_out_storage_slots); - // Combine the components into a single account(builder) + // Combine the components into a single account(builder). + // Note: bridge_config::register_faucet is also exposed via the agglayer library + // included in bridge_out_component, using the faucet_registry storage slot above. Account::builder(seed.into()) .storage_mode(AccountStorageMode::Network) .with_component(bridge_out_component) @@ -322,15 +399,28 @@ pub fn create_existing_bridge_account(seed: Word) -> Account { } /// Creates a complete agglayer faucet account builder with the specified configuration. +#[allow(clippy::too_many_arguments)] pub fn create_agglayer_faucet_builder( seed: Word, token_symbol: &str, decimals: u8, max_supply: Felt, + token_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> AccountBuilder { - let agglayer_component = - create_agglayer_faucet_component(token_symbol, decimals, max_supply, bridge_account_id); + let agglayer_component = create_agglayer_faucet_component( + token_symbol, + decimals, + max_supply, + token_supply, + bridge_account_id, + origin_token_address, + origin_network, + scale, + ); Account::builder(seed.into()) .account_type(AccountType::FungibleFaucet) @@ -347,26 +437,54 @@ pub fn create_agglayer_faucet( decimals: u8, max_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> Account { - create_agglayer_faucet_builder(seed, token_symbol, decimals, max_supply, bridge_account_id) - .with_auth_component(AccountComponent::from(NoAuth)) - .build() - .expect("Agglayer faucet account should be valid") + create_agglayer_faucet_builder( + seed, + token_symbol, + decimals, + max_supply, + Felt::ZERO, + bridge_account_id, + origin_token_address, + origin_network, + scale, + ) + .with_auth_component(AccountComponent::from(NoAuth)) + .build() + .expect("Agglayer faucet account should be valid") } /// Creates an existing agglayer faucet account with the specified configuration. /// /// This creates an existing account suitable for testing scenarios. #[cfg(any(feature = "testing", test))] +#[allow(clippy::too_many_arguments)] pub fn create_existing_agglayer_faucet( seed: Word, token_symbol: &str, decimals: u8, max_supply: Felt, + token_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> Account { - create_agglayer_faucet_builder(seed, token_symbol, decimals, max_supply, bridge_account_id) - .with_auth_component(AccountComponent::from(NoAuth)) - .build_existing() - .expect("Agglayer faucet account should be valid") + create_agglayer_faucet_builder( + seed, + token_symbol, + decimals, + max_supply, + token_supply, + bridge_account_id, + origin_token_address, + origin_network, + scale, + ) + .with_auth_component(AccountComponent::from(NoAuth)) + .build_existing() + .expect("Agglayer faucet account should be valid") } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index ffe578b1cb..9c29812ab2 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -2,18 +2,19 @@ extern crate alloc; use miden_agglayer::{ ClaimNoteStorage, + EthAddressFormat, OutputNoteData, UpdateGerNote, create_claim_note, create_existing_agglayer_faucet, create_existing_bridge_account, }; -use miden_protocol::Felt; use miden_protocol::account::Account; use miden_protocol::asset::FungibleAsset; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, FieldElement}; use miden_standards::account::wallets::BasicWallet; use miden_testing::{AccountState, Auth, MockChain}; use rand::Rng; @@ -46,12 +47,21 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let agglayer_faucet_seed = builder.rng_mut().draw_word(); + // Origin token address for the faucet's conversion metadata + let origin_token_address = EthAddressFormat::new([0u8; 20]); + let origin_network = 0u32; + let scale = 0u8; + let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, token_symbol, decimals, max_supply, + Felt::ZERO, bridge_account.id(), + &origin_token_address, + origin_network, + scale, ); builder.add_account(agglayer_faucet.clone())?; diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index c970a3d40c..83fc957eb3 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,7 +1,14 @@ extern crate alloc; -use miden_agglayer::errors::ERR_B2AGG_TARGET_ACCOUNT_MISMATCH; -use miden_agglayer::{B2AggNote, EthAddressFormat, ExitRoot, create_existing_bridge_account}; +use miden_agglayer::errors::{ERR_B2AGG_TARGET_ACCOUNT_MISMATCH, ERR_FAUCET_NOT_REGISTERED}; +use miden_agglayer::{ + B2AggNote, + ConfigAggBridgeNote, + EthAddressFormat, + ExitRoot, + create_existing_agglayer_faucet, + create_existing_bridge_account, +}; use miden_crypto::rand::FeltRng; use miden_protocol::Felt; use miden_protocol::account::{ @@ -13,9 +20,10 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::note::NoteAssets; +use miden_protocol::note::{NoteAssets, NoteScript, NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; use miden_standards::account::faucets::TokenMetadata; +use miden_standards::note::StandardNote; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; use miden_tx::utils::hex_to_bytes; @@ -64,12 +72,19 @@ fn read_let_num_leaves(account: &Account) -> u64 { /// Tests that 32 sequential B2AGG note consumptions match all 32 Solidity MMR roots. /// -/// This test flow: -/// 1. Creates a network faucet to provide assets -/// 2. Creates a bridge account with the bridge_out component (using network storage) -/// 3. Creates a B2AGG note with assets from the network faucet -/// 4. Executes the B2AGG note consumption via network transaction -/// 5. Consumes the BURN note +/// This test exercises the complete bridge-out lifecycle: +/// 1. Creates a bridge account (empty faucet registry) and an agglayer faucet with conversion +/// metadata (origin token address, network, scale) +/// 2. Registers the faucet in the bridge's faucet registry via a CONFIG_AGG_BRIDGE note +/// 3. Creates a B2AGG note with assets from the agglayer faucet +/// 4. Consumes the B2AGG note against the bridge account — the bridge's `bridge_out` procedure: +/// - Validates the faucet is registered via `convert_asset` +/// - Calls the faucet's `asset_to_origin_asset` via FPI to get the scaled amount, origin token +/// address, and origin network +/// - Writes the leaf data and computes the Keccak hash for the MMR +/// - Creates a BURN note addressed to the faucet +/// 5. Verifies the BURN note was created with the correct asset, tag, and script +/// 6. Consumes the BURN note with the faucet to burn the tokens #[tokio::test] async fn bridge_out_consecutive() -> anyhow::Result<()> { let vectors = &*SOLIDITY_MMR_FRONTIER_VECTORS; @@ -88,29 +103,52 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { ); let mut builder = MockChain::builder(); - let faucet_owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); + let mut bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + + let expected_amounts = vectors + .amounts + .iter() + .map(|amount| amount.parse::().expect("valid amount decimal string")) + .collect::>(); + let total_burned: u64 = expected_amounts.iter().sum(); - // We burn all 32 produced burn notes at the end; initial supply must cover their total amount. - let faucet = builder.add_existing_network_faucet( + // CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI) + // -------------------------------------------------------------------------------------------- + let origin_token_address = EthAddressFormat::from_hex(&vectors.origin_token_address) + .expect("valid shared origin token address"); + let origin_network = 64u32; + let scale = 0u8; + let faucet = create_existing_agglayer_faucet( + builder.rng_mut().draw_word(), "AGG", - 10_000, - faucet_owner_account_id, - Some(10_000), - )?; + 8, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(total_burned), + bridge_account.id(), + &origin_token_address, + origin_network, + scale, + ); + builder.add_account(faucet.clone())?; - let mut bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); - builder.add_account(bridge_account.clone())?; + // CREATE SENDER ACCOUNT + // -------------------------------------------------------------------------------------------- + let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; + // CONFIG_AGG_BRIDGE note to register the faucet in the bridge + let config_note = ConfigAggBridgeNote::create( + faucet.id(), + sender_account.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(OutputNote::Full(config_note.clone())); + + // CREATE ALL B2AGG NOTES UPFRONT (before building mock chain) + // -------------------------------------------------------------------------------------------- let mut notes = Vec::with_capacity(note_count); - let mut expected_amounts = Vec::with_capacity(note_count); - for i in 0..note_count { - let amount: u64 = vectors.amounts[i].parse().expect("valid amount decimal string"); - expected_amounts.push(amount); + for (i, &amount) in expected_amounts.iter().enumerate().take(note_count) { let destination_network = vectors.destination_networks[i]; let eth_address = EthAddressFormat::from_hex(&vectors.destination_addresses[i]) .expect("valid destination address"); @@ -129,11 +167,31 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { } let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // STEP 1: REGISTER FAUCET VIA CONFIG_AGG_BRIDGE NOTE + // -------------------------------------------------------------------------------------------- + let config_executed = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()? + .execute() + .await?; + bridge_account.apply_delta(config_executed.account_delta())?; + mock_chain.add_pending_executed_transaction(&config_executed)?; + mock_chain.prove_next_block()?; + + // STEP 2: CONSUME 32 B2AGG NOTES AND VERIFY FRONTIER EVOLUTION + // -------------------------------------------------------------------------------------------- + let burn_note_script: NoteScript = StandardNote::BURN.script(); let mut burn_note_ids = Vec::with_capacity(note_count); for (i, note) in notes.iter().enumerate() { + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + let executed_tx = mock_chain - .build_tx_context(bridge_account.id(), &[note.id()], &[])? + .build_tx_context(bridge_account.clone(), &[note.id()], &[])? + .add_note_script(burn_note_script.clone()) + .foreign_accounts(vec![foreign_account_inputs]) .build()? .execute() .await?; @@ -156,6 +214,21 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { "BURN note after consume #{} should contain the bridged asset", i + 1 ); + assert_eq!( + burn_note.metadata().note_type(), + NoteType::Public, + "BURN note should be public" + ); + assert_eq!( + burn_note.metadata().tag(), + NoteTag::with_account_target(faucet.id()), + "BURN note should have the correct tag" + ); + assert_eq!( + burn_note.recipient().script().root(), + StandardNote::BURN.script_root(), + "BURN note should use the BURN script" + ); bridge_account.apply_delta(executed_tx.account_delta())?; assert_eq!( @@ -177,8 +250,14 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { mock_chain.prove_next_block()?; } + // STEP 3: CONSUME ALL BURN NOTES WITH THE AGGLAYER FAUCET + // -------------------------------------------------------------------------------------------- let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); - let total_burned: u64 = expected_amounts.iter().sum(); + assert_eq!( + initial_token_supply, + Felt::new(total_burned), + "Initial issuance should match all pending burns" + ); let mut faucet = faucet; for burn_note_id in burn_note_ids { @@ -207,6 +286,77 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { Ok(()) } +/// Tests that bridging out fails when the faucet is not registered in the bridge's registry. +/// +/// This test verifies the faucet allowlist check in bridge_out's `convert_asset` procedure: +/// 1. Creates a bridge account with an empty faucet registry (no faucets registered) +/// 2. Creates a B2AGG note with an asset from an agglayer faucet +/// 3. Attempts to consume the B2AGG note against the bridge — this should fail because +/// `convert_asset` checks the faucet registry and panics with ERR_FAUCET_NOT_REGISTERED when the +/// faucet is not found +#[tokio::test] +async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ACCOUNT (empty faucet registry — no faucets registered) + // -------------------------------------------------------------------------------------------- + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + + // CREATE AGGLAYER FAUCET ACCOUNT (NOT registered in the bridge) + // -------------------------------------------------------------------------------------------- + let origin_token_address = EthAddressFormat::new([0u8; 20]); + let faucet = create_existing_agglayer_faucet( + builder.rng_mut().draw_word(), + "AGG", + 8, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(100), + bridge_account.id(), + &origin_token_address, + 0, // origin_network + 0, // scale + ); + builder.add_account(faucet.clone())?; + + // CREATE B2AGG NOTE WITH ASSETS FROM THE UNREGISTERED FAUCET + // -------------------------------------------------------------------------------------------- + let amount = Felt::new(100); + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); + + let destination_address = "0x1234567890abcdef1122334455667788990011aa"; + let eth_address = + EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + + let b2agg_note = B2AggNote::create( + 1u32, // destination_network + eth_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + faucet.id(), + builder.rng_mut(), + )?; + + builder.add_output_note(OutputNote::Full(b2agg_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // ATTEMPT TO BRIDGE OUT WITHOUT REGISTERING THE FAUCET (SHOULD FAIL) + // -------------------------------------------------------------------------------------------- + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + let result = mock_chain + .build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])? + .foreign_accounts(vec![foreign_account_inputs]) + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_FAUCET_NOT_REGISTERED); + + Ok(()) +} + /// Tests the B2AGG (Bridge to AggLayer) note script reclaim functionality. /// /// This test covers the "reclaim" branch where the note creator consumes their own B2AGG note. @@ -234,7 +384,7 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; - // Create a bridge account (includes a `bridge_out` component tested here) + // Create a bridge account (includes a `bridge_out` component) let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); builder.add_account(bridge_account.clone())?; @@ -243,11 +393,9 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // CREATE B2AGG NOTE WITH USER ACCOUNT AS SENDER // -------------------------------------------------------------------------------------------- - let amount = Felt::new(50); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - // Create note storage with destination network and address let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = @@ -255,8 +403,8 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let assets = NoteAssets::new(vec![bridge_asset])?; - // Create the B2AGG note with the USER ACCOUNT as the sender - // This is the key difference - the note sender will be the same as the consuming account + // Create the B2AGG note with the USER ACCOUNT as the sender. + // This is the key difference — the note sender will be the same as the consuming account. let b2agg_note = B2AggNote::create( destination_network, eth_address, @@ -266,7 +414,6 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { builder.rng_mut(), )?; - // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); let mut mock_chain = builder.build()?; @@ -282,7 +429,6 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // VERIFY NO BURN NOTE WAS CREATED (RECLAIM BRANCH) // -------------------------------------------------------------------------------------------- - // In the reclaim scenario, no BURN note should be created assert_eq!( executed_transaction.output_notes().num_notes(), 0, @@ -295,14 +441,12 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // VERIFY ASSETS WERE ADDED BACK TO THE ACCOUNT // -------------------------------------------------------------------------------------------- let final_balance = user_account.vault().get_balance(faucet.id()).unwrap_or(0u64); - let expected_balance = initial_balance + amount.as_int(); - assert_eq!( - final_balance, expected_balance, + final_balance, + initial_balance + amount.as_int(), "User account should have received the assets back from the B2AGG note" ); - // Apply the transaction to the mock chain mock_chain.add_pending_executed_transaction(&executed_transaction)?; mock_chain.prove_next_block()?; @@ -351,11 +495,9 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { // CREATE B2AGG NOTE // -------------------------------------------------------------------------------------------- - let amount = Felt::new(50); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - // Create note storage with destination network and address let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = @@ -363,7 +505,7 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { let assets = NoteAssets::new(vec![bridge_asset])?; - // Create the B2AGG note + // Create the B2AGG note targeting the real bridge account let b2agg_note = B2AggNote::create( destination_network, eth_address, @@ -373,7 +515,6 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { builder.rng_mut(), )?; - // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); let mock_chain = builder.build()?; diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs new file mode 100644 index 0000000000..ee676d0816 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -0,0 +1,82 @@ +extern crate alloc; + +use miden_agglayer::{ConfigAggBridgeNote, create_existing_bridge_account, faucet_registry_key}; +use miden_protocol::account::{ + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, + StorageSlotName, +}; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, FieldElement}; +use miden_testing::{Auth, MockChain}; + +/// Tests that a CONFIG_AGG_BRIDGE note registers a faucet in the bridge's faucet registry. +/// +/// Flow: +/// 1. Create a bridge account (empty faucet registry) +/// 2. Create a sender account +/// 3. Create a CONFIG_AGG_BRIDGE note carrying a faucet ID +/// 4. Consume the note with the bridge account +/// 5. Verify the faucet is now in the bridge's faucet_registry map +#[tokio::test] +async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ACCOUNT (starts with empty faucet registry) + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + + // CREATE SENDER ACCOUNT + let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; + + // Use a dummy faucet ID to register (any valid AccountId will do) + let faucet_to_register = AccountId::dummy( + [42; 15], + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Network, + ); + + // Verify the faucet is NOT in the registry before registration + let registry_slot_name = StorageSlotName::new("miden::agglayer::bridge::faucet_registry")?; + let key = faucet_registry_key(faucet_to_register); + let value_before = bridge_account.storage().get_map_item(®istry_slot_name, key)?; + assert_eq!( + value_before, + [Felt::ZERO; 4].into(), + "Faucet should not be in registry before registration" + ); + + // CREATE CONFIG_AGG_BRIDGE NOTE + let config_note = ConfigAggBridgeNote::create( + faucet_to_register, + sender_account.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + + builder.add_output_note(OutputNote::Full(config_note.clone())); + let mock_chain = builder.build()?; + + // CONSUME THE CONFIG_AGG_BRIDGE NOTE WITH THE BRIDGE ACCOUNT + let tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()?; + let executed_transaction = tx_context.execute().await?; + + // VERIFY FAUCET IS NOW REGISTERED + let mut updated_bridge = bridge_account.clone(); + updated_bridge.apply_delta(executed_transaction.account_delta())?; + + let value_after = updated_bridge.storage().get_map_item(®istry_slot_name, key)?; + let expected_value = [Felt::new(1), Felt::ZERO, Felt::ZERO, Felt::ZERO].into(); + assert_eq!( + value_after, expected_value, + "Faucet should be registered with value [0, 0, 0, 1]" + ); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index f96326ffa3..7fbb3d38a0 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,6 +1,7 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; +mod config_bridge; mod crypto_utils; mod global_index; mod mmr_frontier; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 6a021047eb..39bc77c917 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -198,8 +198,9 @@ pub struct CanonicalZerosFile { /// Deserialized MMR frontier vectors from Solidity DepositContractV2. /// /// Each leaf is produced by `getLeafValue` using the same hardcoded fields as `bridge_out.masm` -/// (leafType=0, originNetwork=64, originTokenAddress=0, metadataHash=0), parametrised by -/// `amounts[i]` and per-index `destination_networks[i]` / `destination_addresses[i]`. +/// (leafType=0, originNetwork=64, metadataHash=0), parametrised by +/// a shared `origin_token_address`, `amounts[i]`, and per-index +/// `destination_networks[i]` / `destination_addresses[i]`. /// /// Amounts are serialized as uint256 values (JSON numbers). #[derive(Debug, Deserialize)] @@ -209,6 +210,7 @@ pub struct MmrFrontierVectorsFile { pub counts: Vec, #[serde(deserialize_with = "deserialize_uint_vec_to_strings")] pub amounts: Vec, + pub origin_token_address: String, pub destination_networks: Vec, pub destination_addresses: Vec, } From 86fcb9ec17d598b50eb91ecf266f634114584da4 Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:45:26 +0300 Subject: [PATCH 12/21] feat: add solidity-compat test for generating local claimAsset() param data (#2474) * feat: add solidity-compat test for generating local claimAsset() param data * refactor: rename solidity compat test files & claimAsset vector JSON files * refactor: rename testing methods & update test comments * Update crates/miden-testing/tests/agglayer/bridge_in.rs * Update crates/miden-testing/tests/agglayer/bridge_in.rs --- .../solidity-compat/foundry.toml | 11 +- .../claim_asset_vectors_local_tx.json | 86 ++++++++ ....json => claim_asset_vectors_real_tx.json} | 0 .../test/ClaimAssetTestVectorsLocalTx.t.sol | 194 ++++++++++++++++++ ....sol => ClaimAssetTestVectorsRealTx.t.sol} | 13 +- .../solidity-compat/test/ExitRoots.t.sol | 16 +- .../test/LeafValueTestVectors.t.sol | 11 +- .../solidity-compat/test/MMRTestVectors.t.sol | 37 ++-- .../test/SMTMerkleProofVectors.t.sol | 13 +- .../miden-testing/tests/agglayer/bridge_in.rs | 58 ++++-- .../tests/agglayer/test_utils.rs | 31 ++- 11 files changed, 390 insertions(+), 80 deletions(-) create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json rename crates/miden-agglayer/solidity-compat/test-vectors/{claim_asset_vectors.json => claim_asset_vectors_real_tx.json} (100%) create mode 100644 crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol rename crates/miden-agglayer/solidity-compat/test/{ClaimAssetTestVectors.t.sol => ClaimAssetTestVectorsRealTx.t.sol} (91%) diff --git a/crates/miden-agglayer/solidity-compat/foundry.toml b/crates/miden-agglayer/solidity-compat/foundry.toml index d8633d0d32..e841d3e689 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.toml +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -1,8 +1,11 @@ [profile.default] -libs = ["lib"] -out = "out" -solc = "0.8.20" -src = "src" +libs = ["lib"] +optimizer = true +optimizer_runs = 200 +out = "out" +solc = "0.8.20" +src = "src" +via_ir = true remappings = [ "@agglayer/=lib/agglayer-contracts/contracts/", diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json new file mode 100644 index 0000000000..612ac6a3b9 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json @@ -0,0 +1,86 @@ +{ + "amount": 1000000000000000, + "deposit_count": 1, + "description": "L1 bridgeAsset transaction test vectors with valid Merkle proofs", + "destination_address": "0x00000000AA0000000000bb000000cc000000Dd00", + "destination_network": 20, + "global_exit_root": "0xbb7f9cbe48190e8859cbaecc54c8957863c786862bde7cff62df0a8b5adc19bb", + "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", + "leaf_type": 0, + "leaf_value": "0x8e5f0c4b8526561e30a89f900185ec681cc620fe3cadd8b281fb929063bd27ac", + "local_exit_root": "0x3e9a88b19d477b03175f9db12ba6b20eb33c0fe7a271667031685f034caf5af6", + "mainnet_exit_root": "0x3e9a88b19d477b03175f9db12ba6b20eb33c0fe7a271667031685f034caf5af6", + "metadata": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a5465737420546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045445535400000000000000000000000000000000000000000000000000000000", + "metadata_hash": "0x4d0d9fb7f9ab2f012da088dc1c228173723db7e09147fe4fea2657849d580161", + "origin_network": 0, + "origin_token_address": "0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF", + "rollup_exit_root": "0xd18cc25ae65a4e3d95587ffea9411747238567d6e5d3744240554713edefc197", + "smt_proof_local_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344", + "0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d", + "0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968", + "0xffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f83", + "0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af", + "0xcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0", + "0xf9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5", + "0xf8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf892", + "0x3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c", + "0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb", + "0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc", + "0xda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d2", + "0x2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f", + "0xe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a", + "0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0", + "0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0", + "0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2", + "0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9", + "0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377", + "0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652", + "0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef", + "0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d", + "0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0", + "0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e", + "0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e", + "0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322", + "0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735", + "0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9" + ], + "smt_proof_rollup_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_real_tx.json similarity index 100% rename from crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json rename to crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_real_tx.json diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol new file mode 100644 index 0000000000..1608951319 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; + +/** + * @title ClaimAssetTestVectorsLocalTx + * @notice Test contract that generates test vectors for an L1 bridgeAsset transaction. + * This simulates calling bridgeAsset() on the PolygonZkEVMBridgeV2 contract + * and captures all relevant data including VALID Merkle proofs. + * + * Run with: forge test -vv --match-contract ClaimAssetTestVectorsLocalTx + * + * The output can be used to verify Miden's ability to process L1 bridge transactions. + */ +contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2 { + /** + * @notice Generates bridge asset test vectors with VALID Merkle proofs. + * Simulates a user calling bridgeAsset() to bridge tokens from L1 to Miden. + * + * Output file: test-vectors/bridge_asset_vectors.json + */ + function test_generateClaimAssetVectorsLocalTx() public { + string memory obj = "root"; + + // ====== BRIDGE TRANSACTION PARAMETERS ====== + + uint8 leafType = 0; + uint32 originNetwork = 0; + address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; + uint32 destinationNetwork = 20; + address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; + uint256 amount = 1000000000000000; + + bytes memory metadata = abi.encode("Test Token", "TEST", uint8(18)); + bytes32 metadataHash = keccak256(metadata); + + // ====== COMPUTE LEAF VALUE AND ADD TO TREE ====== + + bytes32 leafValue = getLeafValue( + leafType, originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadataHash + ); + + // Add the leaf to the deposit tree to generate valid Merkle proof + _addLeaf(leafValue); + + // Get the deposit count (leaf index) - depositCount is uint256 in DepositContractBase + uint256 depositCountValue = uint256(depositCount); + + // Get the local exit root (root of the deposit tree) + bytes32 localExitRoot = getRoot(); + + // ====== GENERATE MERKLE PROOF ====== + + // Generate canonical zeros for the Merkle proof + bytes32[32] memory canonicalZeros = _computeCanonicalZeros(); + + // Build the Merkle proof from _branch array and canonical zeros + // The leaf index is depositCountValue - 1 (0-indexed) + uint256 leafIndex = depositCountValue - 1; + bytes32[32] memory smtProofLocal = _generateLocalProof(leafIndex, canonicalZeros); + + // For mainnet deposits, the rollup proof is all zeros + bytes32[32] memory smtProofRollup; + for (uint256 i = 0; i < 32; i++) { + smtProofRollup[i] = bytes32(0); + } + + // ====== COMPUTE EXIT ROOTS ====== + + // For a simulated L1 bridge transaction: + // - mainnetExitRoot is the local exit root from the deposit tree + // - rollupExitRoot is simulated (deterministic for reproducibility) + bytes32 mainnetExitRoot = localExitRoot; + bytes32 rollupExitRoot = keccak256(abi.encodePacked("rollup_exit_root_simulated")); + + // Compute global exit root + bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoot, rollupExitRoot); + + // ====== VERIFY MERKLE PROOF ====== + + // Verify that the generated proof is valid + require( + this.verifyMerkleProof(leafValue, smtProofLocal, uint32(leafIndex), mainnetExitRoot), + "Generated Merkle proof is invalid!" + ); + + // ====== COMPUTE GLOBAL INDEX ====== + + // Global index for mainnet deposits: (1 << 64) | leafIndex + // Note: leafIndex is 0-based (depositCount - 1), matching how the bridge contract + // extracts it via uint32(globalIndex) in _verifyLeaf() + uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex); + + // ====== SERIALIZE SMT PROOFS ====== + _serializeProofs(obj, smtProofLocal, smtProofRollup); + + // Scoped block 2: Serialize transaction parameters + { + vm.serializeUint(obj, "leaf_type", leafType); + vm.serializeUint(obj, "origin_network", originNetwork); + vm.serializeAddress(obj, "origin_token_address", originTokenAddress); + vm.serializeUint(obj, "destination_network", destinationNetwork); + vm.serializeAddress(obj, "destination_address", destinationAddress); + vm.serializeUint(obj, "amount", amount); + vm.serializeBytes(obj, "metadata", metadata); + vm.serializeBytes32(obj, "metadata_hash", metadataHash); + vm.serializeBytes32(obj, "leaf_value", leafValue); + } + + // Scoped block 3: Serialize state, exit roots, and finalize + { + vm.serializeUint(obj, "deposit_count", depositCountValue); + vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); + vm.serializeBytes32(obj, "local_exit_root", localExitRoot); + vm.serializeBytes32(obj, "mainnet_exit_root", mainnetExitRoot); + vm.serializeBytes32(obj, "rollup_exit_root", rollupExitRoot); + vm.serializeBytes32(obj, "global_exit_root", globalExitRoot); + + string memory json = vm.serializeString( + obj, "description", "L1 bridgeAsset transaction test vectors with valid Merkle proofs" + ); + + string memory outputPath = "test-vectors/claim_asset_vectors_local_tx.json"; + vm.writeJson(json, outputPath); + + console.log("Generated claim asset local tx test vectors with valid Merkle proofs"); + console.log("Output file:", outputPath); + console.log("Leaf index:", leafIndex); + console.log("Deposit count:", depositCountValue); + } + } + + /** + * @notice Computes the canonical zero hashes for the Sparse Merkle Tree. + * @dev Each level i has zero hash: keccak256(zero[i-1], zero[i-1]) + * @return canonicalZeros Array of 32 zero hashes, one per tree level + */ + function _computeCanonicalZeros() internal pure returns (bytes32[32] memory canonicalZeros) { + bytes32 current = bytes32(0); + for (uint256 i = 0; i < 32; i++) { + canonicalZeros[i] = current; + current = keccak256(abi.encodePacked(current, current)); + } + } + + /** + * @notice Generates the SMT proof for the local exit root. + * @dev For each level i: + * - If bit i of leafIndex is 1: use _branch[i] (sibling on left) + * - If bit i of leafIndex is 0: use canonicalZeros[i] (sibling on right) + * @param leafIndex The 0-indexed position of the leaf in the tree + * @param canonicalZeros The precomputed canonical zero hashes + * @return smtProofLocal The 32-element Merkle proof array + */ + function _generateLocalProof(uint256 leafIndex, bytes32[32] memory canonicalZeros) + internal + view + returns (bytes32[32] memory smtProofLocal) + { + for (uint256 i = 0; i < 32; i++) { + // Check if bit i of leafIndex is set + if ((leafIndex >> i) & 1 == 1) { + // Bit is 1: sibling is on the left, use _branch[i] + smtProofLocal[i] = _branch[i]; + } else { + // Bit is 0: sibling is on the right (or doesn't exist), use zero hash + smtProofLocal[i] = canonicalZeros[i]; + } + } + } + + /** + * @notice Helper function to serialize SMT proofs (avoids stack too deep) + * @param obj The JSON object key + * @param smtProofLocal The local exit root proof + * @param smtProofRollup The rollup exit root proof + */ + function _serializeProofs(string memory obj, bytes32[32] memory smtProofLocal, bytes32[32] memory smtProofRollup) + internal + { + bytes32[] memory smtProofLocalDyn = new bytes32[](32); + bytes32[] memory smtProofRollupDyn = new bytes32[](32); + for (uint256 i = 0; i < 32; i++) { + smtProofLocalDyn[i] = smtProofLocal[i]; + smtProofRollupDyn[i] = smtProofRollup[i]; + } + + vm.serializeBytes32(obj, "smt_proof_local_exit_root", smtProofLocalDyn); + vm.serializeBytes32(obj, "smt_proof_rollup_exit_root", smtProofRollupDyn); + } +} diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol similarity index 91% rename from crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol rename to crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol index 4352c232eb..0f4e56bb5b 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol @@ -6,17 +6,17 @@ import "@agglayer/v2/lib/DepositContractV2.sol"; import "@agglayer/lib/GlobalExitRootLib.sol"; /** - * @title ClaimAssetTestVectors + * @title ClaimAssetTestVectorsRealTx * @notice Test contract that generates comprehensive test vectors for verifying * compatibility between Solidity's claimAsset and Miden's implementation. * * Generates vectors for both LeafData and ProofData from a real transaction. * - * Run with: forge test -vv --match-contract ClaimAssetTestVectors + * Run with: forge test -vv --match-contract ClaimAssetTestVectorsRealTx * * The output can be compared against the Rust ClaimNoteStorage implementation. */ -contract ClaimAssetTestVectors is Test, DepositContractV2 { +contract ClaimAssetTestVectorsRealTx is Test, DepositContractV2 { /** * @notice Generates claim asset test vectors from real Katana transaction and saves to JSON. * Uses real transaction data from Katana explorer: @@ -68,7 +68,7 @@ contract ClaimAssetTestVectors is Test, DepositContractV2 { // forge-std JSON serialization supports `bytes32[]` but not `bytes32[32]`. bytes32[] memory smtProofLocalExitRootDyn = new bytes32[](32); - for (uint i = 0; i < 32; i++) { + for (uint256 i = 0; i < 32; i++) { smtProofLocalExitRootDyn[i] = smtProofLocalExitRoot[i]; } @@ -105,7 +105,8 @@ contract ClaimAssetTestVectors is Test, DepositContractV2 { // Original metadata from the transaction (ABI encoded: name, symbol, decimals) // name = "Vault Bridge ETH", symbol = "vbETH", decimals = 18 - bytes memory metadata = hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000105661756c7420427269646765204554480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000057662455448000000000000000000000000000000000000000000000000000000"; + bytes memory metadata = + hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000105661756c7420427269646765204554480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000057662455448000000000000000000000000000000000000000000000000000000"; bytes32 metadataHash = keccak256(metadata); // Compute the leaf value using the official DepositContractV2 implementation @@ -129,7 +130,7 @@ contract ClaimAssetTestVectors is Test, DepositContractV2 { string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); // Save to file - string memory outputPath = "test-vectors/claim_asset_vectors.json"; + string memory outputPath = "test-vectors/claim_asset_vectors_real_tx.json"; vm.writeJson(json, outputPath); } } diff --git a/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol b/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol index 9224231a00..b518e5de15 100644 --- a/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol @@ -8,14 +8,13 @@ import "@agglayer/lib/GlobalExitRootLib.sol"; * @title ExitRootsTestVectors * @notice Test contract that generates global exit root test vectors from * mainnet-rollup exit root pairs. - * + * * Run with: forge test -vv --match-contract ExitRootsTestVectors - * + * * The output can be compared against Rust implementations that compute * the global exit root as keccak256(mainnetExitRoot || rollupExitRoot). */ contract ExitRootsTestVectors is Test { - /** * @notice Generates global exit root vectors from mainnet-rollup pairs * and saves to JSON file. @@ -29,22 +28,19 @@ contract ExitRootsTestVectors is Test { // TX 2: 0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188 bytes32[] memory mainnetExitRoots = new bytes32[](2); bytes32[] memory rollupExitRoots = new bytes32[](2); - + // Pair 1 (TX: 0xe1a20811d757c48eba534f63041f58cd39eec762bfb6e4496dccf4e675fd5619) mainnetExitRoots[0] = bytes32(0x98c911b6dcface93fd0bb490d09390f2f7f9fcf36fc208cbb36528a229298326); rollupExitRoots[0] = bytes32(0x6a2533a24cc2a3feecf5c09b6a270bbb24a5e2ce02c18c0e26cd54c3dddc2d70); - + // Pair 2 (TX: 0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188) mainnetExitRoots[1] = bytes32(0xbb71d991caf89fe64878259a61ae8d0b4310c176e66d90fd2370b02573e80c90); rollupExitRoots[1] = bytes32(0xd9b546933b59acd388dc0c6520cbf2d4dbb9bac66f74f167ba70f221d82a440c); - + // Compute global exit roots bytes32[] memory globalExitRoots = new bytes32[](mainnetExitRoots.length); for (uint256 i = 0; i < mainnetExitRoots.length; i++) { - globalExitRoots[i] = GlobalExitRootLib.calculateGlobalExitRoot( - mainnetExitRoots[i], - rollupExitRoots[i] - ); + globalExitRoots[i] = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoots[i], rollupExitRoots[i]); } // Serialize parallel arrays to JSON diff --git a/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol index ab4fdf8443..3d39576a02 100644 --- a/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol @@ -31,18 +31,13 @@ contract LeafValueTestVectors is Test, DepositContractV2 { uint256 amount = 2000000000000000000; // 2e18 // Original metadata from the transaction (ABI encoded: name, symbol, decimals) - bytes memory metadata = hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000b4c756d696120546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054c554d4941000000000000000000000000000000000000000000000000000000"; + bytes memory metadata = + hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000b4c756d696120546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054c554d4941000000000000000000000000000000000000000000000000000000"; bytes32 metadataHash = keccak256(metadata); // Compute the leaf value using the official DepositContractV2 implementation bytes32 leafValue = getLeafValue( - leafType, - originNetwork, - originTokenAddress, - destinationNetwork, - destinationAddress, - amount, - metadataHash + leafType, originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadataHash ); // Serialize to JSON diff --git a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol index 1f6ab63d2c..b3b090b471 100644 --- a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol @@ -13,19 +13,18 @@ import "@agglayer/v2/lib/DepositContractV2.sol"; * bridge_out.masm uses (leafType=0, originNetwork=64, originTokenAddress=fixed random value, * metadataHash=0), parametrised by amount (i+1) and deterministic per-leaf * destination network/address values derived from a fixed seed. - * + * * Run with: forge test -vv --match-contract MMRTestVectors - * + * * The output can be compared against the Rust KeccakMmrFrontier32 implementation * in crates/miden-testing/tests/agglayer/mmr_frontier.rs */ contract MMRTestVectors is Test, DepositContractV2 { - // Constants matching bridge_out.masm hardcoded values - uint8 constant LEAF_TYPE = 0; - uint32 constant ORIGIN_NETWORK = 64; - address constant ORIGIN_TOKEN_ADDR = 0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891; - bytes32 constant METADATA_HASH = bytes32(0); + uint8 constant LEAF_TYPE = 0; + uint32 constant ORIGIN_NETWORK = 64; + address constant ORIGIN_TOKEN_ADDR = 0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891; + bytes32 constant METADATA_HASH = bytes32(0); // Fixed seed for deterministic "random" destination vectors. // Keeping this constant ensures everyone regenerates the exact same JSON vectors. @@ -35,19 +34,13 @@ contract MMRTestVectors is Test, DepositContractV2 { * @notice Builds a leaf hash identical to what bridge_out.masm would produce for the * given amount. */ - function _createLeaf( - uint256 amount, - uint32 destinationNetwork, - address destinationAddress - ) internal pure returns (bytes32) { + function _createLeaf(uint256 amount, uint32 destinationNetwork, address destinationAddress) + internal + pure + returns (bytes32) + { return getLeafValue( - LEAF_TYPE, - ORIGIN_NETWORK, - ORIGIN_TOKEN_ADDR, - destinationNetwork, - destinationAddress, - amount, - METADATA_HASH + LEAF_TYPE, ORIGIN_NETWORK, ORIGIN_TOKEN_ADDR, destinationNetwork, destinationAddress, amount, METADATA_HASH ); } @@ -68,7 +61,7 @@ contract MMRTestVectors is Test, DepositContractV2 { */ function test_generateCanonicalZeros() public { bytes32[] memory zeros = new bytes32[](32); - + bytes32 z = bytes32(0); for (uint256 i = 0; i < 32; i++) { zeros[i] = z; @@ -77,13 +70,13 @@ contract MMRTestVectors is Test, DepositContractV2 { // Foundry serializes bytes32[] to a JSON array automatically string memory json = vm.serializeBytes32("root", "canonical_zeros", zeros); - + // Save to file string memory outputPath = "test-vectors/canonical_zeros.json"; vm.writeJson(json, outputPath); console.log("Saved canonical zeros to:", outputPath); } - + /** * @notice Generates MMR frontier vectors (leaf-root pairs) and saves to JSON file. * Each leaf is created via _createLeaf(i+1, network[i], address[i]) so that: diff --git a/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol index 5867414ec6..94e7586900 100644 --- a/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol @@ -7,17 +7,16 @@ import "@agglayer/v2/lib/DepositContractBase.sol"; /** * @title SMTMerkleProofVectors * @notice Test contract that generates test vectors for Merkle proofs verification. - * + * * Run with: forge test -vv --match-contract SMTMerkleProofVectors - * + * * The output can be used during the bridge-in tests in * crates/miden-testing/tests/agglayer/bridge_in.rs */ contract SMTMerkleProofVectors is Test, DepositContractBase { - /** * @notice Generates vectors of leaves, roots and merkle paths and saves them to the JSON. - * Notice that each value in the leaves/roots array corresponds to 32 values in the + * Notice that each value in the leaves/roots array corresponds to 32 values in the * merkle paths array. */ function test_generateVerificationProofData() public { @@ -30,7 +29,7 @@ contract SMTMerkleProofVectors is Test, DepositContractBase { // This is a work around which allows to provide the merkle path to the verifyMerkleProof // function, since the merkle_paths array cannot be sliced. bytes32[32] memory current_path; - + // generate canonical zeros array bytes32 z = bytes32(0); for (uint256 i = 0; i < 32; i++) { @@ -49,8 +48,8 @@ contract SMTMerkleProofVectors is Test, DepositContractBase { // first update the merkle_paths array and only after that actually add a leaf and // recompute the _branch. // - // Merkle paths in the _branch array contain plain zeros for the nodes which were not - // updated during the leaf insertion. To get the proper Merkle path we should use + // Merkle paths in the _branch array contain plain zeros for the nodes which were not + // updated during the leaf insertion. To get the proper Merkle path we should use // canonical zeros instead. for (uint256 j = 0; j < 32; j++) { if (i >> j & 1 == 1) { diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 9c29812ab2..24d36f148c 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -2,7 +2,6 @@ extern crate alloc; use miden_agglayer::{ ClaimNoteStorage, - EthAddressFormat, OutputNoteData, UpdateGerNote, create_claim_note, @@ -19,19 +18,35 @@ use miden_standards::account::wallets::BasicWallet; use miden_testing::{AccountState, Auth, MockChain}; use rand::Rng; -use super::test_utils::real_claim_data; +use super::test_utils::{local_claim_data, real_claim_data}; -/// Tests the bridge-in flow using real claim data: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID -/// note created. +/// Identifies the source of claim data used in the bridge-in test. +#[derive(Debug, Clone, Copy)] +enum ClaimDataSource { + /// Real on-chain claimAsset data from claim_asset_vectors_real_tx.json.json. + Real, + /// Locally simulated bridgeAsset data from claim_asset_vectors_local_tx.json. + Simulated, +} + +/// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created. /// -/// This test uses real ProofData and LeafData deserialized from claim_asset_vectors.json. -/// The claim note is processed against the agglayer faucet, which validates the Merkle proof -/// and creates a P2ID note for the destination address. +/// Parameterized over two claim data sources: +/// - [`ClaimDataSource::Real`]: uses real [`ProofData`] and [`LeafData`] from +/// `claim_asset_vectors_real_tx.json`, captured from an actual on-chain `claimAsset` transaction. +/// - [`ClaimDataSource::Simulated`]: uses locally generated [`ProofData`] and [`LeafData`] from +/// `claim_asset_vectors_local_tx.json`, produced by simulating a `bridgeAsset()` call. /// -/// Note: Modifying anything in the test vectors would invalidate the Merkle proof, -/// as the proof was computed for the original leaf_data including the original destination. +/// In both cases the claim note is processed against the agglayer faucet, which validates the +/// Merkle proof and creates a P2ID note for the destination address. +/// +/// Note: Modifying anything in the real test vectors would invalidate the Merkle proof, +/// as the proof was computed for the original leaf data including the original destination. +#[rstest::rstest] +#[case::real(ClaimDataSource::Real)] +#[case::simulated(ClaimDataSource::Simulated)] #[tokio::test] -async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { +async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { let mut builder = MockChain::builder(); // CREATE BRIDGE ACCOUNT (with bridge_out component for MMR validation) @@ -40,16 +55,23 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let bridge_account = create_existing_bridge_account(bridge_seed); builder.add_account(bridge_account.clone())?; + // GET CLAIM DATA FROM JSON (source depends on the test case) + // -------------------------------------------------------------------------------------------- + let (proof_data, leaf_data, ger) = match data_source { + ClaimDataSource::Real => real_claim_data(), + ClaimDataSource::Simulated => local_claim_data(), + }; + // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) + // Use the origin token address and network from the claim data. // -------------------------------------------------------------------------------------------- let token_symbol = "AGG"; let decimals = 8u8; let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let agglayer_faucet_seed = builder.rng_mut().draw_word(); - // Origin token address for the faucet's conversion metadata - let origin_token_address = EthAddressFormat::new([0u8; 20]); - let origin_network = 0u32; + let origin_token_address = leaf_data.origin_token_address; + let origin_network = leaf_data.origin_network; let scale = 0u8; let agglayer_faucet = create_existing_agglayer_faucet( @@ -65,11 +87,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { ); builder.add_account(agglayer_faucet.clone())?; - // GET REAL CLAIM DATA FROM JSON - // -------------------------------------------------------------------------------------------- - let (proof_data, leaf_data, ger) = real_claim_data(); - - // Get the destination account ID from the leaf data + // Get the destination account ID from the leaf data. // This requires the destination_address to be in the embedded Miden AccountId format // (first 4 bytes must be zero). let destination_account_id = leaf_data @@ -87,7 +105,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { AccountState::Exists, )?; - // CREATE CLAIM NOTE WITH REAL PROOF DATA AND LEAF DATA + // CREATE CLAIM NOTE // -------------------------------------------------------------------------------------------- // Generate a serial number for the P2ID note @@ -151,7 +169,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // Note: We intentionally do NOT verify the exact note ID or asset amount here because // the scale_u256_to_native_amount function is currently a TODO stub that doesn't perform // proper u256-to-native scaling. The test verifies that the bridge-in flow correctly - // validates the Merkle proof using real cryptographic proof data and creates an output note. + // validates the Merkle proof and creates an output note. // // TODO: Once scale_u256_to_native_amount is properly implemented, add: // - Verification that the minted amount matches the expected scaled value diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 39bc77c917..b67cff187b 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -29,8 +29,14 @@ use serde::Deserialize; /// Claim asset test vectors JSON — contains both LeafData and ProofData from a real claimAsset /// transaction. -const CLAIM_ASSET_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json"); +const CLAIM_ASSET_VECTORS_JSON: &str = include_str!( + "../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_real_tx.json" +); + +/// Bridge asset test vectors JSON — contains test data for an L1 bridgeAsset transaction. +const BRIDGE_ASSET_VECTORS_JSON: &str = include_str!( + "../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json" +); /// Leaf data test vectors JSON from the Foundry-generated file. pub const LEAF_VALUE_VECTORS_JSON: &str = @@ -224,6 +230,12 @@ pub static CLAIM_ASSET_VECTOR: LazyLock = LazyLock::new(|| { .expect("failed to parse claim asset vectors JSON") }); +/// Lazily parsed bridge asset test vector from the JSON file (locally simulated L1 transaction). +pub static CLAIM_ASSET_VECTOR_LOCAL: LazyLock = LazyLock::new(|| { + serde_json::from_str(BRIDGE_ASSET_VECTORS_JSON) + .expect("failed to parse bridge asset vectors JSON") +}); + /// Lazily parsed Merkle proof vectors from the JSON file. pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = LazyLock::new(|| { @@ -247,7 +259,8 @@ pub static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = Laz /// Returns real claim data from the claim_asset_vectors.json file. /// -/// Returns a tuple of (ProofData, LeafData) parsed from the real on-chain claim transaction. +/// Returns a tuple of (ProofData, LeafData, ExitRoot) parsed from the real on-chain claim +/// transaction. pub fn real_claim_data() -> (ProofData, LeafData, ExitRoot) { let vector = &*CLAIM_ASSET_VECTOR; let ger = ExitRoot::new( @@ -256,6 +269,18 @@ pub fn real_claim_data() -> (ProofData, LeafData, ExitRoot) { (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) } +/// Returns simulated bridge asset data from the bridge_asset_vectors.json file. +/// +/// Returns a tuple of (ProofData, LeafData, ExitRoot) from a locally simulated L1 bridgeAsset +/// transaction. This data represents what would be generated when a user calls bridgeAsset() on L1. +pub fn local_claim_data() -> (ProofData, LeafData, ExitRoot) { + let vector = &*CLAIM_ASSET_VECTOR_LOCAL; + let ger = ExitRoot::new( + hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), + ); + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) +} + /// Execute a program with a default host and optional advice inputs. pub async fn execute_program_with_default_host( program: Program, From 1531d50feaead8ba75c54de9d1729a36cf4b8a34 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 23 Feb 2026 10:56:56 +0100 Subject: [PATCH 13/21] refactor: address review nits from PR #2474 (#2489) * refactor: address review nits from PR #2474 - Add `ClaimDataSource::get_data()` method so the match is encapsulated in the enum rather than inlined in the test body - Extract `claim_data_from_vector()` helper to deduplicate the shared logic between `real_claim_data()` and `local_claim_data()` - Fix `.json.json` double-extension typo in `ClaimDataSource::Real` doc - Extract `_computeCanonicalZeros()` and `_generateLocalProof()` helpers in `SMTMerkleProofVectors.t.sol`, mirroring `ClaimAssetTestVectorsLocalTx`, and replace the inline loop with calls to these helpers Co-Authored-By: Claude Sonnet 4.6 * refactor: address follow-up review comments on PR #2489 - Move `ClaimDataSource` into `test_utils.rs` so its `get_data()` method can select the right lazy static directly, removing the now-unnecessary `real_claim_data()` and `local_claim_data()` public helpers - Extract `_computeCanonicalZeros` and `_generateLocalProof` into a new `DepositContractTestHelpers` abstract contract; both `SMTMerkleProofVectors` and `ClaimAssetTestVectorsLocalTx` now inherit from it instead of each defining their own copies Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../test/ClaimAssetTestVectorsLocalTx.t.sol | 42 +---------------- .../test/DepositContractTestHelpers.sol | 47 +++++++++++++++++++ .../test/SMTMerkleProofVectors.t.sol | 31 ++++-------- .../miden-testing/tests/agglayer/bridge_in.rs | 16 +------ .../tests/agglayer/test_utils.rs | 39 ++++++++------- 5 files changed, 78 insertions(+), 97 deletions(-) create mode 100644 crates/miden-agglayer/solidity-compat/test/DepositContractTestHelpers.sol diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol index 1608951319..3f0df2a344 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "@agglayer/v2/lib/DepositContractV2.sol"; import "@agglayer/lib/GlobalExitRootLib.sol"; +import "./DepositContractTestHelpers.sol"; /** * @title ClaimAssetTestVectorsLocalTx @@ -15,7 +16,7 @@ import "@agglayer/lib/GlobalExitRootLib.sol"; * * The output can be used to verify Miden's ability to process L1 bridge transactions. */ -contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2 { +contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContractTestHelpers { /** * @notice Generates bridge asset test vectors with VALID Merkle proofs. * Simulates a user calling bridgeAsset() to bridge tokens from L1 to Miden. @@ -133,45 +134,6 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2 { } } - /** - * @notice Computes the canonical zero hashes for the Sparse Merkle Tree. - * @dev Each level i has zero hash: keccak256(zero[i-1], zero[i-1]) - * @return canonicalZeros Array of 32 zero hashes, one per tree level - */ - function _computeCanonicalZeros() internal pure returns (bytes32[32] memory canonicalZeros) { - bytes32 current = bytes32(0); - for (uint256 i = 0; i < 32; i++) { - canonicalZeros[i] = current; - current = keccak256(abi.encodePacked(current, current)); - } - } - - /** - * @notice Generates the SMT proof for the local exit root. - * @dev For each level i: - * - If bit i of leafIndex is 1: use _branch[i] (sibling on left) - * - If bit i of leafIndex is 0: use canonicalZeros[i] (sibling on right) - * @param leafIndex The 0-indexed position of the leaf in the tree - * @param canonicalZeros The precomputed canonical zero hashes - * @return smtProofLocal The 32-element Merkle proof array - */ - function _generateLocalProof(uint256 leafIndex, bytes32[32] memory canonicalZeros) - internal - view - returns (bytes32[32] memory smtProofLocal) - { - for (uint256 i = 0; i < 32; i++) { - // Check if bit i of leafIndex is set - if ((leafIndex >> i) & 1 == 1) { - // Bit is 1: sibling is on the left, use _branch[i] - smtProofLocal[i] = _branch[i]; - } else { - // Bit is 0: sibling is on the right (or doesn't exist), use zero hash - smtProofLocal[i] = canonicalZeros[i]; - } - } - } - /** * @notice Helper function to serialize SMT proofs (avoids stack too deep) * @param obj The JSON object key diff --git a/crates/miden-agglayer/solidity-compat/test/DepositContractTestHelpers.sol b/crates/miden-agglayer/solidity-compat/test/DepositContractTestHelpers.sol new file mode 100644 index 0000000000..997349c05d --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/DepositContractTestHelpers.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@agglayer/v2/lib/DepositContractBase.sol"; + +/** + * @title DepositContractTestHelpers + * @notice Shared helpers for Sparse Merkle Tree test vector generation. + * Inherited by SMTMerkleProofVectors and ClaimAssetTestVectorsLocalTx. + */ +abstract contract DepositContractTestHelpers is DepositContractBase { + /** + * @notice Computes the canonical zero hashes for the Sparse Merkle Tree. + * @dev Each level i has zero hash: keccak256(zero[i-1], zero[i-1]) + * @return canonicalZeros Array of 32 zero hashes, one per tree level + */ + function _computeCanonicalZeros() internal pure returns (bytes32[32] memory canonicalZeros) { + bytes32 current = bytes32(0); + for (uint256 i = 0; i < 32; i++) { + canonicalZeros[i] = current; + current = keccak256(abi.encodePacked(current, current)); + } + } + + /** + * @notice Generates the SMT proof for a given leaf index using the current _branch state. + * @dev For each level i: + * - If bit i of leafIndex is 1: use _branch[i] (sibling on left) + * - If bit i of leafIndex is 0: use canonicalZeros[i] (sibling on right) + * @param leafIndex The 0-indexed position of the leaf in the tree + * @param canonicalZeros The precomputed canonical zero hashes + * @return smtProof The 32-element Merkle proof array + */ + function _generateLocalProof(uint256 leafIndex, bytes32[32] memory canonicalZeros) + internal + view + returns (bytes32[32] memory smtProof) + { + for (uint256 i = 0; i < 32; i++) { + if ((leafIndex >> i) & 1 == 1) { + smtProof[i] = _branch[i]; + } else { + smtProof[i] = canonicalZeros[i]; + } + } + } +} diff --git a/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol index 94e7586900..e6b466e521 100644 --- a/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "@agglayer/v2/lib/DepositContractBase.sol"; +import "./DepositContractTestHelpers.sol"; /** * @title SMTMerkleProofVectors @@ -13,7 +13,7 @@ import "@agglayer/v2/lib/DepositContractBase.sol"; * The output can be used during the bridge-in tests in * crates/miden-testing/tests/agglayer/bridge_in.rs */ -contract SMTMerkleProofVectors is Test, DepositContractBase { +contract SMTMerkleProofVectors is Test, DepositContractTestHelpers { /** * @notice Generates vectors of leaves, roots and merkle paths and saves them to the JSON. * Notice that each value in the leaves/roots array corresponds to 32 values in the @@ -23,19 +23,13 @@ contract SMTMerkleProofVectors is Test, DepositContractBase { bytes32[] memory leaves = new bytes32[](32); bytes32[] memory roots = new bytes32[](32); bytes32[] memory merkle_paths = new bytes32[](1024); - bytes32[] memory canonical_zeros = new bytes32[](32); - // This array represent a merkle path during each iteration. - // This is a work around which allows to provide the merkle path to the verifyMerkleProof - // function, since the merkle_paths array cannot be sliced. + // This array represents a merkle path during each iteration. + // This is a workaround which allows to provide the merkle path to verifyMerkleProof + // since the merkle_paths array cannot be sliced. bytes32[32] memory current_path; - // generate canonical zeros array - bytes32 z = bytes32(0); - for (uint256 i = 0; i < 32; i++) { - canonical_zeros[i] = z; - z = keccak256(abi.encodePacked(z, z)); - } + bytes32[32] memory canonicalZeros = _computeCanonicalZeros(); // generate leaves, roots, and merkle_paths arrays for (uint256 i = 0; i < 32; i++) { @@ -47,18 +41,9 @@ contract SMTMerkleProofVectors is Test, DepositContractBase { // the overall number of leaves in the SMT instead of the index of the last leaf), so we // first update the merkle_paths array and only after that actually add a leaf and // recompute the _branch. - // - // Merkle paths in the _branch array contain plain zeros for the nodes which were not - // updated during the leaf insertion. To get the proper Merkle path we should use - // canonical zeros instead. + current_path = _generateLocalProof(i, canonicalZeros); for (uint256 j = 0; j < 32; j++) { - if (i >> j & 1 == 1) { - merkle_paths[i * 32 + j] = _branch[j]; - current_path[j] = _branch[j]; - } else { - merkle_paths[i * 32 + j] = canonical_zeros[j]; - current_path[j] = canonical_zeros[j]; - } + merkle_paths[i * 32 + j] = current_path[j]; } _addLeaf(leaf); diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 24d36f148c..8a9cc4c16b 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -18,16 +18,7 @@ use miden_standards::account::wallets::BasicWallet; use miden_testing::{AccountState, Auth, MockChain}; use rand::Rng; -use super::test_utils::{local_claim_data, real_claim_data}; - -/// Identifies the source of claim data used in the bridge-in test. -#[derive(Debug, Clone, Copy)] -enum ClaimDataSource { - /// Real on-chain claimAsset data from claim_asset_vectors_real_tx.json.json. - Real, - /// Locally simulated bridgeAsset data from claim_asset_vectors_local_tx.json. - Simulated, -} +use super::test_utils::ClaimDataSource; /// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created. /// @@ -57,10 +48,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // GET CLAIM DATA FROM JSON (source depends on the test case) // -------------------------------------------------------------------------------------------- - let (proof_data, leaf_data, ger) = match data_source { - ClaimDataSource::Real => real_claim_data(), - ClaimDataSource::Simulated => local_claim_data(), - }; + let (proof_data, leaf_data, ger) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) // Use the origin token address and network from the claim data. diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index b67cff187b..901ed3abde 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -257,28 +257,27 @@ pub static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = Laz // HELPER FUNCTIONS // ================================================================================================ -/// Returns real claim data from the claim_asset_vectors.json file. -/// -/// Returns a tuple of (ProofData, LeafData, ExitRoot) parsed from the real on-chain claim -/// transaction. -pub fn real_claim_data() -> (ProofData, LeafData, ExitRoot) { - let vector = &*CLAIM_ASSET_VECTOR; - let ger = ExitRoot::new( - hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), - ); - (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) +/// Identifies the source of claim data used in bridge-in tests. +#[derive(Debug, Clone, Copy)] +pub enum ClaimDataSource { + /// Real on-chain claimAsset data from claim_asset_vectors_real_tx.json. + Real, + /// Locally simulated bridgeAsset data from claim_asset_vectors_local_tx.json. + Simulated, } -/// Returns simulated bridge asset data from the bridge_asset_vectors.json file. -/// -/// Returns a tuple of (ProofData, LeafData, ExitRoot) from a locally simulated L1 bridgeAsset -/// transaction. This data represents what would be generated when a user calls bridgeAsset() on L1. -pub fn local_claim_data() -> (ProofData, LeafData, ExitRoot) { - let vector = &*CLAIM_ASSET_VECTOR_LOCAL; - let ger = ExitRoot::new( - hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), - ); - (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) +impl ClaimDataSource { + /// Returns the `(ProofData, LeafData, ExitRoot)` tuple for this data source. + pub fn get_data(self) -> (ProofData, LeafData, ExitRoot) { + let vector = match self { + ClaimDataSource::Real => &*CLAIM_ASSET_VECTOR, + ClaimDataSource::Simulated => &*CLAIM_ASSET_VECTOR_LOCAL, + }; + let ger = ExitRoot::new( + hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), + ); + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) + } } /// Execute a program with a default host and optional advice inputs. From 9cfb4f2aff3d6f3cb25ac46dfe992e187d6f4615 Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:57:49 +0300 Subject: [PATCH 14/21] feat: add native asset claim amount as element in `NoteStorage` of `CLAIM` note (#2460) * feat: add native asset claim amount as element in NoteStorage of CLAIM note * feat: add output note assertion checks * refactor: rm debug statement * Update crates/miden-testing/tests/agglayer/bridge_in.rs Co-authored-by: Marti * refactor: cleanup test & rm leftover comments * wip: consume P2ID by destination AccountId * feat: add solidity-compat test for generating local claimAsset() param data * feat: add solidity-compat test for generating local claimAsset() param data * feat: consume output P2ID note from CLAIM note * fix: taplo fmt * refactor: rename solidity compat test files & claimAsset vector JSON files * refactor: rename testing methods & update test comments * Update crates/miden-testing/tests/agglayer/bridge_in.rs * Update crates/miden-testing/tests/agglayer/bridge_in.rs * refactor: add warnings in comments & update TODO regarding u256 scaling * refactor: assert mock destination account ID matches claim data * fix: load amount BE-felt from mem --------- Co-authored-by: Marti --- .../asm/bridge/agglayer_faucet.masm | 45 ++++--- .../asm/bridge/asset_conversion.masm | 5 - .../asm/note_scripts/CLAIM.masm | 7 +- .../claim_asset_vectors_local_tx.json | 10 +- .../test/ClaimAssetTestVectorsLocalTx.t.sol | 2 +- crates/miden-agglayer/src/claim_note.rs | 8 +- crates/miden-testing/Cargo.toml | 2 +- crates/miden-testing/src/utils.rs | 23 +++- .../miden-testing/tests/agglayer/bridge_in.rs | 110 ++++++++++++++++-- crates/miden-testing/tests/scripts/faucet.rs | 2 +- crates/miden-testing/tests/scripts/swap.rs | 23 +--- 11 files changed, 172 insertions(+), 65 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index 7b7bd43e93..655a00190e 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -40,6 +40,7 @@ const CLAIM_NOTE_DATA_MEM_ADDR = 712 const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 const OUTPUT_NOTE_TAG_MEM_ADDR = 574 +const OUTPUT_NOTE_FAUCET_AMOUNT = 575 const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 549 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 550 @@ -57,8 +58,8 @@ const DESTINATION_ADDRESS_3 = 547 const DESTINATION_ADDRESS_4 = 548 # Memory locals in claim -const CLAIM_PREFIX_MEM_LOC = 5 -const CLAIM_SUFFIX_MEM_LOC = 6 +const CLAIM_PREFIX_MEM_LOC = 8 +const CLAIM_SUFFIX_MEM_LOC = 9 const CLAIM_AMOUNT_MEM_LOC_0 = 0 const CLAIM_AMOUNT_MEM_LOC_1 = 4 @@ -94,6 +95,7 @@ pub proc get_origin_token_address push.CONVERSION_INFO_1_SLOT[0..2] exec.active_account::get_item # => [addr3, addr2, addr1, addr0] + exec.word::reverse # => [addr0, addr1, addr2, addr3] @@ -224,14 +226,14 @@ end # Inputs: [] # Outputs: [U256[0], U256[1]] proc get_raw_claim_amount - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 - mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 end # Inputs: [U256[0], U256[1]] @@ -298,6 +300,12 @@ end #! Inputs: [prefix, suffix, AMOUNT[0], AMOUNT[1]] #! Outputs: [] #! +#! WARNING: This procedure currently assumes the claim amount fits within 128 bits (i.e. AMOUNT[1] +#! is all zeros). This assumption holds for all practical token amounts but is not explicitly +#! enforced here. See the TODO below. +#! +#! TODO: Add an explicit assertion that AMOUNT[1] is zero. +#! #! Note: This procedure will be refactored in a follow-up to use leaf data to build the output note. @locals(8) proc build_p2id_output_note @@ -330,10 +338,16 @@ proc build_p2id_output_note mem_load.OUTPUT_NOTE_TAG_MEM_ADDR # => [tag, RECIPIENT] - # TODO: implement scale down logic; stubbed out for now padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 # => [AMOUNT[0], AMOUNT[1], tag, note_type, RECIPIENT] - exec.asset_conversion::verify_u256_to_native_amount_conversion_stubbed + + mem_load.OUTPUT_NOTE_FAUCET_AMOUNT movdn.8 + # => [AMOUNT[0], AMOUNT[1], native_amount, tag, note_type, RECIPIENT] + + exec.get_scale movdn.8 + # => [AMOUNT[0], AMOUNT[1], scale, native_amount, tag, note_type, RECIPIENT] + + exec.asset_conversion::verify_u256_to_native_amount_conversion # => [amount, tag, note_type, RECIPIENT] exec.faucets::distribute @@ -345,6 +359,9 @@ end #! This procedure validates the rollup exit root Merkle Proof via FPI against the agglayer bridge, #! and if validation passes, mints the asset and creates an output note for the recipient. #! +#! WARNING: The EVM claim asset amount is currently assumed to fit within 128 bits. See the WARNING in +#! build_p2id_output_note for details. +#! #! TODO: Expand this description to cover the double-spend protection mechanism in detail. #! Double-spend can be prevented in two ways: #! 1) While it's possible to create two identical P2ID notes, only one can actually be consumed. @@ -403,8 +420,8 @@ pub proc claim # VALIDATE CLAIM mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR # => [PROOF_DATA_KEY, pad(12)] - swapw - mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR + + swapw mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR # => [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)] # Errors on invalid proof @@ -414,8 +431,10 @@ pub proc claim # Create P2ID output note loc_loadw_be.CLAIM_AMOUNT_MEM_LOC_1 swapw loc_loadw_be.CLAIM_AMOUNT_MEM_LOC_0 # => [AMOUNT[0], AMOUNT[1], pad(8)] + loc_load.CLAIM_SUFFIX_MEM_LOC loc_load.CLAIM_PREFIX_MEM_LOC # => [prefix, suffix, AMOUNT[0], AMOUNT[1], pad(8)] + exec.build_p2id_output_note # => [pad(16)] end diff --git a/crates/miden-agglayer/asm/bridge/asset_conversion.masm b/crates/miden-agglayer/asm/bridge/asset_conversion.masm index c3e6201f03..0e77613fff 100644 --- a/crates/miden-agglayer/asm/bridge/asset_conversion.masm +++ b/crates/miden-agglayer/asm/bridge/asset_conversion.masm @@ -346,8 +346,3 @@ pub proc verify_u256_to_native_amount_conversion assert.err=ERR_REMAINDER_TOO_LARGE # => [y] end - -# TODO: Rm & use verify_u256_to_native_amount_conversion -pub proc verify_u256_to_native_amount_conversion_stubbed - repeat.7 drop end -end \ No newline at end of file diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index c682993550..dc705c4dc6 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -83,7 +83,7 @@ end #! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) #! target_faucet_account_id[2], // Target faucet account ID (2 felts, prefix and suffix) #! output_note_tag[1], // P2ID output note tag -#! padding[1], // padding (1 felt) +#! miden_claim_amount[1], // Miden claim amount (1 felt) #! ] #! #! Invocation: exec @@ -164,7 +164,7 @@ end #! - output_p2id_serial_num [568..571]: 4 felts #! - target_faucet_account_id [572..573]: 2 felts #! - output_note_tag [574] : 1 felt -#! - padding [575] : 1 felt +#! - miden_claim_amount [575] : 1 felt #! #! Where: #! - smtProofLocalExitRoot: SMT proof for local exit root (bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) @@ -188,7 +188,8 @@ end #! - target_faucet_account_id: Target agglayer faucet account ID (prefix and suffix). Only this specific #! account can consume the note - any other account will cause a panic. #! - output_note_tag: P2ID output note tag -#! - padding (1 felt) +#! - miden_claim_amount: Scaled-down Miden token amount (Felt). This is the Y value computed from +#! scaling down the Ethereum amount (X) by the scale exponent: Y = floor(X / 10^scale_exp) #! #! Panics if: #! - account does not expose claim procedure. diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json index 612ac6a3b9..3bf850e8e0 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json @@ -1,15 +1,15 @@ { - "amount": 1000000000000000, + "amount": 100000000000000000000, "deposit_count": 1, "description": "L1 bridgeAsset transaction test vectors with valid Merkle proofs", "destination_address": "0x00000000AA0000000000bb000000cc000000Dd00", "destination_network": 20, - "global_exit_root": "0xbb7f9cbe48190e8859cbaecc54c8957863c786862bde7cff62df0a8b5adc19bb", + "global_exit_root": "0xc84f1e3744c151b345a8899034b3677c0fdbaf45aa3aaf18a3f97dbcf70836cb", "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", "leaf_type": 0, - "leaf_value": "0x8e5f0c4b8526561e30a89f900185ec681cc620fe3cadd8b281fb929063bd27ac", - "local_exit_root": "0x3e9a88b19d477b03175f9db12ba6b20eb33c0fe7a271667031685f034caf5af6", - "mainnet_exit_root": "0x3e9a88b19d477b03175f9db12ba6b20eb33c0fe7a271667031685f034caf5af6", + "leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d", + "local_exit_root": "0xc9e095ea4cfe19b7e9a6d1aff6c55914ccc8df34954f9f6a2ad8e42d2632a0ab", + "mainnet_exit_root": "0xc9e095ea4cfe19b7e9a6d1aff6c55914ccc8df34954f9f6a2ad8e42d2632a0ab", "metadata": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a5465737420546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045445535400000000000000000000000000000000000000000000000000000000", "metadata_hash": "0x4d0d9fb7f9ab2f012da088dc1c228173723db7e09147fe4fea2657849d580161", "origin_network": 0, diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol index 3f0df2a344..64517d8580 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol @@ -33,7 +33,7 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContrac address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; uint32 destinationNetwork = 20; address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; - uint256 amount = 1000000000000000; + uint256 amount = 100000000000000000000; bytes memory metadata = abi.encode("Test Token", "TEST", uint8(18)); bytes32 metadataHash = keccak256(metadata); diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index bc1108f74d..b90220927d 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -181,12 +181,14 @@ pub struct OutputNoteData { pub target_faucet_account_id: AccountId, /// P2ID output note tag pub output_note_tag: NoteTag, + /// Miden claim amount (scaled-down token amount as Felt) + pub miden_claim_amount: Felt, } impl OutputNoteData { /// Converts the output note data to a vector of field elements for note storage pub fn to_elements(&self) -> Vec { - const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 8; // 4 + 2 + 1 + 1 (serial_num + account_id + tag + padding) + const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 8; // 4 + 2 + 1 + 1 (serial_num + account_id + tag + miden_claim_amount) let mut elements = Vec::with_capacity(OUTPUT_NOTE_DATA_ELEMENT_COUNT); // P2ID note serial number (4 felts as Word) @@ -199,8 +201,8 @@ impl OutputNoteData { // Output note tag elements.push(Felt::new(self.output_note_tag.as_u32() as u64)); - // Padding - elements.extend(vec![Felt::ZERO; 1]); + // Miden claim amount + elements.push(self.miden_claim_amount); elements } diff --git a/crates/miden-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index 3ed26233cc..e452b270e0 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -52,6 +52,6 @@ miden-protocol = { features = ["std"], workspace = true } primitive-types = { workspace = true } rstest = { workspace = true } serde = { features = ["derive"], workspace = true } -serde_json = { version = "1.0" } +serde_json = { features = ["arbitrary_precision"], version = "1.0" } tokio = { features = ["macros", "rt"], workspace = true } winter-rand-utils = { version = "0.13" } diff --git a/crates/miden-testing/src/utils.rs b/crates/miden-testing/src/utils.rs index 1732ff3580..c17997789d 100644 --- a/crates/miden-testing/src/utils.rs +++ b/crates/miden-testing/src/utils.rs @@ -1,13 +1,16 @@ use alloc::string::String; use alloc::vec::Vec; +use miden_crypto::Word; use miden_processor::crypto::RpoRandomCoin; use miden_protocol::account::AccountId; use miden_protocol::asset::Asset; use miden_protocol::crypto::rand::FeltRng; -use miden_protocol::note::{Note, NoteType}; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteTag, NoteType}; use miden_protocol::testing::storage::prepare_assets; use miden_standards::code_builder::CodeBuilder; +use miden_standards::note::P2idNoteStorage; use miden_standards::testing::note::NoteBuilder; use rand::SeedableRng; use rand::rngs::SmallRng; @@ -263,3 +266,21 @@ fn note_script_that_creates_notes<'note>( Ok(out) } + +/// Generates a P2ID note - Pay-to-ID note with an exact serial number +pub fn create_p2id_note_exact( + sender: AccountId, + target: AccountId, + assets: Vec, + note_type: NoteType, + serial_num: Word, +) -> Result { + let recipient = P2idNoteStorage::new(target).into_recipient(serial_num); + + let tag = NoteTag::with_account_target(target); + + let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); + let vault = NoteAssets::new(assets)?; + + Ok(Note::new(vault, metadata, recipient)) +} diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 8a9cc4c16b..22ef569b9b 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -1,5 +1,7 @@ extern crate alloc; +use core::slice; + use miden_agglayer::{ ClaimNoteStorage, OutputNoteData, @@ -9,12 +11,16 @@ use miden_agglayer::{ create_existing_bridge_account, }; use miden_protocol::account::Account; -use miden_protocol::asset::FungibleAsset; +use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use miden_protocol::transaction::OutputNote; use miden_protocol::{Felt, FieldElement}; use miden_standards::account::wallets::BasicWallet; +use miden_standards::testing::account_component::IncrNonceAuthComponent; +use miden_standards::testing::mock_account::MockAccountExt; +use miden_testing::utils::create_p2id_note_exact; use miden_testing::{AccountState, Auth, MockChain}; use rand::Rng; @@ -60,7 +66,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a let origin_token_address = leaf_data.origin_token_address; let origin_network = leaf_data.origin_network; - let scale = 0u8; + let scale = 10u8; let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, @@ -83,6 +89,23 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a .to_account_id() .expect("destination address is not an embedded Miden AccountId"); + // For the simulated case, create the destination account so we can consume the P2ID note + let destination_account = if matches!(data_source, ClaimDataSource::Simulated) { + let dest = + Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, IncrNonceAuthComponent); + // Ensure the mock account ID matches the destination embedded in the JSON test vector, + // since the claim note targets this account ID. + assert_eq!( + dest.id(), + destination_account_id, + "mock destination account ID must match the destination_account_id from the claim data" + ); + builder.add_account(dest.clone())?; + Some(dest) + } else { + None + }; + // CREATE SENDER ACCOUNT (for creating the claim note) // -------------------------------------------------------------------------------------------- let sender_account_builder = @@ -99,10 +122,17 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // Generate a serial number for the P2ID note let serial_num = builder.rng_mut().draw_word(); + // Calculate the scaled-down Miden amount using the faucet's scale factor + let miden_claim_amount = leaf_data + .amount + .scale_to_token_amount(scale as u32) + .expect("amount should scale successfully"); + let output_note_data = OutputNoteData { output_p2id_serial_num: serial_num, target_faucet_account_id: agglayer_faucet.id(), output_note_tag: NoteTag::with_account_target(destination_account_id), + miden_claim_amount, }; let claim_inputs = ClaimNoteStorage { proof_data, leaf_data, output_note_data }; @@ -154,15 +184,73 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a assert_eq!(output_note.metadata().sender(), agglayer_faucet.id()); assert_eq!(output_note.metadata().note_type(), NoteType::Public); - // Note: We intentionally do NOT verify the exact note ID or asset amount here because - // the scale_u256_to_native_amount function is currently a TODO stub that doesn't perform - // proper u256-to-native scaling. The test verifies that the bridge-in flow correctly - // validates the Merkle proof and creates an output note. - // - // TODO: Once scale_u256_to_native_amount is properly implemented, add: - // - Verification that the minted amount matches the expected scaled value - // - Full note ID comparison with the expected P2ID note - // - Asset content verification + // Extract and verify P2ID asset contents + let mut assets_iter = output_note.assets().unwrap().iter_fungible(); + let p2id_asset = assets_iter.next().unwrap(); + + // Verify minted amount matches expected scaled value + assert_eq!( + Felt::new(p2id_asset.amount()), + miden_claim_amount, + "asset amount does not match" + ); + + // Verify faucet ID matches agglayer_faucet (P2ID token issuer) + assert_eq!( + p2id_asset.faucet_id(), + agglayer_faucet.id(), + "P2ID asset faucet ID doesn't match agglayer_faucet: got {:?}, expected {:?}", + p2id_asset.faucet_id(), + agglayer_faucet.id() + ); + + // Verify full note ID construction + let expected_asset: Asset = + FungibleAsset::new(agglayer_faucet.id(), miden_claim_amount.as_int()) + .unwrap() + .into(); + let expected_output_p2id_note = create_p2id_note_exact( + agglayer_faucet.id(), + destination_account_id, + vec![expected_asset], + NoteType::Public, + serial_num, + ) + .unwrap(); + + assert_eq!(OutputNote::Full(expected_output_p2id_note.clone()), *output_note); + + // CONSUME THE P2ID NOTE WITH THE DESTINATION ACCOUNT (simulated case only) + // -------------------------------------------------------------------------------------------- + // For the simulated case, we control the destination account and can verify the full + // end-to-end flow including P2ID consumption and balance updates. + if let Some(destination_account) = destination_account { + // Add the faucet transaction to the chain and prove the next block so the P2ID note is + // committed and can be consumed. + mock_chain.add_pending_executed_transaction(&executed_transaction)?; + mock_chain.prove_next_block()?; + + // Execute the consume transaction for the destination account + let consume_tx_context = mock_chain + .build_tx_context( + destination_account.id(), + &[], + slice::from_ref(&expected_output_p2id_note), + )? + .build()?; + let consume_executed_transaction = consume_tx_context.execute().await?; + + // Verify the destination account received the minted asset + let mut destination_account = destination_account.clone(); + destination_account.apply_delta(consume_executed_transaction.account_delta())?; + + let balance = destination_account.vault().get_balance(agglayer_faucet.id())?; + assert_eq!( + balance, + miden_claim_amount.as_int(), + "destination account balance does not match" + ); + } Ok(()) } diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 351648ea5e..adb3b506d1 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -40,9 +40,9 @@ use miden_standards::errors::standards::{ }; use miden_standards::note::{BurnNote, MintNote, MintNoteStorage, StandardNote}; use miden_standards::testing::note::NoteBuilder; +use miden_testing::utils::create_p2id_note_exact; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; -use crate::scripts::swap::create_p2id_note_exact; use crate::{get_note_with_fungible_asset_and_script, prove_and_verify_transaction}; // Shared test utilities for faucet tests diff --git a/crates/miden-testing/tests/scripts/swap.rs b/crates/miden-testing/tests/scripts/swap.rs index e7fd983827..d329161841 100644 --- a/crates/miden-testing/tests/scripts/swap.rs +++ b/crates/miden-testing/tests/scripts/swap.rs @@ -1,8 +1,7 @@ use anyhow::Context; use miden_protocol::account::{Account, AccountId, AccountStorageMode, AccountType}; use miden_protocol::asset::{Asset, FungibleAsset, NonFungibleAsset}; -use miden_protocol::errors::NoteError; -use miden_protocol::note::{Note, NoteAssets, NoteDetails, NoteMetadata, NoteTag, NoteType}; +use miden_protocol::note::{Note, NoteDetails, NoteType}; use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1, @@ -11,7 +10,7 @@ use miden_protocol::testing::account_id::{ use miden_protocol::transaction::OutputNote; use miden_protocol::{Felt, Word}; use miden_standards::code_builder::CodeBuilder; -use miden_standards::note::P2idNoteStorage; +use miden_testing::utils::create_p2id_note_exact; use miden_testing::{Auth, MockChain}; use crate::prove_and_verify_transaction; @@ -335,21 +334,3 @@ fn setup_swap_test(payback_note_type: NoteType) -> anyhow::Result payback_note, }) } - -/// Generates a P2ID note - Pay-to-ID note with an exact serial number -pub fn create_p2id_note_exact( - sender: AccountId, - target: AccountId, - assets: Vec, - note_type: NoteType, - serial_num: Word, -) -> Result { - let recipient = P2idNoteStorage::new(target).into_recipient(serial_num); - - let tag = NoteTag::with_account_target(target); - - let metadata = NoteMetadata::new(sender, note_type).with_tag(tag); - let vault = NoteAssets::new(assets)?; - - Ok(Note::new(vault, metadata, recipient)) -} From 0137fce3c7dd8829921a6a56c438bae5d526f887 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 24 Feb 2026 16:16:27 +0100 Subject: [PATCH 15/21] feat(AggLayer): Sender (resp. consumer) validation on `CONFIG_AGG_BRIDGE` & `UPDATE_GER` (resp. `BURN`) notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: tracking empty commit * feat(AggLayer): validate `CONFIG_AGG_BRIDGE` and `UPDATE_GER` note senders are authorized entities (#2479) * feat: validate CONFIG_AGG_BRIDGE and UPDATE_GER note senders Add sender validation to ensure only authorized entities can update bridge configuration or the global exit root. Two distinct roles are enforced: - **Bridge admin** (BRIDGE_ADMIN_SLOT): authorized to register faucets via CONFIG_AGG_BRIDGE notes - **Global exit root manager** (GER_MANAGER_SLOT): authorized to update the GER via UPDATE_GER notes Changes: - Add BRIDGE_ADMIN_SLOT and GER_MANAGER_SLOT storage slots to bridge account - Add assert_sender_is_bridge_admin account procedure in bridge_config - Add assert_sender_is_ger_manager account procedure in bridge_config - Export both procedures from the bridge component - CONFIG_AGG_BRIDGE calls assert_sender_is_bridge_admin - UPDATE_GER calls assert_sender_is_ger_manager - Update create_bridge_account to accept bridge_admin_id and ger_manager_id - Update all tests accordingly Closes #2450 Closes #2467 Co-authored-by: marti * test: use distinct bridge_admin and ger_manager accounts in all tests Create separate wallet accounts for bridge admin and GER manager roles in every test, even when only one role is exercised. This makes the role distinction explicit and avoids accidentally relying on both roles sharing the same identity. Co-authored-by: marti * lints * fix: order of stack comments; simplify ops --------- Co-authored-by: Cursor Agent * feat: create BURN note with NetworkAccountTarget attachment (#2481) * feat: create BURN note with NetworkAccountTarget attachment Replace NoteTag-based targeting with NetworkAccountTarget attachment for BURN notes created by bridge_out. The BURN note now uses: - A NetworkAccountTarget attachment to specify the faucet as target - A simple tag (0) instead of note_tag::create_account_target - set_attachment is called right after note creation, using a dup'd note_idx so no local is needed to save it Changes: - bridge_out.masm: use network_account_target::new + output_note::set_attachment instead of note_tag::create_account_target - bridge_out test: verify attachment target instead of NoteTag Closes #2470 Co-authored-by: marti * refactor: use DEFAULT_TAG constant and set_attachment in create_burn_note Replace the local BURN_NOTE_TAG constant with the DEFAULT_TAG constant from the note_tag standards module (re-declared locally since MASM does not support cross-module constant references in push). Save attachment_scheme and attachment_kind to locals and use set_attachment instead of set_word_attachment. Co-authored-by: marti * Apply suggestions from code review Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> * Apply suggestion from @partylikeits1983 Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> * feat(standards): add NoteExecutionHint constants to MASM standards Add `note/execution_hint.masm` under the standards library exposing the four NoteExecutionHint variants as public constants: - NONE = 0 - ALWAYS = 1 - AFTER_BLOCK = 2 (tag bits only; payload must be composed at runtime) - ON_BLOCK_SLOT = 3 (tag bits only; payload must be composed at runtime) These mirror the `NONE_TAG` / `ALWAYS_TAG` / `AFTER_BLOCK_TAG` / `ON_BLOCK_SLOT_TAG` internal constants from the Rust `NoteExecutionHint` implementation and allow MASM callers to reference them via `use miden::standards::note::execution_hint::ALWAYS` (etc.) rather than duplicating magic numbers locally. Co-Authored-By: Claude Sonnet 4.6 * refactor(agglayer): import ALWAYS execution hint from standards library Replace the locally-defined `EXECUTION_HINT_ALWAYS = 1` constants with the canonical `ALWAYS` constant from `miden::standards::note::execution_hint`. - `bridge_out.masm`: add `use miden::standards::note::execution_hint::ALWAYS`, drop local const, update `push.EXECUTION_HINT_ALWAYS` → `push.ALWAYS` - `agglayer_faucet.masm`: drop dead `EXECUTION_HINT_ALWAYS = 1` (was defined but never referenced in the file) Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Cursor Agent Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 * chore: lowercase `.expect()` messages in agglayer code (#2491) Co-authored-by: Claude Sonnet 4.6 --------- Co-authored-by: Cursor Agent Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../asm/bridge/agglayer_faucet.masm | 1 - .../asm/bridge/bridge_config.masm | 74 ++++++++++++++++- .../miden-agglayer/asm/bridge/bridge_out.masm | 56 ++++++++++--- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 11 ++- .../asm/note_scripts/UPDATE_GER.masm | 12 ++- crates/miden-agglayer/src/b2agg_note.rs | 2 +- crates/miden-agglayer/src/config_note.rs | 2 +- crates/miden-agglayer/src/errors/agglayer.rs | 5 ++ crates/miden-agglayer/src/lib.rs | 82 +++++++++++++++---- crates/miden-agglayer/src/update_ger_note.rs | 2 +- .../asm/standards/note/execution_hint.masm | 25 ++++++ .../miden-testing/tests/agglayer/bridge_in.rs | 13 ++- .../tests/agglayer/bridge_out.rs | 80 ++++++++++++++---- .../tests/agglayer/config_bridge.rs | 23 ++++-- .../tests/agglayer/crypto_utils.rs | 4 +- .../tests/agglayer/test_utils.rs | 2 +- .../tests/agglayer/update_ger.rs | 26 +++--- 17 files changed, 336 insertions(+), 84 deletions(-) create mode 100644 crates/miden-standards/asm/standards/note/execution_hint.masm diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index 655a00190e..9c6175d455 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -70,7 +70,6 @@ const BUILD_P2ID_AMOUNT_MEM_LOC_1 = 4 # P2ID output note constants const P2ID_NOTE_NUM_STORAGE_ITEMS = 2 const OUTPUT_NOTE_TYPE_PUBLIC = 1 -const EXECUTION_HINT_ALWAYS = 1 const OUTPUT_NOTE_AUX = 0 const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 diff --git a/crates/miden-agglayer/asm/bridge/bridge_config.masm b/crates/miden-agglayer/asm/bridge/bridge_config.masm index 38cb530767..fef4a2c19a 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_config.masm @@ -1,9 +1,20 @@ +use miden::protocol::account_id +use miden::protocol::active_account +use miden::protocol::active_note use miden::protocol::native_account +# ERRORS +# ================================================================================================= + +const ERR_SENDER_NOT_BRIDGE_ADMIN="note sender is not the bridge admin" +const ERR_SENDER_NOT_GER_MANAGER="note sender is not the global exit root manager" + # CONSTANTS # ================================================================================================= const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") +const BRIDGE_ADMIN_SLOT=word("miden::agglayer::bridge::admin") +const GER_MANAGER_SLOT=word("miden::agglayer::bridge::ger_manager") const IS_REGISTERED_FLAG=1 # PUBLIC INTERFACE @@ -17,9 +28,6 @@ const IS_REGISTERED_FLAG=1 #! The sentinel value `[1, 0, 0, 0]` distinguishes registered faucets from #! non-existent entries (SMTs return EMPTY_WORD for missing keys). #! -#! TODO: Currently, no sender validation is performed — anyone can register a faucet. -#! Tracked in https://github.com/0xMiden/miden-base/issues/2450 -#! #! Inputs: [faucet_id_prefix, faucet_id_suffix, pad(14)] #! Outputs: [pad(16)] #! @@ -43,3 +51,63 @@ pub proc register_faucet dropw end + +#! Asserts that the note sender matches the bridge admin stored in account storage. +#! +#! Reads the bridge admin account ID from BRIDGE_ADMIN_SLOT and compares it against +#! the sender of the currently executing note. Panics if they do not match. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender does not match the bridge admin account ID. +#! +#! Invocation: call +pub proc assert_sender_is_bridge_admin + # => [pad(16)] + + push.BRIDGE_ADMIN_SLOT[0..2] + exec.active_account::get_item + # => [admin_prefix, admin_suffix, 0, 0, pad(16)] + + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, admin_prefix, admin_suffix, pad(18)] + + exec.account_id::is_equal + assert.err=ERR_SENDER_NOT_BRIDGE_ADMIN + # => [pad(18)] + + drop drop + # => [pad(16)] +end + +#! Asserts that the note sender matches the global exit root manager stored in account storage. +#! +#! Reads the GER manager account ID from GER_MANAGER_SLOT and compares it against +#! the sender of the currently executing note. Panics if they do not match. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender does not match the GER manager account ID. +#! +#! Invocation: call +pub proc assert_sender_is_ger_manager + # => [pad(16)] + + push.GER_MANAGER_SLOT[0..2] + exec.active_account::get_item + # => [mgr_prefix, mgr_suffix, 0, 0, pad(16)] + + exec.active_note::get_sender + # => [sender_prefix, sender_suffix, mgr_prefix, mgr_suffix, pad(18)] + + exec.account_id::is_equal + assert.err=ERR_SENDER_NOT_GER_MANAGER + # => [pad(18)] + + drop drop + # => [pad(16)] +end diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index 74f3319fdc..1de5a619b6 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -3,8 +3,10 @@ use miden::protocol::active_account use miden::protocol::native_account use miden::protocol::note use miden::protocol::tx -use miden::standards::note_tag use miden::standards::data_structures::double_word_array +use miden::standards::attachments::network_account_target +use miden::standards::note_tag::DEFAULT_TAG +use miden::standards::note::execution_hint::ALWAYS use miden::protocol::output_note use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 @@ -35,7 +37,9 @@ const DESTINATION_NETWORK_LOC=9 # create_burn_note memory locals const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 -const NETWORK_FAUCET_TAG_LOC=5 +const ATTACHMENT_LOC=4 +const ATTACHMENT_SCHEME_LOC=8 +const ATTACHMENT_KIND_LOC=9 const LEAF_DATA_START_PTR=44 @@ -443,10 +447,10 @@ proc compute_burn_note_serial_num # => [SERIAL_NUM] end -#! Creates a BURN note for the specified asset. +#! Creates a BURN note for the specified asset with a NetworkAccountTarget attachment. #! #! This procedure creates an output note that represents a burn operation for the given asset. -#! The note is configured with the appropriate recipient, tag, and execution hint. +#! The note targets the faucet account via a NetworkAccountTarget attachment. #! #! Inputs: [ASSET] #! Outputs: [] @@ -455,7 +459,7 @@ end #! - ASSET is the asset to be burned. #! #! Invocation: exec -@locals(8) +@locals(10) proc create_burn_note loc_storew_be.CREATE_BURN_NOTE_BURN_ASSET_LOC dupw # => [ASSET, ASSET] @@ -463,10 +467,18 @@ proc create_burn_note movup.2 drop movup.2 drop # => [faucet_id_prefix, faucet_id_suffix, ASSET] - exec.note_tag::create_account_target - # => [network_faucet_tag, ASSET] - - loc_store.NETWORK_FAUCET_TAG_LOC + # Create NetworkAccountTarget attachment for the faucet + # new expects [account_id_prefix, account_id_suffix, exec_hint] + push.ALWAYS movdn.2 + # => [faucet_id_prefix, faucet_id_suffix, exec_hint, ASSET] + + exec.network_account_target::new + # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT, ASSET] + + # Save attachment data to locals + loc_store.ATTACHMENT_SCHEME_LOC + loc_store.ATTACHMENT_KIND_LOC + loc_storew_be.ATTACHMENT_LOC dropw # => [ASSET] exec.compute_burn_note_serial_num @@ -475,21 +487,41 @@ proc create_burn_note procref.::miden::standards::notes::burn::main swapw # => [SERIAL_NUM, SCRIPT_ROOT] - # BURN note has no storage items, so we can set the pointer to 0 push.BURN_NOTE_NUM_STORAGE_ITEMS push.0 # => [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] + exec.note::build_recipient # => [RECIPIENT] push.PUBLIC_NOTE - loc_load.NETWORK_FAUCET_TAG_LOC + push.DEFAULT_TAG # => [tag, note_type, RECIPIENT] call.output_note::create # => [note_idx] - movdn.4 loc_loadw_be.CREATE_BURN_NOTE_BURN_ASSET_LOC + # Duplicate note_idx: one for set_attachment, one for add_asset + dup + # => [note_idx, note_idx] + + # Set the NetworkAccountTarget attachment + loc_loadw_be.ATTACHMENT_LOC + # => [NOTE_ATTACHMENT, note_idx, note_idx] + + loc_load.ATTACHMENT_KIND_LOC + loc_load.ATTACHMENT_SCHEME_LOC + # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT, note_idx, note_idx] + + movup.6 + # => [note_idx, attachment_scheme, attachment_kind, NOTE_ATTACHMENT, note_idx] + + exec.output_note::set_attachment + # => [note_idx] + + # Add the asset to the note + loc_loadw_be.CREATE_BURN_NOTE_BURN_ASSET_LOC # => [ASSET, note_idx] exec.output_note::add_asset + # => [] end diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm index 99dcee284f..2bfc1f29b4 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -19,13 +19,13 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! Registers a faucet in the bridge's faucet registry. #! #! This note can only be consumed by the Agglayer Bridge account that is targeted by the note -#! attachment. Upon consumption, it registers the faucet ID from note storage in the bridge's +#! attachment, and only if the note was sent by the bridge admin. +#! Upon consumption, it registers the faucet ID from note storage in the bridge's #! faucet registry. - -#! Note: Currently, there are no sender validation checks, so anyone can register a faucet. #! #! Requires that the account exposes: #! - agglayer::bridge_config::register_faucet procedure. +#! - agglayer::bridge_config::assert_sender_is_bridge_admin procedure. #! #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] @@ -40,6 +40,7 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! #! Panics if: #! - The note attachment target account does not match the consuming bridge account. +#! - The note sender is not the bridge admin. #! - The note does not contain exactly 2 storage items. #! - The account does not expose the register_faucet procedure. #! @@ -52,6 +53,10 @@ begin assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH # => [pad(16)] + # Ensure the note sender is the bridge admin. + call.bridge_config::assert_sender_is_bridge_admin + # => [pad(16)] + # Load note storage to memory push.STORAGE_START_PTR exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index 87a29aaef9..21118fc9d0 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -1,3 +1,4 @@ +use miden::agglayer::bridge_config use miden::agglayer::bridge_in use miden::protocol::active_note use miden::protocol::active_account @@ -22,10 +23,12 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe #! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_in::update_ger function. #! #! This note can only be consumed by the specific agglayer bridge account whose ID is provided -#! in the note attachment (target_account_id). +#! in the note attachment (target_account_id), and only if the note was sent by the +#! global exit root manager. #! #! Requires that the account exposes: -#! - agglayer::bridge_in::update_ger procedure. +#! - agglayer::bridge_config::update_ger procedure. +#! - agglayer::bridge_config::assert_sender_is_ger_manager procedure. #! #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] @@ -36,6 +39,7 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe #! Panics if: #! - account does not expose update_ger procedure. #! - target account ID does not match the consuming account ID. +#! - note sender is not the global exit root manager. #! - number of note storage items is not exactly 8. begin dropw @@ -46,6 +50,10 @@ begin assert.err=ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH # => [pad(16)] + # Ensure the note sender is the global exit root manager. + call.bridge_config::assert_sender_is_ger_manager + # => [pad(16)] + # proceed with the GER update logic push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs index 602fdacfac..b5f980fd06 100644 --- a/crates/miden-agglayer/src/b2agg_note.rs +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -32,7 +32,7 @@ use crate::EthAddressFormat; // Initialize the B2AGG note script only once static B2AGG_SCRIPT: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/B2AGG.masb")); - let program = Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed"); + let program = Program::read_from_bytes(bytes).expect("shipped B2AGG script is well-formed"); NoteScript::new(program) }); diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs index c8c259a7b2..9a323424fb 100644 --- a/crates/miden-agglayer/src/config_note.rs +++ b/crates/miden-agglayer/src/config_note.rs @@ -34,7 +34,7 @@ static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CONFIG_AGG_BRIDGE.masb")); let program = - Program::read_from_bytes(bytes).expect("Shipped CONFIG_AGG_BRIDGE script is well-formed"); + Program::read_from_bytes(bytes).expect("shipped CONFIG_AGG_BRIDGE script is well-formed"); NoteScript::new(program) }); diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index ba836a0cfa..a3d27eecf5 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -60,6 +60,11 @@ pub const ERR_ROLLUP_INDEX_NON_ZERO: MasmError = MasmError::from_static_str("rol /// Error Message: "maximum scaling factor is 18" pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("maximum scaling factor is 18"); +/// Error Message: "note sender is not the bridge admin" +pub const ERR_SENDER_NOT_BRIDGE_ADMIN: MasmError = MasmError::from_static_str("note sender is not the bridge admin"); +/// Error Message: "note sender is not the global exit root manager" +pub const ERR_SENDER_NOT_GER_MANAGER: MasmError = MasmError::from_static_str("note sender is not the global exit root manager"); + /// Error Message: "merkle proof verification failed: provided SMT root does not match the computed root" pub const ERR_SMT_ROOT_VERIFICATION_FAILED: MasmError = MasmError::from_static_str("merkle proof verification failed: provided SMT root does not match the computed root"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 5db0c68195..29d6c03410 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -60,7 +60,7 @@ pub use update_ger_note::UpdateGerNote; // Initialize the CLAIM note script only once static CLAIM_SCRIPT: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CLAIM.masb")); - let program = Program::read_from_bytes(bytes).expect("Shipped CLAIM script is well-formed"); + let program = Program::read_from_bytes(bytes).expect("shipped CLAIM script is well-formed"); NoteScript::new(program) }); @@ -75,7 +75,7 @@ pub fn claim_script() -> NoteScript { // Initialize the unified AggLayer library only once static AGGLAYER_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/agglayer.masl")); - Library::read_from_bytes(bytes).expect("Shipped AggLayer library is well-formed") + Library::read_from_bytes(bytes).expect("shipped AggLayer library is well-formed") }); /// Returns the unified AggLayer Library containing all agglayer modules. @@ -99,6 +99,12 @@ pub fn local_exit_tree_library() -> Library { agglayer_library() } +/// Storage slot name for the bridge admin account ID. +pub const BRIDGE_ADMIN_SLOT_NAME: &str = "miden::agglayer::bridge::admin"; + +/// Storage slot name for the global exit root manager account ID. +pub const GER_MANAGER_SLOT_NAME: &str = "miden::agglayer::bridge::ger_manager"; + /// Creates a Local Exit Tree component with the specified storage slots. /// /// This component uses the local_exit_tree library and can be added to accounts @@ -259,7 +265,7 @@ pub fn faucet_registry_key(faucet_id: AccountId) -> Word { /// Returns an [`AccountComponent`] configured for bridge operations with MMR validation. pub fn create_bridge_account_component() -> AccountComponent { let bridge_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge") - .expect("Bridge storage slot name should be valid"); + .expect("bridge storage slot name should be valid"); let bridge_storage_slots = vec![StorageSlot::with_empty_map(bridge_storage_slot_name)]; bridge_out_component(bridge_storage_slots) } @@ -298,7 +304,7 @@ pub fn create_agglayer_faucet_component( scale: u8, ) -> AccountComponent { // Create network faucet metadata slot: [token_supply, max_supply, decimals, token_symbol] - let token_symbol = TokenSymbol::new(token_symbol).expect("Token symbol should be valid"); + let token_symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); let metadata_word = Word::new([token_supply, max_supply, Felt::from(decimals), token_symbol.into()]); let metadata_slot = @@ -312,7 +318,7 @@ pub fn create_agglayer_faucet_component( bridge_account_id.prefix().as_felt(), ]); let agglayer_storage_slot_name = StorageSlotName::new("miden::agglayer::faucet") - .expect("Agglayer faucet storage slot name should be valid"); + .expect("agglayer faucet storage slot name should be valid"); let bridge_slot = StorageSlot::with_value(agglayer_storage_slot_name, bridge_account_id_word); // Create conversion metadata storage slots @@ -320,11 +326,11 @@ pub fn create_agglayer_faucet_component( agglayer_faucet_conversion_slots(origin_token_address, origin_network, scale); let conversion_info_1_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") - .expect("Conversion info 1 storage slot name should be valid"); + .expect("conversion info 1 storage slot name should be valid"); let conversion_slot1 = StorageSlot::with_value(conversion_info_1_name, conversion_slot1_word); let conversion_info_2_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") - .expect("Conversion info 2 storage slot name should be valid"); + .expect("conversion info 2 storage slot name should be valid"); let conversion_slot2 = StorageSlot::with_value(conversion_info_2_name, conversion_slot2_word); // Combine all storage slots for the agglayer faucet component @@ -337,9 +343,20 @@ pub fn create_agglayer_faucet_component( /// /// The bridge starts with an empty faucet registry. Faucets are registered at runtime /// via CONFIG_AGG_BRIDGE notes that call `bridge_config::register_faucet`. -pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { +/// +/// # Parameters +/// - `seed`: The seed used to derive the account ID. +/// - `bridge_admin_id`: The account ID of the bridge admin. Only notes sent by this account are +/// allowed to update bridge configuration (e.g. register faucets). +/// - `ger_manager_id`: The account ID of the global exit root manager. Only notes sent by this +/// account are allowed to update the GER. +pub fn create_bridge_account_builder( + seed: Word, + bridge_admin_id: AccountId, + ger_manager_id: AccountId, +) -> AccountBuilder { let ger_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger") - .expect("Bridge storage slot name should be valid"); + .expect("bridge storage slot name should be valid"); let bridge_in_storage_slots = vec![StorageSlot::with_empty_map(ger_storage_slot_name)]; let bridge_in_component = bridge_in_component(bridge_in_storage_slots); @@ -358,13 +375,34 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { .expect("LET num_leaves storage slot name should be valid"); let faucet_registry_slot_name = StorageSlotName::new("miden::agglayer::bridge::faucet_registry") - .expect("Faucet registry storage slot name should be valid"); + .expect("faucet registry storage slot name should be valid"); + + let bridge_admin_slot_name = StorageSlotName::new(BRIDGE_ADMIN_SLOT_NAME) + .expect("bridge admin storage slot name should be valid"); + let bridge_admin_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + bridge_admin_id.suffix(), + bridge_admin_id.prefix().as_felt(), + ]); + + let ger_manager_slot_name = StorageSlotName::new(GER_MANAGER_SLOT_NAME) + .expect("GER manager storage slot name should be valid"); + let ger_manager_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + ger_manager_id.suffix(), + ger_manager_id.prefix().as_felt(), + ]); + let bridge_out_storage_slots = vec![ StorageSlot::with_empty_map(let_storage_slot_name), StorageSlot::with_value(let_root_lo_slot_name, Word::empty()), StorageSlot::with_value(let_root_hi_slot_name, Word::empty()), StorageSlot::with_value(let_num_leaves_slot_name, Word::empty()), StorageSlot::with_empty_map(faucet_registry_slot_name), + StorageSlot::with_value(bridge_admin_slot_name, bridge_admin_word), + StorageSlot::with_value(ger_manager_slot_name, ger_manager_word), ]; let bridge_out_component = bridge_out_component(bridge_out_storage_slots); @@ -380,22 +418,30 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { /// Creates a new bridge account with the standard configuration. /// /// This creates a new account suitable for production use. -pub fn create_bridge_account(seed: Word) -> Account { - create_bridge_account_builder(seed) +pub fn create_bridge_account( + seed: Word, + bridge_admin_id: AccountId, + ger_manager_id: AccountId, +) -> Account { + create_bridge_account_builder(seed, bridge_admin_id, ger_manager_id) .with_auth_component(AccountComponent::from(NoAuth)) .build() - .expect("Bridge account should be valid") + .expect("bridge account should be valid") } /// Creates an existing bridge account with the standard configuration. /// /// This creates an existing account suitable for testing scenarios. #[cfg(any(feature = "testing", test))] -pub fn create_existing_bridge_account(seed: Word) -> Account { - create_bridge_account_builder(seed) +pub fn create_existing_bridge_account( + seed: Word, + bridge_admin_id: AccountId, + ger_manager_id: AccountId, +) -> Account { + create_bridge_account_builder(seed, bridge_admin_id, ger_manager_id) .with_auth_component(AccountComponent::from(NoAuth)) .build_existing() - .expect("Bridge account should be valid") + .expect("bridge account should be valid") } /// Creates a complete agglayer faucet account builder with the specified configuration. @@ -454,7 +500,7 @@ pub fn create_agglayer_faucet( ) .with_auth_component(AccountComponent::from(NoAuth)) .build() - .expect("Agglayer faucet account should be valid") + .expect("agglayer faucet account should be valid") } /// Creates an existing agglayer faucet account with the specified configuration. @@ -486,5 +532,5 @@ pub fn create_existing_agglayer_faucet( ) .with_auth_component(AccountComponent::from(NoAuth)) .build_existing() - .expect("Agglayer faucet account should be valid") + .expect("agglayer faucet account should be valid") } diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index a9928aec17..e705a6d8a0 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -35,7 +35,7 @@ use crate::ExitRoot; static UPDATE_GER_SCRIPT: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/UPDATE_GER.masb")); let program = - Program::read_from_bytes(bytes).expect("Shipped UPDATE_GER script is well-formed"); + Program::read_from_bytes(bytes).expect("shipped UPDATE_GER script is well-formed"); NoteScript::new(program) }); diff --git a/crates/miden-standards/asm/standards/note/execution_hint.masm b/crates/miden-standards/asm/standards/note/execution_hint.masm new file mode 100644 index 0000000000..6fb7e4c5d2 --- /dev/null +++ b/crates/miden-standards/asm/standards/note/execution_hint.masm @@ -0,0 +1,25 @@ +# CONSTANTS +# ================================================================================================= + +#! Unspecified execution hint. The conditions under which the note is consumable are not known. +#! +#! Felt encoding: `0` +pub const NONE = 0 + +#! The note's script can be executed at any time. +#! +#! Felt encoding: `1` +pub const ALWAYS = 1 + +#! The note's script can be executed after the specified block number. +#! +#! This constant encodes only the tag bits. The block number must be encoded in the upper 32 bits +#! of the Felt: `(block_num << 8) | AFTER_BLOCK`. +pub const AFTER_BLOCK = 2 + +#! The note's script can be executed in a specified slot within a specified round. +#! +#! This constant encodes only the tag bits. The slot parameters must be encoded in the upper 32 +#! bits of the Felt: `((round_len as u32) << 16 | (slot_len as u32) << 8 | slot_offset) << 8 | +#! ON_BLOCK_SLOT`. +pub const ON_BLOCK_SLOT = 3 diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 22ef569b9b..596ffb926f 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -46,10 +46,19 @@ use super::test_utils::ClaimDataSource; async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { let mut builder = MockChain::builder(); + // CREATE BRIDGE ADMIN ACCOUNT (not used in this test, but distinct from GER manager) + // -------------------------------------------------------------------------------------------- + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // CREATE GER MANAGER ACCOUNT (sends the UPDATE_GER note) + // -------------------------------------------------------------------------------------------- + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + // CREATE BRIDGE ACCOUNT (with bridge_out component for MMR validation) // -------------------------------------------------------------------------------------------- let bridge_seed = builder.rng_mut().draw_word(); - let bridge_account = create_existing_bridge_account(bridge_seed); + let bridge_account = + create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id()); builder.add_account(bridge_account.clone())?; // GET CLAIM DATA FROM JSON (source depends on the test case) @@ -145,7 +154,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // CREATE UPDATE_GER NOTE WITH GLOBAL EXIT ROOT // -------------------------------------------------------------------------------------------- let update_ger_note = - UpdateGerNote::create(ger, sender_account.id(), bridge_account.id(), builder.rng_mut())?; + UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?; builder.add_output_note(OutputNote::Full(update_ger_note.clone())); // BUILD MOCK CHAIN WITH ALL ACCOUNTS diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 83fc957eb3..b46c18b416 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -20,7 +20,7 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::note::{NoteAssets, NoteScript, NoteTag, NoteType}; +use miden_protocol::note::{NoteAssets, NoteScript, NoteType}; use miden_protocol::transaction::OutputNote; use miden_standards::account::faucets::TokenMetadata; use miden_standards::note::StandardNote; @@ -103,7 +103,18 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { ); let mut builder = MockChain::builder(); - let mut bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + + // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // CREATE GER MANAGER ACCOUNT (not used in this test, but distinct from admin) + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + + let mut bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); builder.add_account(bridge_account.clone())?; let expected_amounts = vectors @@ -132,14 +143,10 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { ); builder.add_account(faucet.clone())?; - // CREATE SENDER ACCOUNT - // -------------------------------------------------------------------------------------------- - let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; - - // CONFIG_AGG_BRIDGE note to register the faucet in the bridge + // CONFIG_AGG_BRIDGE note to register the faucet in the bridge (sent by bridge admin) let config_note = ConfigAggBridgeNote::create( faucet.id(), - sender_account.id(), + bridge_admin.id(), bridge_account.id(), builder.rng_mut(), )?; @@ -219,10 +226,13 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { NoteType::Public, "BURN note should be public" ); + let attachment = burn_note.metadata().attachment(); + let network_target = miden_standards::note::NetworkAccountTarget::try_from(attachment) + .expect("BURN note attachment should be a valid NetworkAccountTarget"); assert_eq!( - burn_note.metadata().tag(), - NoteTag::with_account_target(faucet.id()), - "BURN note should have the correct tag" + network_target.target_id(), + faucet.id(), + "BURN note attachment should target the faucet" ); assert_eq!( burn_note.recipient().script().root(), @@ -298,9 +308,19 @@ async fn bridge_out_consecutive() -> anyhow::Result<()> { async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); + // CREATE BRIDGE ADMIN ACCOUNT + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // CREATE GER MANAGER ACCOUNT (not used in this test, but distinct from admin) + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + // CREATE BRIDGE ACCOUNT (empty faucet registry — no faucets registered) // -------------------------------------------------------------------------------------------- - let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + let bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); builder.add_account(bridge_account.clone())?; // CREATE AGGLAYER FAUCET ACCOUNT (NOT registered in the bridge) @@ -326,7 +346,7 @@ async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = - EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + EthAddressFormat::from_hex(destination_address).expect("valid Ethereum address"); let b2agg_note = B2AggNote::create( 1u32, // destination_network @@ -384,8 +404,18 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + // Create a bridge admin account + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // Create a GER manager account (not used in this test, but distinct from admin) + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + // Create a bridge account (includes a `bridge_out` component) - let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + let bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); builder.add_account(bridge_account.clone())?; // Create a user account that will create and consume the B2AGG note @@ -399,7 +429,7 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = - EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + EthAddressFormat::from_hex(destination_address).expect("valid Ethereum address"); let assets = NoteAssets::new(vec![bridge_asset])?; @@ -482,15 +512,29 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; + // Create a bridge admin account + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // Create a GER manager account (not used in this test, but distinct from admin) + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + // Create a bridge account as the designated TARGET for the B2AGG note - let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + let bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); builder.add_account(bridge_account.clone())?; // Create a user account as the SENDER of the B2AGG note let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; // Create a "malicious" account with a bridge interface - let malicious_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + let malicious_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); builder.add_account(malicious_account.clone())?; // CREATE B2AGG NOTE @@ -501,7 +545,7 @@ async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = - EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + EthAddressFormat::from_hex(destination_address).expect("valid Ethereum address"); let assets = NoteAssets::new(vec![bridge_asset])?; diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index ee676d0816..29f863d5f7 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -16,22 +16,29 @@ use miden_testing::{Auth, MockChain}; /// Tests that a CONFIG_AGG_BRIDGE note registers a faucet in the bridge's faucet registry. /// /// Flow: -/// 1. Create a bridge account (empty faucet registry) -/// 2. Create a sender account -/// 3. Create a CONFIG_AGG_BRIDGE note carrying a faucet ID +/// 1. Create an admin (sender) account +/// 2. Create a bridge account with the admin as authorized operator +/// 3. Create a CONFIG_AGG_BRIDGE note carrying a faucet ID, sent by the admin /// 4. Consume the note with the bridge account /// 5. Verify the faucet is now in the bridge's faucet_registry map #[tokio::test] async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { let mut builder = MockChain::builder(); + // CREATE BRIDGE ADMIN ACCOUNT (note sender) + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // CREATE GER MANAGER ACCOUNT (not used in this test, but distinct from admin) + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + // CREATE BRIDGE ACCOUNT (starts with empty faucet registry) - let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + let bridge_account = create_existing_bridge_account( + builder.rng_mut().draw_word(), + bridge_admin.id(), + ger_manager.id(), + ); builder.add_account(bridge_account.clone())?; - // CREATE SENDER ACCOUNT - let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; - // Use a dummy faucet ID to register (any valid AccountId will do) let faucet_to_register = AccountId::dummy( [42; 15], @@ -53,7 +60,7 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { // CREATE CONFIG_AGG_BRIDGE NOTE let config_note = ConfigAggBridgeNote::create( faucet_to_register, - sender_account.id(), + bridge_admin.id(), bridge_account.id(), builder.rng_mut(), )?; diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 6f8e26a017..28fd421451 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -111,7 +111,7 @@ fn merkle_proof_verification_code( #[tokio::test] async fn pack_leaf_data() -> anyhow::Result<()> { let vector: LeafValueVector = - serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("Failed to parse leaf value vector"); + serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("failed to parse leaf value vector"); let leaf_data = vector.to_leaf_data(); @@ -233,7 +233,7 @@ async fn pack_leaf_data() -> anyhow::Result<()> { #[tokio::test] async fn get_leaf_value() -> anyhow::Result<()> { let vector: LeafValueVector = - serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("Failed to parse leaf value vector"); + serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("failed to parse leaf value vector"); let leaf_data = vector.to_leaf_data(); let key: Word = leaf_data.to_commitment(); diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 901ed3abde..50fb537243 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -245,7 +245,7 @@ pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock /// Lazily parsed canonical zeros from the JSON file. pub static SOLIDITY_CANONICAL_ZEROS: LazyLock = LazyLock::new(|| { - serde_json::from_str(CANONICAL_ZEROS_JSON).expect("Failed to parse canonical zeros JSON") + serde_json::from_str(CANONICAL_ZEROS_JSON).expect("failed to parse canonical zeros JSON") }); /// Lazily parsed MMR frontier vectors from the JSON file. diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 3b66f15dcb..907fff9c2d 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -42,24 +42,28 @@ struct ExitRootsFile { /// Lazily parsed exit root vectors from the JSON file. static EXIT_ROOTS_VECTORS: LazyLock = LazyLock::new(|| { - serde_json::from_str(EXIT_ROOTS_JSON).expect("Failed to parse exit roots JSON") + serde_json::from_str(EXIT_ROOTS_JSON).expect("failed to parse exit roots JSON") }); #[tokio::test] async fn update_ger_note_updates_storage() -> anyhow::Result<()> { let mut builder = MockChain::builder(); + // CREATE BRIDGE ADMIN ACCOUNT (not used in this test, but distinct from GER manager) + // -------------------------------------------------------------------------------------------- + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth)?; + + // CREATE GER MANAGER ACCOUNT (note sender) + // -------------------------------------------------------------------------------------------- + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth)?; + // CREATE BRIDGE ACCOUNT // -------------------------------------------------------------------------------------------- let bridge_seed = builder.rng_mut().draw_word(); - let bridge_account = create_existing_bridge_account(bridge_seed); + let bridge_account = + create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id()); builder.add_account(bridge_account.clone())?; - // CREATE USER ACCOUNT (NOTE SENDER) - // -------------------------------------------------------------------------------------------- - let user_account = builder.add_existing_wallet(Auth::BasicAuth)?; - builder.add_account(user_account.clone())?; - // CREATE UPDATE_GER NOTE WITH 8 STORAGE ITEMS (NEW GER AS TWO WORDS) // -------------------------------------------------------------------------------------------- @@ -70,7 +74,7 @@ async fn update_ger_note_updates_storage() -> anyhow::Result<()> { ]; let ger = ExitRoot::from(ger_bytes); let update_ger_note = - UpdateGerNote::create(ger, user_account.id(), bridge_account.id(), builder.rng_mut())?; + UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?; builder.add_output_note(OutputNote::Full(update_ger_note.clone())); let mock_chain = builder.build()?; @@ -124,11 +128,11 @@ async fn compute_ger() -> anyhow::Result<()> { for i in 0..vectors.mainnet_exit_roots.len() { let mainnet_exit_root_bytes = - hex_to_bytes(vectors.mainnet_exit_roots[i].as_str()).expect("Invalid hex string"); + hex_to_bytes(vectors.mainnet_exit_roots[i].as_str()).expect("invalid hex string"); let rollup_exit_root_bytes = - hex_to_bytes(vectors.rollup_exit_roots[i].as_str()).expect("Invalid hex string"); + hex_to_bytes(vectors.rollup_exit_roots[i].as_str()).expect("invalid hex string"); let expected_ger_bytes = - hex_to_bytes(vectors.global_exit_roots[i].as_str()).expect("Invalid hex string"); + hex_to_bytes(vectors.global_exit_roots[i].as_str()).expect("invalid hex string"); // Convert expected GER to felts for comparison let expected_ger_exit_root = ExitRoot::from(expected_ger_bytes); From 4ee27781e4574c46fbb2e9ad6a7d9ee9c9ff673b Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 24 Feb 2026 16:57:04 +0100 Subject: [PATCH 16/21] refactor(Agglayer): restructure `asm` directory and split out agglayer lib from bridge & faucet components libs * feat: add native asset claim amount as element in NoteStorage of CLAIM note * feat: add output note assertion checks * refactor: rm debug statement * Update crates/miden-testing/tests/agglayer/bridge_in.rs Co-authored-by: Marti * refactor: cleanup test & rm leftover comments * empty commit * Agglayer: restructure the `asm` directory: clear split between library, components, notes (#2471) * refactor: restructure miden-agglayer asm directory with per-component libraries Restructure the agglayer asm directory to follow the miden-standards pattern, addressing the security vulnerability where all component library functions returned the same unified library, causing bridge accounts to expose faucet procedures and vice versa. Changes: - Move asm/bridge/ files to asm/agglayer/bridge/ subdirectory - Rename agglayer_faucet.masm to asm/agglayer/faucet/mod.masm - Create thin component wrappers in asm/components/ (bridge.masm, faucet.masm) that pub use only relevant procedures - Update build.rs to compile main library from asm/agglayer/, then compile per-component libraries from asm/components/ - Update lib.rs with per-component LazyLock libraries so bridge_*_library() returns the bridge component library and faucet_library() returns the faucet component library - Update all MASM namespace references (miden::agglayer::X to miden::agglayer::bridge::X, agglayer_faucet to faucet) - Update Rust test files with new namespace paths Closes #2294 Co-authored-by: marti * fix: address CI failures (rustfmt and changelog) - Fix rustfmt line length issue in mmr_frontier.rs test strings - Add CHANGELOG.md entry for the asm directory restructuring Co-authored-by: marti * refactor: consolidate component library API - Remove bridge_out_library, bridge_in_library, local_exit_tree_library aliases; keep a single agglayer_bridge_library() - Remove faucet_library alias; keep a single agglayer_faucet_library() - Replace separate bridge_out_component/bridge_in_component/ local_exit_tree_component with a single bridge_component() - Remove bridge_out_with_local_exit_tree_component convenience function - Consolidate create_bridge_account_builder to use one bridge_component with all storage slots combined Co-authored-by: marti * refactor: move update_ger to bridge_config and shared modules to common/ - Move update_ger procedure from bridge_in.masm to bridge_config.masm, since it is a bridge configuration operation alongside register_faucet - Move utils.masm, asset_conversion.masm, eth_address.masm from agglayer/bridge/ to agglayer/common/ since they are shared between bridge and faucet modules - Update all MASM use statements and Rust test strings to reflect the new module paths (bridge::utils -> common::utils, etc.) - Update component wrapper and note script references accordingly Co-authored-by: marti * refactor: move bridge_in-specific procs out of crypto_utils Move procedures that are only called from bridge_in.masm out of the shared crypto_utils.masm module: - get_leaf_value: loads leaf data from advice map, delegates to compute_leaf_value - compute_ger: computes GER from mainnet/rollup exit roots - verify_merkle_proof: verifies a Keccak-based Merkle proof - calculate_root: private helper for verify_merkle_proof crypto_utils.masm now only contains compute_leaf_value and pack_leaf_data, which are genuinely shared between bridge_in and bridge_out. Co-authored-by: marti * refactor: deduplicate storage slot constants into bridge_config Move GER and faucet registry storage management into bridge_config.masm to eliminate duplicate constant definitions: - Move assert_valid_ger from bridge_in to bridge_config as a pub proc, so GER_STORAGE_SLOT and GER_KNOWN_FLAG are defined only in bridge_config - Add assert_faucet_registered to bridge_config as a pub proc, so FAUCET_REGISTRY_SLOT is defined only in bridge_config - bridge_in now calls bridge_config::assert_valid_ger - bridge_out now calls bridge_config::assert_faucet_registered instead of inlining the registry lookup Co-authored-by: marti * refactor: rename crypto_utils to leaf_utils The module now only contains compute_leaf_value and pack_leaf_data, so leaf_utils better describes its purpose. Co-authored-by: marti * chore: remove unused local exit tree mod * chore: remove getters from interface * refactor: move verify_merkle_proof test to bridge_in module Move solidity_verify_merkle_proof_compatibility test and its helper from leaf_utils.rs to bridge_in.rs, since verify_merkle_proof now lives in bridge_in. Drop the 'test_' prefix from the name. Co-authored-by: marti * refactor: standardize file organization across all agglayer MASM files Apply consistent section ordering to all MASM files: 1. Imports (use statements, alphabetized) 2. Type aliases 3. Errors 4. Constants (grouped: storage slots, memory pointers, data offsets, local memory offsets, data sizes, flags/other) 5. Public interface (pub proc) 6. Helper procedures (private proc) Files reorganized: - bridge_config.masm: separate storage slots from flags, errors first - bridge_in.masm: group memory pointers, local offsets, data sizes - bridge_out.masm: group LET storage slots, memory pointers, leaf data offsets, local memory offsets, and other constants into labeled blocks - common/eth_address.masm: errors before constants, public interface before helper procs - common/asset_conversion.masm: errors before constants - faucet/mod.masm: group storage slots, memory pointers, local memory offsets, data sizes, and note constants; public interface before helpers Co-authored-by: marti * Apply suggestions from code review dont need such verbosity * chore: remove dead code; make functions private * chore: use ZERO instead of Felt::new(0) --------- Co-authored-by: Cursor Agent * refactor(agglayer): encapsulate faucet and bridge in structs (#2478) * refactor: restructure miden-agglayer asm directory with per-component libraries Restructure the agglayer asm directory to follow the miden-standards pattern, addressing the security vulnerability where all component library functions returned the same unified library, causing bridge accounts to expose faucet procedures and vice versa. Changes: - Move asm/bridge/ files to asm/agglayer/bridge/ subdirectory - Rename agglayer_faucet.masm to asm/agglayer/faucet/mod.masm - Create thin component wrappers in asm/components/ (bridge.masm, faucet.masm) that pub use only relevant procedures - Update build.rs to compile main library from asm/agglayer/, then compile per-component libraries from asm/components/ - Update lib.rs with per-component LazyLock libraries so bridge_*_library() returns the bridge component library and faucet_library() returns the faucet component library - Update all MASM namespace references (miden::agglayer::X to miden::agglayer::bridge::X, agglayer_faucet to faucet) - Update Rust test files with new namespace paths Closes #2294 Co-authored-by: marti * fix: address CI failures (rustfmt and changelog) - Fix rustfmt line length issue in mmr_frontier.rs test strings - Add CHANGELOG.md entry for the asm directory restructuring Co-authored-by: marti * refactor: consolidate component library API - Remove bridge_out_library, bridge_in_library, local_exit_tree_library aliases; keep a single agglayer_bridge_library() - Remove faucet_library alias; keep a single agglayer_faucet_library() - Replace separate bridge_out_component/bridge_in_component/ local_exit_tree_component with a single bridge_component() - Remove bridge_out_with_local_exit_tree_component convenience function - Consolidate create_bridge_account_builder to use one bridge_component with all storage slots combined Co-authored-by: marti * refactor: move update_ger to bridge_config and shared modules to common/ - Move update_ger procedure from bridge_in.masm to bridge_config.masm, since it is a bridge configuration operation alongside register_faucet - Move utils.masm, asset_conversion.masm, eth_address.masm from agglayer/bridge/ to agglayer/common/ since they are shared between bridge and faucet modules - Update all MASM use statements and Rust test strings to reflect the new module paths (bridge::utils -> common::utils, etc.) - Update component wrapper and note script references accordingly Co-authored-by: marti * refactor: move bridge_in-specific procs out of crypto_utils Move procedures that are only called from bridge_in.masm out of the shared crypto_utils.masm module: - get_leaf_value: loads leaf data from advice map, delegates to compute_leaf_value - compute_ger: computes GER from mainnet/rollup exit roots - verify_merkle_proof: verifies a Keccak-based Merkle proof - calculate_root: private helper for verify_merkle_proof crypto_utils.masm now only contains compute_leaf_value and pack_leaf_data, which are genuinely shared between bridge_in and bridge_out. Co-authored-by: marti * refactor: deduplicate storage slot constants into bridge_config Move GER and faucet registry storage management into bridge_config.masm to eliminate duplicate constant definitions: - Move assert_valid_ger from bridge_in to bridge_config as a pub proc, so GER_STORAGE_SLOT and GER_KNOWN_FLAG are defined only in bridge_config - Add assert_faucet_registered to bridge_config as a pub proc, so FAUCET_REGISTRY_SLOT is defined only in bridge_config - bridge_in now calls bridge_config::assert_valid_ger - bridge_out now calls bridge_config::assert_faucet_registered instead of inlining the registry lookup Co-authored-by: marti * refactor: rename crypto_utils to leaf_utils The module now only contains compute_leaf_value and pack_leaf_data, so leaf_utils better describes its purpose. Co-authored-by: marti * chore: remove unused local exit tree mod * chore: remove getters from interface * refactor: move verify_merkle_proof test to bridge_in module Move solidity_verify_merkle_proof_compatibility test and its helper from leaf_utils.rs to bridge_in.rs, since verify_merkle_proof now lives in bridge_in. Drop the 'test_' prefix from the name. Co-authored-by: marti * refactor: standardize file organization across all agglayer MASM files Apply consistent section ordering to all MASM files: 1. Imports (use statements, alphabetized) 2. Type aliases 3. Errors 4. Constants (grouped: storage slots, memory pointers, data offsets, local memory offsets, data sizes, flags/other) 5. Public interface (pub proc) 6. Helper procedures (private proc) Files reorganized: - bridge_config.masm: separate storage slots from flags, errors first - bridge_in.masm: group memory pointers, local offsets, data sizes - bridge_out.masm: group LET storage slots, memory pointers, leaf data offsets, local memory offsets, and other constants into labeled blocks - common/eth_address.masm: errors before constants, public interface before helper procs - common/asset_conversion.masm: errors before constants - faucet/mod.masm: group storage slots, memory pointers, local memory offsets, data sizes, and note constants; public interface before helpers Co-authored-by: marti * Apply suggestions from code review dont need such verbosity * chore: remove dead code; make functions private * chore: use ZERO instead of Felt::new(0) * refactor(agglayer): encapsulate faucet and bridge in structs, make helpers private - Add AggLayerBridge and AggLayerFaucet structs with new() and From for AccountComponent - Use TokenMetadata for faucet metadata slot instead of hardcoded layout - Make internal helpers private: agglayer_bridge_library, agglayer_faucet_library, create_agglayer_faucet_component, create_bridge_account_builder, create_agglayer_faucet_builder Closes #2371 Co-authored-by: marti * lint * chore: slot name getters; improve struct docs * chore: use slot getters in tests * Update crates/miden-agglayer/src/lib.rs Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --------- Co-authored-by: Cursor Agent Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --------- Co-authored-by: riemann Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Co-authored-by: Cursor Agent --- CHANGELOG.md | 1 + .../{ => agglayer}/bridge/bridge_config.masm | 108 +++- .../bridge/bridge_in.masm} | 343 ++++++------ .../asm/{ => agglayer}/bridge/bridge_out.masm | 83 ++- .../bridge/canonical_zeros.masm | 2 +- .../asm/agglayer/bridge/leaf_utils.masm | 154 ++++++ .../bridge/mmr_frontier32_keccak.masm | 6 +- .../common}/asset_conversion.masm | 13 +- .../common}/eth_address.masm | 96 ++-- .../{bridge => agglayer/common}/utils.masm | 0 .../faucet/mod.masm} | 56 +- .../miden-agglayer/asm/bridge/bridge_in.masm | 239 -------- .../asm/bridge/local_exit_tree.masm | 120 ---- .../miden-agglayer/asm/components/bridge.masm | 11 + .../miden-agglayer/asm/components/faucet.masm | 10 + .../asm/note_scripts/B2AGG.masm | 2 +- .../asm/note_scripts/CLAIM.masm | 2 +- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 2 +- .../asm/note_scripts/UPDATE_GER.masm | 7 +- crates/miden-agglayer/build.rs | 76 +-- crates/miden-agglayer/src/lib.rs | 519 ++++++++++-------- .../tests/agglayer/asset_conversion.rs | 8 +- .../miden-testing/tests/agglayer/bridge_in.rs | 98 +++- .../tests/agglayer/bridge_out.rs | 21 +- .../tests/agglayer/config_bridge.rs | 19 +- .../tests/agglayer/global_index.rs | 2 +- .../{crypto_utils.rs => leaf_utils.rs} | 111 +--- .../tests/agglayer/mmr_frontier.rs | 10 +- crates/miden-testing/tests/agglayer/mod.rs | 2 +- .../solidity_miden_address_conversion.rs | 2 +- .../tests/agglayer/update_ger.rs | 21 +- 31 files changed, 1081 insertions(+), 1063 deletions(-) rename crates/miden-agglayer/asm/{ => agglayer}/bridge/bridge_config.masm (52%) rename crates/miden-agglayer/asm/{bridge/crypto_utils.masm => agglayer/bridge/bridge_in.masm} (54%) rename crates/miden-agglayer/asm/{ => agglayer}/bridge/bridge_out.masm (93%) rename crates/miden-agglayer/asm/{ => agglayer}/bridge/canonical_zeros.masm (99%) create mode 100644 crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm rename crates/miden-agglayer/asm/{ => agglayer}/bridge/mmr_frontier32_keccak.masm (98%) rename crates/miden-agglayer/asm/{bridge => agglayer/common}/asset_conversion.masm (99%) rename crates/miden-agglayer/asm/{bridge => agglayer/common}/eth_address.masm (92%) rename crates/miden-agglayer/asm/{bridge => agglayer/common}/utils.masm (100%) rename crates/miden-agglayer/asm/{bridge/agglayer_faucet.masm => agglayer/faucet/mod.masm} (97%) delete mode 100644 crates/miden-agglayer/asm/bridge/bridge_in.masm delete mode 100644 crates/miden-agglayer/asm/bridge/local_exit_tree.masm create mode 100644 crates/miden-agglayer/asm/components/bridge.masm create mode 100644 crates/miden-agglayer/asm/components/faucet.masm rename crates/miden-testing/tests/agglayer/{crypto_utils.rs => leaf_utils.rs} (62%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 202ac9e0ee..c0877a6286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ ### Changes +- Restructured `miden-agglayer/asm` directory to separate bridge and faucet into per-component libraries, preventing cross-component procedure exposure ([#2294](https://github.com/0xMiden/miden-base/issues/2294)). - Made kernel procedure offset constants public and replaced accessor procedures with direct constant usage ([#2375](https://github.com/0xMiden/miden-base/pull/2375)). - [BREAKING] Made `AccountComponentMetadata` a required parameter of `AccountComponent::new()`; removed `with_supported_type`, `with_supports_all_types`, and `with_metadata` methods from `AccountComponent`; simplified `AccountComponentMetadata::new()` to take just `name`; renamed `AccountComponentTemplateError` to `ComponentMetadataError` ([#2373](https://github.com/0xMiden/miden-base/pull/2373), [#2395](https://github.com/0xMiden/miden-base/pull/2395)). - Fixed MASM inline comment casing to adhere to commenting conventions ([#2398](https://github.com/0xMiden/miden-base/pull/2398)). diff --git a/crates/miden-agglayer/asm/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm similarity index 52% rename from crates/miden-agglayer/asm/bridge/bridge_config.masm rename to crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index fef4a2c19a..5193fb3ed6 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -1,3 +1,4 @@ +use miden::core::crypto::hashes::rpo256 use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note @@ -6,20 +7,89 @@ use miden::protocol::native_account # ERRORS # ================================================================================================= +const ERR_GER_NOT_FOUND = "GER not found in storage" +const ERR_FAUCET_NOT_REGISTERED="faucet is not registered in the bridge's faucet registry" const ERR_SENDER_NOT_BRIDGE_ADMIN="note sender is not the bridge admin" const ERR_SENDER_NOT_GER_MANAGER="note sender is not the global exit root manager" # CONSTANTS # ================================================================================================= -const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") +# Storage slots const BRIDGE_ADMIN_SLOT=word("miden::agglayer::bridge::admin") const GER_MANAGER_SLOT=word("miden::agglayer::bridge::ger_manager") -const IS_REGISTERED_FLAG=1 +const GER_STORAGE_SLOT=word("miden::agglayer::bridge::ger") +const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") + +# Flags +const GER_KNOWN_FLAG=1 +const IS_FAUCET_REGISTERED_FLAG=1 # PUBLIC INTERFACE # ================================================================================================= +#! 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. +#! +#! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +pub proc update_ger + # compute hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) + # inputs: [B, A] => output: hash(A || B) + 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)] + + swapw + # => [GER_HASH, VALUE, pad(12)] + + push.GER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_HASH, VALUE, pad(12)] + + exec.native_account::set_map_item + # => [OLD_VALUE, pad(12)] + dropw + # => [pad(16)] +end + +#! Asserts that the provided GER is valid (exists in storage). +#! +#! Computes hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) and looks up the hash in +#! the GER storage map. Panics if the GER has never been stored. +#! +#! Inputs: [GER_ROOT[8]] +#! Outputs: [] +#! +#! Panics if: +#! - the GER is not found in storage. +#! +#! Invocation: exec +pub proc assert_valid_ger + # compute hash(GER) + exec.rpo256::merge + # => [GER_HASH] + + push.GER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_HASH] + + exec.active_account::get_map_item + # => [VALUE] + + # 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_eqw.err=ERR_GER_NOT_FOUND + # => [] +end + #! Registers a faucet in the bridge's faucet registry. #! #! Writes `KEY -> [1, 0, 0, 0]` into the `faucet_registry` map, where @@ -36,11 +106,11 @@ pub proc register_faucet # => [faucet_id_prefix, faucet_id_suffix, pad(14)] # set_map_item expects [slot_id(2), KEY(4), VALUE(4)] and returns [OLD_VALUE(4)]. - push.IS_REGISTERED_FLAG - # => [IS_REGISTERED_FLAG, slot_id_prefix, slot_id_suffix, pad(14)] + push.IS_FAUCET_REGISTERED_FLAG + # => [IS_FAUCET_REGISTERED_FLAG, slot_id_prefix, slot_id_suffix, pad(14)] movdn.7 - # => [[slot_id_prefix, slot_id_suffix, 0, 0], [0, 0, 0, IS_REGISTERED_FLAG], pad(9)] + # => [[slot_id_prefix, slot_id_suffix, 0, 0], [0, 0, 0, IS_FAUCET_REGISTERED_FLAG], pad(9)] # Place slot ID on top push.FAUCET_REGISTRY_SLOT[0..2] @@ -52,6 +122,34 @@ pub proc register_faucet dropw end +#! Asserts that a faucet is registered in the bridge's faucet registry. +#! +#! Looks up the faucet ID in the faucet registry map and asserts the registration +#! flag is set. +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix] +#! Outputs: [] +#! +#! Panics if: +#! - the faucet is not registered in the faucet registry. +#! +#! Invocation: exec +pub proc assert_faucet_registered + # Build KEY = [faucet_id_prefix, faucet_id_suffix, 0, 0] + push.0.0 + movup.3 movup.3 + # => [faucet_id_prefix, faucet_id_suffix, 0, 0] + + push.FAUCET_REGISTRY_SLOT[0..2] + exec.active_account::get_map_item + # => [VALUE(4)] + + # the stored word must be [0, 0, 0, 1] for registered faucets + drop drop drop + assert.err=ERR_FAUCET_NOT_REGISTERED + # => [] +end + #! Asserts that the note sender matches the bridge admin stored in account storage. #! #! Reads the bridge admin account ID from BRIDGE_ADMIN_SLOT and compares it against diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm similarity index 54% rename from crates/miden-agglayer/asm/bridge/crypto_utils.masm rename to crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index a0d92bc582..53a17ded37 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -1,7 +1,9 @@ +use miden::agglayer::bridge::bridge_config +use miden::agglayer::bridge::leaf_utils +use miden::agglayer::common::utils use miden::core::crypto::hashes::keccak256 -use miden::core::word -use miden::agglayer::utils use miden::core::mem +use miden::core::word # TYPE ALIASES # ================================================================================================= @@ -10,23 +12,26 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } type DoubleWord = struct { word_lo: BeWord, word_hi: BeWord } type MemoryAddress = u32 -# CONSTANTS +# ERRORS # ================================================================================================= -# the number of bytes in the leaf data to hash (matches Solidity's abi.encodePacked output) -const LEAF_DATA_BYTES = 113 - -# the number of words (4 felts each) in the advice map data -const LEAF_DATA_NUM_WORDS = 8 +const ERR_BRIDGE_NOT_MAINNET = "bridge not mainnet" +const ERR_LEADING_BITS_NON_ZERO = "leading bits of global index must be zero" +const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet deposit" +const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: provided SMT root does not match the computed root" -# the memory address where leaf data is stored -const LEAF_DATA_START_PTR = 0 +# CONSTANTS +# ================================================================================================= -# the local memory offset where we store the leaf data start pointer -const PACKING_START_PTR_LOCAL= 0 +# Memory pointers for proof data layout +const PROOF_DATA_PTR = 0 +const SMT_PROOF_LOCAL_EXIT_ROOT_PTR = 0 # local SMT proof is first +const GLOBAL_INDEX_PTR = PROOF_DATA_PTR + 2 * 256 # 512 +const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 # 520 +const MAINNET_EXIT_ROOT_PTR = EXIT_ROOTS_PTR # it's the first exit root -# the number of elements to pack (113 bytes = 29 elements, rounding up from 28.25) -const PACKED_DATA_NUM_ELEMENTS = 29 +# the memory address where leaf data is stored for get_leaf_value +const LEAF_DATA_START_PTR = 0 # The offset of the first half of the current Keccak256 hash value in the local memory of the # `calculate_root` procedure. @@ -36,61 +41,98 @@ const CUR_HASH_LO_LOCAL = 0 # `calculate_root` procedure. const CUR_HASH_HI_LOCAL = 4 +# Data sizes +const PROOF_DATA_WORD_LEN = 134 +# the number of words (4 felts each) in the advice map leaf data +const LEAF_DATA_NUM_WORDS = 8 + # PUBLIC INTERFACE # ================================================================================================= -#! Given the leaf data key, loads the leaf data from advice map to memory, packs the data in-place, -#! and computes the leaf value by hashing the packed bytes. +#! Computes the leaf value and verifies it against the AggLayer bridge state. +#! +#! Verification is delegated to `verify_leaf` to mimic the AggLayer Solidity contracts. +#! The steps involved in verification are: +#! 1. Compute the GER from the mainnet and rollup exit roots. +#! 2. Assert that the computed GER is valid (exists in storage). +#! 3. Process the global index to determine if it's a mainnet or rollup deposit. +#! 4. Verify the Merkle proof for the provided leaf-index tuple against the computed GER. #! #! Inputs: -#! Operand stack: [LEAF_DATA_KEY] +#! Operand stack: [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)] #! Advice map: { +#! PROOF_DATA_KEY => [ +#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) +#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts) +#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts) +#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) +#! ], #! LEAF_DATA_KEY => [ -#! leafType[1], // Leaf type (1 felt, uint8) +#! leafType[1], // Leaf type (1 felt, uint32) #! originNetwork[1], // Origin network identifier (1 felt, uint32) #! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) -#! metadata_hash[8], // Metadata hash (8 felts, bytes32 as 8 u32 felts) +#! metadata[8], // ABI encoded metadata (8 felts, fixed size) #! padding[3], // padding (3 felts) - not used in the hash #! ], #! } -#! Outputs: [LEAF_VALUE[8]] #! -#! Invocation: exec -pub proc get_leaf_value(leaf_data_key: BeWord) -> DoubleWord - adv.push_mapval - # => [LEAF_DATA_KEY] - - push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS - exec.mem::pipe_preimage_to_memory drop - # => [] - - # compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR - push.LEAF_DATA_START_PTR - exec.compute_leaf_value - # => [LEAF_VALUE[8]] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the computed GER is invalid (never injected). +#! - the global index is invalid. +#! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid. +#! +#! Invocation: call +pub proc verify_leaf_bridge + # get the leaf value. We have all the necessary leaf data in the advice map + exec.get_leaf_value + # => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(8)] + + movupw.3 dropw + # => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(4)] + + # delegate proof verification + exec.verify_leaf + # => [pad(16)] end -#! Given a memory address where the unpacked leaf data starts, packs the leaf data in-place, and -#! computes the leaf value by hashing the packed bytes. +#! Assert the global index is valid for a mainnet deposit. #! -#! Inputs: [LEAF_DATA_START_PTR] -#! Outputs: [LEAF_VALUE[8]] +#! Each element of the global index is a LE-packed u32 felt (as produced by +#! `bytes_to_packed_u32_felts` / `GlobalIndex::to_elements()`). +#! +#! Inputs: [GLOBAL_INDEX[8]] +#! Outputs: [leaf_index] +#! +#! Panics if: +#! - the leading bits of the global index are not zero. +#! - the mainnet flag is not 1. +#! - the rollup index is not 0. #! #! Invocation: exec -pub proc compute_leaf_value(leaf_data_start_ptr: MemoryAddress) -> DoubleWord - dup - # => [leaf_data_start_ptr, leaf_data_start_ptr] - exec.pack_leaf_data - # => [leaf_data_start_ptr] - - push.LEAF_DATA_BYTES swap - # => [start_ptr, byte_len] - - exec.keccak256::hash_bytes - # => [LEAF_VALUE[8]] +pub proc process_global_index_mainnet + # for v0.1, let's only implement the mainnet branch + # the top 191 bits of the global index are zero + repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end + + # the next element is the mainnet flag (LE-packed u32) + # byte-swap to get the BE value, then assert it is exactly 1 + # => [mainnet_flag_le, rollup_index_le, leaf_index_le, ...] + exec.utils::swap_u32_bytes + assert.err=ERR_BRIDGE_NOT_MAINNET + + # the next element is the rollup index, must be zero for a mainnet deposit + # (zero is byte-order-independent, so no swap needed) + assertz.err=ERR_ROLLUP_INDEX_NON_ZERO + + # the leaf index is the last element; byte-swap from LE to BE to get the actual index + exec.utils::swap_u32_bytes + # => [leaf_index] end #! Computes the Global Exit Tree (GET) root from the mainnet and rollup exit roots. @@ -150,6 +192,100 @@ end # HELPER PROCEDURES # ================================================================================================= +#! Given the leaf data key, loads the leaf data from advice map to memory, packs the data in-place, +#! and computes the leaf value by hashing the packed bytes. +#! +#! Inputs: +#! Operand stack: [LEAF_DATA_KEY] +#! Advice map: { +#! LEAF_DATA_KEY => [ +#! leafType[1], // Leaf type (1 felt, uint8) +#! originNetwork[1], // Origin network identifier (1 felt, uint32) +#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) +#! destinationNetwork[1], // Destination network identifier (1 felt, uint32) +#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) +#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) +#! metadata_hash[8], // Metadata hash (8 felts, bytes32 as 8 u32 felts) +#! padding[3], // padding (3 felts) - not used in the hash +#! ], +#! } +#! Outputs: [LEAF_VALUE[8]] +#! +#! Invocation: exec +pub proc get_leaf_value(leaf_data_key: BeWord) -> DoubleWord + adv.push_mapval + # => [LEAF_DATA_KEY] + + push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS + exec.mem::pipe_preimage_to_memory drop + # => [] + + # compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR + push.LEAF_DATA_START_PTR + exec.leaf_utils::compute_leaf_value + # => [LEAF_VALUE[8]] +end + +#! Verify leaf and checks that it has not been claimed. +#! +#! Inputs: +#! Operand stack: [LEAF_VALUE[8], PROOF_DATA_KEY] +#! +#! Outputs: [] +#! +#! Panics if: +#! - the computed GER is invalid (never injected). +#! - the global index is invalid. +#! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid. +#! +#! Invocation: exec +proc verify_leaf + movupw.2 + # load proof data from the advice map into memory + adv.push_mapval + # => [PROOF_DATA_KEY, LEAF_VALUE[8]] + + push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR push.PROOF_DATA_WORD_LEN + exec.mem::pipe_preimage_to_memory drop + + # 1. compute GER from mainnet + rollup exit roots + push.EXIT_ROOTS_PTR + # => [exit_roots_ptr, LEAF_VALUE[8]] + exec.compute_ger + # => [GER[8], LEAF_VALUE[8]] + + # 2. assert the GER is valid + exec.bridge_config::assert_valid_ger + # => [LEAF_VALUE[8]] + + # 3. load global index from memory + padw mem_loadw_le.GLOBAL_INDEX_PTR + padw push.GLOBAL_INDEX_PTR add.4 mem_loadw_le swapw + # => [GLOBAL_INDEX[8], LEAF_VALUE[8]] + + # to see if we're dealing with a deposit from mainnet or from a rollup, process the global index + # TODO currently only implemented for mainnet deposits (mainnet flag must be 1) + exec.process_global_index_mainnet + # => [leaf_index, LEAF_VALUE[8]] + + # load the pointers to the merkle proof and root, to pass to `verify_merkle_proof` + push.MAINNET_EXIT_ROOT_PTR swap + push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR + # => [smt_proof_ptr, leaf_index, mainnet_exit_root_ptr, LEAF_VALUE[8]] + + # prepare the stack for the verify_merkle_proof procedure: move the pointers deep in the stack + movdn.10 movdn.10 movdn.10 + # => [LEAF_VALUE[8], smt_proof_ptr, leaf_index, mainnet_exit_root_ptr] + + exec.verify_merkle_proof + # => [verification_flag] + + # verify_merkle_proof procedure returns `true` if the verification was successful and `false` + # otherwise. Assert that `true` was returned. + assert.err=ERR_SMT_ROOT_VERIFICATION_FAILED + # => [] +end + #! Computes the root of the SMT based on the provided Merkle path, leaf value and leaf index. #! #! Inputs: [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx] @@ -224,114 +360,3 @@ proc calculate_root( padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ROOT_LO, ROOT_HI] end - -#! Packs the raw leaf data by shifting left 3 bytes to match Solidity's abi.encodePacked format. -#! -#! The raw data has leafType occupying 4 bytes (as a u32 felt) but Solidity's abi.encodePacked -#! only uses 1 byte for uint8 leafType. This procedure shifts all data left by 3 bytes so that: -#! - Byte 0: leafType (1 byte) -#! - Bytes 1-4: originNetwork (4 bytes) -#! - etc. -#! -#! The Keccak precompile expects u32 values packed in little-endian byte order. -#! For each packed element, we drop the leading 3 bytes and rebuild the u32 so that -#! bytes [b0, b1, b2, b3] map to u32::from_le_bytes([b0, b1, b2, b3]). -#! With little-endian input limbs, the first byte comes from the MSB of `curr` and -#! the next three bytes come from the LSBs of `next`: -#! packed = ((curr >> 24) & 0xFF) -#! | (next & 0xFF) << 8 -#! | ((next >> 8) & 0xFF) << 16 -#! | ((next >> 16) & 0xFF) << 24 -#! -#! To help visualize the packing process, consider that each field element represents a 4-byte -#! value [u8; 4] (LE). -#! Memory before is: -#! ptr+0: 1 felt: [a, b, c, d] -#! ptr+1: 1 felt: [e, f, g, h] -#! ptr+2..6: 5 felts: [i, j, k, l, m, ...] -#! -#! Memory after: -#! ptr+0: 1 felt: [d, e, f, g] -#! ptr+1: 1 felt: [h, i, j, k] -#! ptr+2..6: 5 felts: [l, ...] -#! -#! Inputs: [leaf_data_start_ptr] -#! Outputs: [] -#! -#! Invocation: exec -@locals(1) # start_ptr -pub proc pack_leaf_data(leaf_data_start_ptr: MemoryAddress) - loc_store.PACKING_START_PTR_LOCAL - # => [] - - # initialize loop counter to 0 - push.0 - - # push initial condition (true) to enter the loop - push.1 - - # loop through elements from 0 to PACKED_DATA_NUM_ELEMENTS - 1 (28) - while.true - # => [counter] - - # compute source address: packing_start_ptr + counter - dup loc_load.PACKING_START_PTR_LOCAL add - # => [src_addr, counter] - - # load current element - mem_load - # => [curr_elem, counter] - - # extract MSB (upper 8 bits) which becomes the first little-endian byte - dup u32shr.24 - # => [curr_msb, curr_elem, counter] - - # compute source address for next element (counter + 1) - dup.2 loc_load.PACKING_START_PTR_LOCAL add add.1 - # => [next_src_addr, curr_lsb, curr_elem, counter] - - # load next element - mem_load - # => [next_elem, curr_lsb, curr_elem, counter] - - # keep curr_msb on top for combination - swap - # => [curr_msb, next_elem, curr_elem, counter] - - # add next byte0 (bits 0..7) into bits 8..15 - dup.1 u32and.0xFF u32shl.8 u32or - # => [partial, next_elem, curr_elem, counter] - - # add next byte1 (bits 8..15) into bits 16..23 - dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or - # => [partial, next_elem, curr_elem, counter] - - # add next byte2 (bits 16..23) into bits 24..31 - dup.1 u32shr.16 u32and.0xFF u32shl.24 u32or - # => [packed_elem, next_elem, curr_elem, counter] - - # drop the next and current elements (no longer needed) - movdn.2 drop drop - # => [packed_elem, counter] - - # compute destination address: packing_start_ptr + counter (in-place) - dup.1 loc_load.PACKING_START_PTR_LOCAL add - # => [dest_addr, packed_elem, counter] - - # store packed element - mem_store - # => [counter] - - # increment counter - add.1 - # => [counter + 1] - - # check if we should continue (counter < PACKED_DATA_NUM_ELEMENTS) - dup push.PACKED_DATA_NUM_ELEMENTS lt - # => [should_continue, counter] - end - # => [counter] - - drop - # => [] -end \ No newline at end of file diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm similarity index 93% rename from crates/miden-agglayer/asm/bridge/bridge_out.masm rename to crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm index 1de5a619b6..1383bf5d1f 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_out.masm @@ -11,10 +11,11 @@ use miden::protocol::output_note use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 use miden::core::word -use miden::agglayer::utils -use miden::agglayer::agglayer_faucet -use miden::agglayer::crypto_utils -use miden::agglayer::mmr_frontier32_keccak +use miden::agglayer::common::utils +use miden::agglayer::faucet -> agglayer_faucet +use miden::agglayer::bridge::bridge_config +use miden::agglayer::bridge::leaf_utils +use miden::agglayer::bridge::mmr_frontier32_keccak # TYPE ALIASES @@ -26,28 +27,22 @@ type MemoryAddress = u32 # CONSTANTS # ================================================================================================= -# bridge_out memory locals -const BRIDGE_OUT_BURN_ASSET_LOC=0 -const DESTINATION_ADDRESS_0_LOC=4 -const DESTINATION_ADDRESS_1_LOC=5 -const DESTINATION_ADDRESS_2_LOC=6 -const DESTINATION_ADDRESS_3_LOC=7 -const DESTINATION_ADDRESS_4_LOC=8 -const DESTINATION_NETWORK_LOC=9 - -# create_burn_note memory locals -const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 -const ATTACHMENT_LOC=4 -const ATTACHMENT_SCHEME_LOC=8 -const ATTACHMENT_KIND_LOC=9 +# Storage slot constants for the LET (Local Exit Tree). +# The frontier is stored as a double-word array in a map slot. +# The root and num_leaves are stored in separate value slots. +const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") +const LET_ROOT_LO_SLOT=word("miden::agglayer::let::root_lo") +const LET_ROOT_HI_SLOT=word("miden::agglayer::let::root_hi") +const LET_NUM_LEAVES_SLOT=word("miden::agglayer::let::num_leaves") +# Memory pointers const LEAF_DATA_START_PTR=44 - # Memory pointer for loading the LET (Local Exit Tree) frontier into memory. # The memory layout at this address matches what append_and_update_frontier expects: # [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] const LET_FRONTIER_MEM_PTR=100 +# Leaf data field offsets (relative to LEAF_DATA_START_PTR) const LEAF_TYPE_OFFSET=0 const ORIGIN_NETWORK_OFFSET=1 const ORIGIN_TOKEN_ADDRESS_OFFSET=2 @@ -57,24 +52,25 @@ const AMOUNT_OFFSET=13 const METADATA_HASH_OFFSET=21 const PADDING_OFFSET=29 -const PUBLIC_NOTE=1 -const BURN_NOTE_NUM_STORAGE_ITEMS=0 +# bridge_out memory locals +const BRIDGE_OUT_BURN_ASSET_LOC=0 +const DESTINATION_ADDRESS_0_LOC=4 +const DESTINATION_ADDRESS_1_LOC=5 +const DESTINATION_ADDRESS_2_LOC=6 +const DESTINATION_ADDRESS_3_LOC=7 +const DESTINATION_ADDRESS_4_LOC=8 +const DESTINATION_NETWORK_LOC=9 -# Storage slot constants for the LET (Local Exit Tree). -# The frontier is stored as a double-word array in a map slot. -# The root and num_leaves are stored in separate value slots. -const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") -const LET_ROOT_LO_SLOT=word("miden::agglayer::let::root_lo") -const LET_ROOT_HI_SLOT=word("miden::agglayer::let::root_hi") -const LET_NUM_LEAVES_SLOT=word("miden::agglayer::let::num_leaves") -const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") +# create_burn_note memory locals +const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 +const ATTACHMENT_LOC=4 +const ATTACHMENT_SCHEME_LOC=8 +const ATTACHMENT_KIND_LOC=9 +# Other constants const LEAF_TYPE_ASSET=0 - -# ERRORS -# ================================================================================================= - -const ERR_FAUCET_NOT_REGISTERED="faucet is not registered in the bridge's faucet registry" +const PUBLIC_NOTE=1 +const BURN_NOTE_NUM_STORAGE_ITEMS=0 # PUBLIC INTERFACE # ================================================================================================= @@ -157,7 +153,7 @@ pub proc bridge_out exec.utils::mem_store_double_word_unaligned # Explicitly zero the 3 padding felts after METADATA_HASH for - # crypto_utils::pack_leaf_data + # leaf_utils::pack_leaf_data push.0 push.LEAF_DATA_START_PTR push.PADDING_OFFSET add mem_store @@ -211,21 +207,8 @@ proc convert_asset # => [faucet_id_prefix, faucet_id_suffix, 0, amount] # --- Step 1: Assert faucet is registered --- - - # Build KEY = [faucet_id_prefix, faucet_id_suffix, 0, 0] for the map lookup. - # Duplicate faucet ID onto top of stack, then add zero padding. dup.1 dup.1 - push.0.0 - movup.3 movup.3 - # => [faucet_id_prefix, faucet_id_suffix, 0, 0, faucet_id_prefix, faucet_id_suffix, 0, amount] - - push.FAUCET_REGISTRY_SLOT[0..2] - exec.active_account::get_map_item - # => [VALUE(4), faucet_id_prefix, faucet_id_suffix, 0, amount] - - # Check flag, the stored word must be [0, 0, 0, 1] for registered faucets - drop drop drop - assert.err=ERR_FAUCET_NOT_REGISTERED + exec.bridge_config::assert_faucet_registered # => [faucet_id_prefix, faucet_id_suffix, 0, amount] # --- Step 2: FPI to faucet's asset_to_origin_asset --- @@ -268,7 +251,7 @@ end #! #! Invocation: exec proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress) - exec.crypto_utils::compute_leaf_value + exec.leaf_utils::compute_leaf_value # => [LEAF_VALUE_LO, LEAF_VALUE_HI] # Load the LET frontier from storage into memory at LET_FRONTIER_MEM_PTR diff --git a/crates/miden-agglayer/asm/bridge/canonical_zeros.masm b/crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm similarity index 99% rename from crates/miden-agglayer/asm/bridge/canonical_zeros.masm rename to crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm index e693c4fa16..1915726cee 100644 --- a/crates/miden-agglayer/asm/bridge/canonical_zeros.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/canonical_zeros.masm @@ -101,7 +101,7 @@ const ZERO_30_R = [1078065138, 2904706143, 1223587258, 1350312851] const ZERO_31_L = [2840985724, 1653344606, 4049365781, 2389186238] const ZERO_31_R = [3759582231, 2660540036, 1648733876, 2340505732] -use ::miden::agglayer::mmr_frontier32_keccak::mem_store_double_word +use ::miden::agglayer::common::utils::mem_store_double_word #! Inputs: [zeros_ptr] #! Outputs: [] diff --git a/crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm b/crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm new file mode 100644 index 0000000000..673b3a4fda --- /dev/null +++ b/crates/miden-agglayer/asm/agglayer/bridge/leaf_utils.masm @@ -0,0 +1,154 @@ +use miden::core::crypto::hashes::keccak256 + +# TYPE ALIASES +# ================================================================================================= + +type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } +type DoubleWord = struct { word_lo: BeWord, word_hi: BeWord } +type MemoryAddress = u32 + +# CONSTANTS +# ================================================================================================= + +# the number of bytes in the leaf data to hash (matches Solidity's abi.encodePacked output) +const LEAF_DATA_BYTES = 113 + +# the local memory offset where we store the leaf data start pointer +const PACKING_START_PTR_LOCAL= 0 + +# the number of elements to pack (113 bytes = 29 elements, rounding up from 28.25) +const PACKED_DATA_NUM_ELEMENTS = 29 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Given a memory address where the unpacked leaf data starts, packs the leaf data in-place, and +#! computes the leaf value by hashing the packed bytes. +#! +#! Inputs: [LEAF_DATA_START_PTR] +#! Outputs: [LEAF_VALUE[8]] +#! +#! Invocation: exec +pub proc compute_leaf_value(leaf_data_start_ptr: MemoryAddress) -> DoubleWord + dup + # => [leaf_data_start_ptr, leaf_data_start_ptr] + exec.pack_leaf_data + # => [leaf_data_start_ptr] + + push.LEAF_DATA_BYTES swap + # => [start_ptr, byte_len] + + exec.keccak256::hash_bytes + # => [LEAF_VALUE[8]] +end + +#! Packs the raw leaf data by shifting left 3 bytes to match Solidity's abi.encodePacked format. +#! +#! The raw data has leafType occupying 4 bytes (as a u32 felt) but Solidity's abi.encodePacked +#! only uses 1 byte for uint8 leafType. This procedure shifts all data left by 3 bytes so that: +#! - Byte 0: leafType (1 byte) +#! - Bytes 1-4: originNetwork (4 bytes) +#! - etc. +#! +#! The Keccak precompile expects u32 values packed in little-endian byte order. +#! For each packed element, we drop the leading 3 bytes and rebuild the u32 so that +#! bytes [b0, b1, b2, b3] map to u32::from_le_bytes([b0, b1, b2, b3]). +#! With little-endian input limbs, the first byte comes from the MSB of `curr` and +#! the next three bytes come from the LSBs of `next`: +#! packed = ((curr >> 24) & 0xFF) +#! | (next & 0xFF) << 8 +#! | ((next >> 8) & 0xFF) << 16 +#! | ((next >> 16) & 0xFF) << 24 +#! +#! To help visualize the packing process, consider that each field element represents a 4-byte +#! value [u8; 4] (LE). +#! Memory before is: +#! ptr+0: 1 felt: [a, b, c, d] +#! ptr+1: 1 felt: [e, f, g, h] +#! ptr+2..6: 5 felts: [i, j, k, l, m, ...] +#! +#! Memory after: +#! ptr+0: 1 felt: [d, e, f, g] +#! ptr+1: 1 felt: [h, i, j, k] +#! ptr+2..6: 5 felts: [l, ...] +#! +#! Inputs: [leaf_data_start_ptr] +#! Outputs: [] +#! +#! Invocation: exec +@locals(1) # start_ptr +pub proc pack_leaf_data(leaf_data_start_ptr: MemoryAddress) + loc_store.PACKING_START_PTR_LOCAL + # => [] + + # initialize loop counter to 0 + push.0 + + # push initial condition (true) to enter the loop + push.1 + + # loop through elements from 0 to PACKED_DATA_NUM_ELEMENTS - 1 (28) + while.true + # => [counter] + + # compute source address: packing_start_ptr + counter + dup loc_load.PACKING_START_PTR_LOCAL add + # => [src_addr, counter] + + # load current element + mem_load + # => [curr_elem, counter] + + # extract MSB (upper 8 bits) which becomes the first little-endian byte + dup u32shr.24 + # => [curr_msb, curr_elem, counter] + + # compute source address for next element (counter + 1) + dup.2 loc_load.PACKING_START_PTR_LOCAL add add.1 + # => [next_src_addr, curr_lsb, curr_elem, counter] + + # load next element + mem_load + # => [next_elem, curr_lsb, curr_elem, counter] + + # keep curr_msb on top for combination + swap + # => [curr_msb, next_elem, curr_elem, counter] + + # add next byte0 (bits 0..7) into bits 8..15 + dup.1 u32and.0xFF u32shl.8 u32or + # => [partial, next_elem, curr_elem, counter] + + # add next byte1 (bits 8..15) into bits 16..23 + dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or + # => [partial, next_elem, curr_elem, counter] + + # add next byte2 (bits 16..23) into bits 24..31 + dup.1 u32shr.16 u32and.0xFF u32shl.24 u32or + # => [packed_elem, next_elem, curr_elem, counter] + + # drop the next and current elements (no longer needed) + movdn.2 drop drop + # => [packed_elem, counter] + + # compute destination address: packing_start_ptr + counter (in-place) + dup.1 loc_load.PACKING_START_PTR_LOCAL add + # => [dest_addr, packed_elem, counter] + + # store packed element + mem_store + # => [counter] + + # increment counter + add.1 + # => [counter + 1] + + # check if we should continue (counter < PACKED_DATA_NUM_ELEMENTS) + dup push.PACKED_DATA_NUM_ELEMENTS lt + # => [should_continue, counter] + end + # => [counter] + + drop + # => [] +end diff --git a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm b/crates/miden-agglayer/asm/agglayer/bridge/mmr_frontier32_keccak.masm similarity index 98% rename from crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm rename to crates/miden-agglayer/asm/agglayer/bridge/mmr_frontier32_keccak.masm index f8be97dfa6..8ff7efb453 100644 --- a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/mmr_frontier32_keccak.masm @@ -1,7 +1,7 @@ use miden::core::crypto::hashes::keccak256 -use ::miden::agglayer::canonical_zeros::load_zeros_to_memory -use ::miden::agglayer::utils::mem_store_double_word -use ::miden::agglayer::utils::mem_load_double_word +use ::miden::agglayer::bridge::canonical_zeros::load_zeros_to_memory +use ::miden::agglayer::common::utils::mem_store_double_word +use ::miden::agglayer::common::utils::mem_load_double_word # An MMR Frontier is a data structure based on an MMR, which combines some features of an MMR and an # SMT. diff --git a/crates/miden-agglayer/asm/bridge/asset_conversion.masm b/crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm similarity index 99% rename from crates/miden-agglayer/asm/bridge/asset_conversion.masm rename to crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm index 0e77613fff..1693356c0e 100644 --- a/crates/miden-agglayer/asm/bridge/asset_conversion.masm +++ b/crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm @@ -1,13 +1,8 @@ use miden::core::math::u64 use miden::core::word -use miden::agglayer::utils +use miden::agglayer::common::utils use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT -# CONSTANTS -# ================================================================================================= - -const MAX_SCALING_FACTOR=18 - # ERRORS # ================================================================================================= @@ -15,9 +10,13 @@ const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT="maximum scaling factor is 18" const ERR_X_TOO_LARGE="x must fit into 128 bits (x4..x7 must be 0)" const ERR_UNDERFLOW="x < y*10^s (underflow detected)" const ERR_REMAINDER_TOO_LARGE="remainder z must be < 10^s" - const ERR_Y_TOO_LARGE="y exceeds max fungible token amount" +# CONSTANTS +# ================================================================================================= + +const MAX_SCALING_FACTOR=18 + #! Calculate 10^scale where scale is a u8 exponent. #! #! Inputs: [scale] diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm similarity index 92% rename from crates/miden-agglayer/asm/bridge/eth_address.masm rename to crates/miden-agglayer/asm/agglayer/common/eth_address.masm index bcf418ce21..87882aeb9a 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/agglayer/common/eth_address.masm @@ -1,60 +1,22 @@ -use miden::agglayer::utils +use miden::agglayer::common::utils use miden::core::crypto::hashes::keccak256 use miden::core::word -# CONSTANTS +# ERRORS # ================================================================================================= -const U32_MAX=4294967295 -const TWO_POW_32=4294967296 - const ERR_NOT_U32="address limb is not u32" const ERR_MSB_NONZERO="most-significant 4 bytes must be zero for AccountId" const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" -# ETHEREUM ADDRESS PROCEDURES +# CONSTANTS # ================================================================================================= -#! Builds a single felt from two u32 limbs (little-endian limb order). -#! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. -#! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping -#! through u32split and comparing the limbs. -#! -#! Inputs: [lo, hi] -#! Outputs: [felt] -proc build_felt - # --- validate u32 limbs --- - u32assert2.err=ERR_NOT_U32 - # => [lo_be, hi_be] - - # limbs are little-endian bytes; swap to big-endian for building account ID - exec.utils::swap_u32_bytes - swap - exec.utils::swap_u32_bytes - swap - # => [lo, hi] - - # keep copies for the overflow check - dup.1 dup.1 - # => [lo_be, hi_be, lo_be, hi_be] - - # felt = (hi * 2^32) + lo - swap - push.TWO_POW_32 mul - add - # => [felt, lo_be, hi_be] - - # ensure no reduction mod p happened: - # split felt back into (hi, lo) and compare to inputs - dup u32split - # => [hi2, lo2, felt, lo_be, hi_be] - - movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD - # => [lo2, felt, lo] +const U32_MAX=4294967295 +const TWO_POW_32=4294967296 - movup.2 assert_eq.err=ERR_FELT_OUT_OF_FIELD - # => [felt] -end +# PUBLIC INTERFACE +# ================================================================================================= #! Converts an Ethereum address format (address[5] type) back into an AccountId [prefix, suffix] type. #! @@ -98,3 +60,47 @@ pub proc to_account_id exec.build_felt # => [prefix, suffix] end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Builds a single felt from two u32 limbs (little-endian limb order). +#! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. +#! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping +#! through u32split and comparing the limbs. +#! +#! Inputs: [lo, hi] +#! Outputs: [felt] +proc build_felt + # --- validate u32 limbs --- + u32assert2.err=ERR_NOT_U32 + # => [lo_be, hi_be] + + # limbs are little-endian bytes; swap to big-endian for building account ID + exec.utils::swap_u32_bytes + swap + exec.utils::swap_u32_bytes + swap + # => [lo, hi] + + # keep copies for the overflow check + dup.1 dup.1 + # => [lo_be, hi_be, lo_be, hi_be] + + # felt = (hi * 2^32) + lo + swap + push.TWO_POW_32 mul + add + # => [felt, lo_be, hi_be] + + # ensure no reduction mod p happened: + # split felt back into (hi, lo) and compare to inputs + dup u32split + # => [hi2, lo2, felt, lo_be, hi_be] + + movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD + # => [lo2, felt, lo] + + movup.2 assert_eq.err=ERR_FELT_OUT_OF_FIELD + # => [felt] +end diff --git a/crates/miden-agglayer/asm/bridge/utils.masm b/crates/miden-agglayer/asm/agglayer/common/utils.masm similarity index 100% rename from crates/miden-agglayer/asm/bridge/utils.masm rename to crates/miden-agglayer/asm/agglayer/common/utils.masm diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm similarity index 97% rename from crates/miden-agglayer/asm/bridge/agglayer_faucet.masm rename to crates/miden-agglayer/asm/agglayer/faucet/mod.masm index 9c6175d455..f62567585c 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -1,8 +1,8 @@ -use miden::agglayer::bridge_in +use miden::agglayer::bridge::bridge_in use miden::core::sys -use miden::agglayer::utils -use miden::agglayer::asset_conversion -use miden::agglayer::eth_address +use miden::agglayer::common::utils +use miden::agglayer::common::asset_conversion +use miden::agglayer::common::eth_address use miden::protocol::active_account use miden::protocol::active_note use miden::standards::faucets @@ -12,36 +12,40 @@ use miden::core::mem use miden::core::word +# ERRORS +# ================================================================================================= + +const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" + # CONSTANTS # ================================================================================================= +# Storage slots # The slot in this component's storage layout where the bridge account ID is stored. const BRIDGE_ID_SLOT = word("miden::agglayer::faucet") - # Storage slots for conversion metadata. # Slot 1: [addr_felt0, addr_felt1, addr_felt2, addr_felt3] — first 4 felts of origin token address const CONVERSION_INFO_1_SLOT = word("miden::agglayer::faucet::conversion_info_1") # Slot 2: [addr_felt4, origin_network, scale, 0] — remaining address felt + origin network + scale const CONVERSION_INFO_2_SLOT = word("miden::agglayer::faucet::conversion_info_2") -const PROOF_DATA_WORD_LEN = 134 -const LEAF_DATA_WORD_LEN = 8 -const OUTPUT_NOTE_DATA_WORD_LEN = 2 - +# Memory pointers for piped advice map data const PROOF_DATA_START_PTR = 0 const LEAF_DATA_START_PTR = 536 const OUTPUT_NOTE_DATA_START_PTR = 568 -# Memory Addresses +# Memory addresses for stored keys const PROOF_DATA_KEY_MEM_ADDR = 700 const LEAF_DATA_KEY_MEM_ADDR = 704 const OUTPUT_NOTE_DATA_MEM_ADDR = 708 const CLAIM_NOTE_DATA_MEM_ADDR = 712 -const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 -const OUTPUT_NOTE_TAG_MEM_ADDR = 574 -const OUTPUT_NOTE_FAUCET_AMOUNT = 575 -const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 +# Memory addresses for output note fields (derived from leaf data layout) +const DESTINATION_ADDRESS_0 = 544 +const DESTINATION_ADDRESS_1 = 545 +const DESTINATION_ADDRESS_2 = 546 +const DESTINATION_ADDRESS_3 = 547 +const DESTINATION_ADDRESS_4 = 548 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 549 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 550 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 = 551 @@ -50,12 +54,11 @@ const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = 553 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556 - -const DESTINATION_ADDRESS_0 = 544 -const DESTINATION_ADDRESS_1 = 545 -const DESTINATION_ADDRESS_2 = 546 -const DESTINATION_ADDRESS_3 = 547 -const DESTINATION_ADDRESS_4 = 548 +const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 +const OUTPUT_NOTE_TAG_MEM_ADDR = 574 +const OUTPUT_NOTE_FAUCET_AMOUNT = 575 +const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 +const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 # Memory locals in claim const CLAIM_PREFIX_MEM_LOC = 8 @@ -67,18 +70,17 @@ const CLAIM_AMOUNT_MEM_LOC_1 = 4 const BUILD_P2ID_AMOUNT_MEM_LOC_0 = 0 const BUILD_P2ID_AMOUNT_MEM_LOC_1 = 4 +# Data sizes +const PROOF_DATA_WORD_LEN = 134 +const LEAF_DATA_WORD_LEN = 8 +const OUTPUT_NOTE_DATA_WORD_LEN = 2 + # P2ID output note constants const P2ID_NOTE_NUM_STORAGE_ITEMS = 2 const OUTPUT_NOTE_TYPE_PUBLIC = 1 const OUTPUT_NOTE_AUX = 0 -const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 -# ERRORS -# ================================================================================================= - -const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" - -# CONVERSION METADATA HELPERS +# PUBLIC INTERFACE # ================================================================================================= #! Returns the origin token address (5 felts) from faucet conversion storage. diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm deleted file mode 100644 index 50ef55aae6..0000000000 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ /dev/null @@ -1,239 +0,0 @@ -use miden::agglayer::crypto_utils -use miden::agglayer::utils -use miden::core::crypto::hashes::keccak256 -use miden::core::crypto::hashes::rpo256 -use miden::core::mem -use miden::protocol::active_account -use miden::protocol::native_account - -# ERRORS -# ================================================================================================= - -const ERR_BRIDGE_NOT_MAINNET = "bridge not mainnet" -const ERR_GER_NOT_FOUND = "GER not found in storage" -const ERR_LEADING_BITS_NON_ZERO = "leading bits of global index must be zero" -const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet deposit" -const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: provided SMT root does not match the computed root" - -# CONSTANTS -# ================================================================================================= - -const PROOF_DATA_PTR = 0 -const PROOF_DATA_WORD_LEN = 134 -const SMT_PROOF_LOCAL_EXIT_ROOT_PTR = 0 # local SMT proof is first -const GLOBAL_INDEX_PTR = PROOF_DATA_PTR + 2 * 256 # 512 -const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 # 520 -const MAINNET_EXIT_ROOT_PTR = EXIT_ROOTS_PTR # it's the first exit root - -const GER_STORAGE_SLOT=word("miden::agglayer::bridge::ger") -const GER_KNOWN_FLAG=1 - -# PUBLIC INTERFACE -# ================================================================================================= - -#! 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. -#! -#! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)] -#! Outputs: [pad(16)] -#! -#! Invocation: call -pub proc update_ger - # compute hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) - # inputs: [B, A] => output: hash(A || B) - 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)] - - swapw - # => [GER_HASH, VALUE, pad(12)] - - push.GER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_HASH, VALUE, pad(12)] - - exec.native_account::set_map_item - # => [OLD_VALUE, pad(12)] - dropw - # => [pad(16)] -end - -#! Computes the leaf value and verifies it against the AggLayer bridge state. -#! -#! Verification is delegated to `verify_leaf` to mimic the AggLayer Solidity contracts. -#! The steps involved in verification are: -#! 1. Compute the GER from the mainnet and rollup exit roots. -#! 2. Assert that the computed GER is valid (exists in storage). -#! 3. Process the global index to determine if it's a mainnet or rollup deposit. -#! 4. Verify the Merkle proof for the provided leaf-index tuple against the computed GER. -#! -#! Inputs: -#! Operand stack: [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)] -#! Advice map: { -#! PROOF_DATA_KEY => [ -#! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) -#! smtProofRollupExitRoot[256], // SMT proof for rollup exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) -#! globalIndex[8], // Global index (8 felts, uint256 as 8 u32 felts) -#! mainnetExitRoot[8], // Mainnet exit root hash (8 felts, bytes32 as 8 u32 felts) -#! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) -#! ], -#! LEAF_DATA_KEY => [ -#! leafType[1], // Leaf type (1 felt, uint32) -#! originNetwork[1], // Origin network identifier (1 felt, uint32) -#! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) -#! destinationNetwork[1], // Destination network identifier (1 felt, uint32) -#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) -#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) -#! metadata[8], // ABI encoded metadata (8 felts, fixed size) -#! padding[3], // padding (3 felts) - not used in the hash -#! ], -#! } -#! -#! Outputs: [pad(16)] -#! -#! Panics if: -#! - the computed GER is invalid (never injected). -#! - the global index is invalid. -#! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid. -#! -#! Invocation: call -pub proc verify_leaf_bridge - # get the leaf value. We have all the necessary leaf data in the advice map - exec.crypto_utils::get_leaf_value - # => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(8)] - - movupw.3 dropw - # => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(4)] - - # delegate proof verification - exec.verify_leaf - # => [pad(16)] -end - -# HELPER PROCEDURES -# ================================================================================================= - -#! Asserts that the provided GER is valid (exists in storage). -#! -#! Inputs: [GER_ROOT[8]] -#! Outputs: [] -#! -#! Invocation: exec -proc assert_valid_ger - # compute hash(GER) - exec.rpo256::merge - # => [GER_HASH] - - push.GER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_HASH] - - exec.active_account::get_map_item - # => [VALUE] - - # 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_eqw.err=ERR_GER_NOT_FOUND - # => [] -end - -#! Assert the global index is valid for a mainnet deposit. -#! -#! Each element of the global index is a LE-packed u32 felt (as produced by -#! `bytes_to_packed_u32_felts` / `GlobalIndex::to_elements()`). -#! -#! Inputs: [GLOBAL_INDEX[8]] -#! Outputs: [leaf_index] -#! -#! Panics if: -#! - the leading bits of the global index are not zero. -#! - the mainnet flag is not 1. -#! - the rollup index is not 0. -#! -#! Invocation: exec -pub proc process_global_index_mainnet - # for v0.1, let's only implement the mainnet branch - # the top 191 bits of the global index are zero - repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end - - # the next element is the mainnet flag (LE-packed u32) - # byte-swap to get the BE value, then assert it is exactly 1 - # => [mainnet_flag_le, rollup_index_le, leaf_index_le, ...] - exec.utils::swap_u32_bytes - assert.err=ERR_BRIDGE_NOT_MAINNET - - # the next element is the rollup index, must be zero for a mainnet deposit - # (zero is byte-order-independent, so no swap needed) - assertz.err=ERR_ROLLUP_INDEX_NON_ZERO - - # the leaf index is the last element; byte-swap from LE to BE to get the actual index - exec.utils::swap_u32_bytes - # => [leaf_index] -end - -#! Verify leaf and checks that it has not been claimed. -#! -#! Inputs: -#! Operand stack: [LEAF_VALUE[8], PROOF_DATA_KEY] -#! -#! Outputs: [] -#! -#! Panics if: -#! - the computed GER is invalid (never injected). -#! - the global index is invalid. -#! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid. -#! -#! Invocation: exec -proc verify_leaf - movupw.2 - # load proof data from the advice map into memory - adv.push_mapval - # => [PROOF_DATA_KEY, LEAF_VALUE[8]] - - push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR push.PROOF_DATA_WORD_LEN - exec.mem::pipe_preimage_to_memory drop - - # 1. compute GER from mainnet + rollup exit roots - push.EXIT_ROOTS_PTR - # => [exit_roots_ptr, LEAF_VALUE[8]] - exec.crypto_utils::compute_ger - # => [GER[8], LEAF_VALUE[8]] - - # 2. assert the GER is valid - exec.assert_valid_ger - # => [LEAF_VALUE[8]] - - # 3. load global index from memory - padw mem_loadw_le.GLOBAL_INDEX_PTR - padw push.GLOBAL_INDEX_PTR add.4 mem_loadw_le swapw - # => [GLOBAL_INDEX[8], LEAF_VALUE[8]] - - # to see if we're dealing with a deposit from mainnet or from a rollup, process the global index - # TODO currently only implemented for mainnet deposits (mainnet flag must be 1) - exec.process_global_index_mainnet - # => [leaf_index, LEAF_VALUE[8]] - - # load the pointers to the merkle proof and root, to pass to `verify_merkle_proof` - push.MAINNET_EXIT_ROOT_PTR swap - push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR - # => [smt_proof_ptr, leaf_index, mainnet_exit_root_ptr, LEAF_VALUE[8]] - - # prepare the stack for the crypto_utils::verify_merkle_proof procedure: move the pointers deep - # in the stack - movdn.10 movdn.10 movdn.10 - # => [LEAF_VALUE[8], smt_proof_ptr, leaf_index, mainnet_exit_root_ptr] - - # delegate verification to crypto_utils::verify_merkle_proof - exec.crypto_utils::verify_merkle_proof - # => [verification_flag] - - # verify_merkle_proof procedure returns `true` if the verification was successful and `false` - # otherwise. Assert that `true` was returned. - assert.err=ERR_SMT_ROOT_VERIFICATION_FAILED - # => [] -end diff --git a/crates/miden-agglayer/asm/bridge/local_exit_tree.masm b/crates/miden-agglayer/asm/bridge/local_exit_tree.masm deleted file mode 100644 index 89e744507b..0000000000 --- a/crates/miden-agglayer/asm/bridge/local_exit_tree.masm +++ /dev/null @@ -1,120 +0,0 @@ -use miden::protocol::active_account -use miden::protocol::native_account - -# CONSTANTS -# ================================================================================================= - -const MMR_PTR=42 -const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") - -#! Adds a leaf to the MMR frontier using Keccak hashing (stubbed implementation). -#! -#! This is a stubbed implementation that currently drops all inputs without performing -#! the actual MMR frontier addition operation. -#! -#! Inputs: [LEAF[1], LEAF[0], mmr_ptr] -#! Outputs: [] -#! -#! Where: -#! - LEAF[1], LEAF[0] are the leaf data to add to the MMR frontier. -#! - mmr_ptr is the pointer to the MMR frontier data structure. -#! -#! Invocation: exec -proc mmr_frontier_keccak_add - dropw dropw drop - # => [] -end - -#! Gets the root of the MMR frontier using Keccak hashing (stubbed implementation). -#! -#! This is a stubbed implementation that returns placeholder values instead of -#! computing the actual MMR frontier root. -#! -#! Inputs: [mmr_ptr] -#! Outputs: [ROOT[1], ROOT[0]] -#! -#! Where: -#! - ROOT[1], ROOT[0] are the root hash components of the MMR frontier whose memory location starts at mmr_ptr -#! -#! Invocation: exec -pub proc mmr_frontier_keccak_get_root - # stubbed out for now - drop - # => [] - - push.0.0.0.1 push.LOCAL_EXIT_TREE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, KEY] - - exec.active_account::get_map_item - # => [ROOT[0]] - - push.0.0.0.0 push.LOCAL_EXIT_TREE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, KEY, ROOT[0]] - - exec.active_account::get_map_item - # => [ROOT[1], ROOT[0]] -end - -#! Writes the MMR frontier root to account storage. -#! -#! This procedure retrieves the current MMR frontier root and stores it as a double word -#! in the account's storage map. The root is split across two storage keys: -#! - Key [0,0,0,0] stores ROOT[1] (high part) -#! - Key [0,0,0,1] stores ROOT[0] (low part) -#! -#! Inputs: [] -#! Outputs: [] -#! -#! Invocation: exec -proc write_mmr_frontier_root - push.MMR_PTR - # => [MMR_PTR] - - # getting mmr frontier root - exec.mmr_frontier_keccak_get_root - # => [ROOT[1], ROOT[0]] - - # writing double word root to map keys [0,0,0,0] & [0,0,0,1] - push.0.0.0.0 push.LOCAL_EXIT_TREE_SLOT[0..2] - # => [index, KEY, ROOT[1], ROOT[0]] - - exec.native_account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE, ROOT[0]] - - dropw dropw - # => [ROOT[0]] - - push.1.0.0.0 push.LOCAL_EXIT_TREE_SLOT[0..2] - # => [index, KEY, ROOT[0]] - - exec.native_account::set_map_item - # => [OLD_MAP_ROOT, OLD_MAP_VALUE] - - dropw dropw - # => [] -end - -#! Adds an asset message to the MMR frontier and updates the stored root. -#! -#! This procedure takes a Keccak digest (represented as 8 u32 values) and adds it -#! as a leaf to the MMR frontier. After adding the leaf, it updates the MMR root -#! in the account's storage to reflect the new state. -#! -#! Inputs: [DIGEST_U32[8]] -#! Outputs: [] -#! -#! Where: -#! - DIGEST_U32[8] is a Keccak256 hash represented as 8 u32 values (256 bits total). -#! -#! Invocation: exec -pub proc add_asset_message - push.MMR_PTR movdn.8 - # => [LEAF[1], LEAF[0], mmr_ptr] - - exec.mmr_frontier_keccak_add - # => [] - - exec.write_mmr_frontier_root - # => [] -end - diff --git a/crates/miden-agglayer/asm/components/bridge.masm b/crates/miden-agglayer/asm/components/bridge.masm new file mode 100644 index 0000000000..2d919b687d --- /dev/null +++ b/crates/miden-agglayer/asm/components/bridge.masm @@ -0,0 +1,11 @@ +# The MASM code of the AggLayer Bridge Account Component. +# +# This is a thin wrapper that re-exports bridge-related procedures from the +# agglayer library. + +pub use ::miden::agglayer::bridge::bridge_config::assert_sender_is_bridge_admin +pub use ::miden::agglayer::bridge::bridge_config::assert_sender_is_ger_manager +pub use ::miden::agglayer::bridge::bridge_config::register_faucet +pub use ::miden::agglayer::bridge::bridge_config::update_ger +pub use ::miden::agglayer::bridge::bridge_in::verify_leaf_bridge +pub use ::miden::agglayer::bridge::bridge_out::bridge_out diff --git a/crates/miden-agglayer/asm/components/faucet.masm b/crates/miden-agglayer/asm/components/faucet.masm new file mode 100644 index 0000000000..641b6089e7 --- /dev/null +++ b/crates/miden-agglayer/asm/components/faucet.masm @@ -0,0 +1,10 @@ +# The MASM code of the AggLayer Faucet Account Component. +# +# This is a thin wrapper that re-exports faucet-related procedures from the +# agglayer library. Only procedures relevant to faucet accounts are exposed +# here, so that bridge-specific procedures (like `bridge_out`) are not +# available on faucet accounts. + +pub use ::miden::agglayer::faucet::claim +pub use ::miden::agglayer::faucet::asset_to_origin_asset +pub use ::miden::agglayer::faucet::burn diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index 31afc1d83c..2fbb1b01cb 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -1,4 +1,4 @@ -use miden::agglayer::bridge_out +use miden::agglayer::bridge::bridge_out use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index dc705c4dc6..6eb1bc3539 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -1,4 +1,4 @@ -use miden::agglayer::agglayer_faucet -> agg_faucet +use miden::agglayer::faucet -> agg_faucet use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm index 2bfc1f29b4..2e161f0447 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -1,4 +1,4 @@ -use miden::agglayer::bridge_config +use miden::agglayer::bridge::bridge_config use miden::protocol::active_note use miden::protocol::active_account use miden::protocol::account_id diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index 21118fc9d0..fe2a632e08 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -1,5 +1,4 @@ -use miden::agglayer::bridge_config -use miden::agglayer::bridge_in +use miden::agglayer::bridge::bridge_config use miden::protocol::active_note use miden::protocol::active_account use miden::protocol::account_id @@ -20,7 +19,7 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe # NOTE SCRIPT # ================================================================================================= -#! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_in::update_ger function. +#! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_config::update_ger function. #! #! This note can only be consumed by the specific agglayer bridge account whose ID is provided #! in the note attachment (target_account_id), and only if the note was sent by the @@ -69,7 +68,7 @@ begin mem_loadw_le.STORAGE_PTR_GER_LOWER # => [GER_LOWER[4], GER_UPPER[4], pad(8)] - call.bridge_in::update_ger + call.bridge_config::update_ger # => [pad(16)] end diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index d91a2ac1d7..24f48a3091 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -2,7 +2,7 @@ use std::env; use std::path::Path; use fs_err as fs; -use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; +use miden_assembly::diagnostics::{IntoDiagnostic, NamedSource, Result, WrapErr}; use miden_assembly::utils::Serializable; use miden_assembly::{Assembler, Library, Report}; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; @@ -19,7 +19,9 @@ const BUILD_GENERATED_FILES_IN_SRC: bool = option_env!("BUILD_GENERATED_FILES_IN const ASSETS_DIR: &str = "assets"; const ASM_DIR: &str = "asm"; const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts"; -const ASM_BRIDGE_DIR: &str = "bridge"; +const ASM_AGGLAYER_DIR: &str = "agglayer"; +const ASM_AGGLAYER_BRIDGE_DIR: &str = "agglayer/bridge"; +const ASM_COMPONENTS_DIR: &str = "components"; const AGGLAYER_ERRORS_FILE: &str = "src/errors/agglayer.rs"; const AGGLAYER_ERRORS_ARRAY_NAME: &str = "AGGLAYER_ERRORS"; @@ -28,8 +30,9 @@ const AGGLAYER_ERRORS_ARRAY_NAME: &str = "AGGLAYER_ERRORS"; // ================================================================================================ /// Read and parse the contents from `./asm`. +/// - Compiles the contents of asm/agglayer directory into a single agglayer.masl library. +/// - Compiles the contents of asm/components directory into individual per-component .masl files. /// - Compiles the contents of asm/note_scripts directory into individual .masb files. -/// - Compiles the contents of asm/account_components directory into individual .masl files. fn main() -> Result<()> { // re-build when the MASM code changes println!("cargo::rerun-if-changed={ASM_DIR}/"); @@ -40,8 +43,8 @@ fn main() -> Result<()> { let build_dir = env::var("OUT_DIR").unwrap(); let src = Path::new(&crate_dir).join(ASM_DIR); - // generate canonical zeros in `asm/bridge/canonical_zeros.masm` - generate_canonical_zeros(&src.join(ASM_BRIDGE_DIR))?; + // generate canonical zeros in `asm/agglayer/bridge/canonical_zeros.masm` + generate_canonical_zeros(&src.join(ASM_AGGLAYER_BRIDGE_DIR))?; let dst = Path::new(&build_dir).to_path_buf(); shared::copy_directory(src, &dst, ASM_DIR)?; @@ -59,6 +62,13 @@ fn main() -> Result<()> { let mut assembler = TransactionKernel::assembler(); assembler.link_static_library(agglayer_lib)?; + // compile account components (thin wrappers per component) + compile_account_components( + &source_dir.join(ASM_COMPONENTS_DIR), + &target_dir.join(ASM_COMPONENTS_DIR), + assembler.clone(), + )?; + // compile note scripts compile_note_scripts( &source_dir.join(ASM_NOTE_SCRIPTS_DIR), @@ -74,7 +84,7 @@ fn main() -> Result<()> { // COMPILE AGGLAYER LIB // ================================================================================================ -/// Reads the MASM files from "{source_dir}/bridge" directory, compiles them into a Miden +/// Reads the MASM files from "{source_dir}/agglayer" directory, compiles them into a Miden /// assembly library, saves the library into "{target_dir}/agglayer.masl", and returns the compiled /// library. fn compile_agglayer_lib( @@ -82,7 +92,7 @@ fn compile_agglayer_lib( target_dir: &Path, mut assembler: Assembler, ) -> Result { - let source_dir = source_dir.join(ASM_BRIDGE_DIR); + let source_dir = source_dir.join(ASM_AGGLAYER_DIR); // Add the miden-standards library to the assembler so agglayer components can use it let standards_lib = miden_standards::StandardsLib::default(); @@ -136,38 +146,28 @@ fn compile_note_scripts( Ok(()) } -// COMPILE ACCOUNT COMPONENTS (DEPRECATED) +// COMPILE ACCOUNT COMPONENTS // ================================================================================================ -/// Compiles the agglayer library in `source_dir` into MASL libraries and stores the compiled +/// Compiles the account components in `source_dir` into MASL libraries and stores the compiled /// files in `target_dir`. /// -/// NOTE: This function is deprecated and replaced by compile_agglayer_lib -fn _compile_bridge_components( +/// Each `.masm` file in the components directory is a thin wrapper that re-exports specific +/// procedures from the main agglayer library. This ensures each component (bridge, faucet) +/// only exposes the procedures relevant to its role. +/// +/// The assembler must already have the agglayer library linked so that `pub use` re-exports +/// can resolve. +fn compile_account_components( source_dir: &Path, target_dir: &Path, - mut assembler: Assembler, -) -> Result { + assembler: Assembler, +) -> Result<()> { if !target_dir.exists() { fs::create_dir_all(target_dir).unwrap(); } - // Add the miden-standards library to the assembler so agglayer components can use it - let standards_lib = miden_standards::StandardsLib::default(); - assembler.link_static_library(standards_lib)?; - - // Compile all components together as a single library under the "miden::agglayer" namespace - // This allows cross-references between components (e.g., bridge_out using - // miden::agglayer::local_exit_tree) - let agglayer_library = assembler.assemble_library_from_dir(source_dir, "miden::agglayer")?; - - // Write the combined library - let library_path = target_dir.join("agglayer").with_extension(Library::LIBRARY_EXTENSION); - agglayer_library.write_to_file(library_path).into_diagnostic()?; - - // Also write individual component files for reference - let masm_files = shared::get_masm_files(source_dir).unwrap(); - for masm_file_path in &masm_files { + for masm_file_path in shared::get_masm_files(source_dir).unwrap() { let component_name = masm_file_path .file_stem() .expect("masm file should have a file stem") @@ -175,14 +175,22 @@ fn _compile_bridge_components( .expect("file stem should be valid UTF-8") .to_owned(); - let component_source_code = fs::read_to_string(masm_file_path) + let component_source_code = fs::read_to_string(&masm_file_path) .expect("reading the component's MASM source code should succeed"); - let individual_file_path = target_dir.join(&component_name).with_extension("masm"); - fs::write(individual_file_path, component_source_code).into_diagnostic()?; + let named_source = NamedSource::new(component_name.clone(), component_source_code); + + let component_library = assembler + .clone() + .assemble_library([named_source]) + .expect("library assembly should succeed"); + + let component_file_path = + target_dir.join(component_name).with_extension(Library::LIBRARY_EXTENSION); + component_library.write_to_file(component_file_path).into_diagnostic()?; } - Ok(agglayer_library) + Ok(()) } // ERROR CONSTANTS FILE GENERATION @@ -287,7 +295,7 @@ fn generate_canonical_zeros(target_dir: &Path) -> Result<()> { // remove once CANONICAL_ZEROS advice map is available zero_constants.push_str( " -use ::miden::agglayer::mmr_frontier32_keccak::mem_store_double_word +use ::miden::agglayer::common::utils::mem_store_double_word #! Inputs: [zeros_ptr] #! Outputs: [] diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 29d6c03410..5e44056335 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -22,7 +22,7 @@ use miden_protocol::account::{ use miden_protocol::asset::TokenSymbol; use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; -use miden_standards::account::faucets::NetworkFungibleFaucet; +use miden_standards::account::faucets::{FungibleFaucetError, TokenMetadata}; use miden_utils_sync::LazyLock; pub mod b2agg_note; @@ -72,96 +72,183 @@ pub fn claim_script() -> NoteScript { // AGGLAYER ACCOUNT COMPONENTS // ================================================================================================ -// Initialize the unified AggLayer library only once static AGGLAYER_LIBRARY: LazyLock = LazyLock::new(|| { let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/agglayer.masl")); Library::read_from_bytes(bytes).expect("shipped AggLayer library is well-formed") }); -/// Returns the unified AggLayer Library containing all agglayer modules. +static BRIDGE_COMPONENT_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/bridge.masl")); + Library::read_from_bytes(bytes).expect("shipped bridge component library is well-formed") +}); + +static FAUCET_COMPONENT_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/faucet.masl")); + Library::read_from_bytes(bytes).expect("shipped faucet component library is well-formed") +}); + +/// Returns the AggLayer Library containing all agglayer modules. pub fn agglayer_library() -> Library { AGGLAYER_LIBRARY.clone() } -/// Returns the Bridge Out Library. -/// -/// Note: This is now the same as agglayer_library() since all agglayer components -/// are compiled into a single library. -pub fn bridge_out_library() -> Library { - agglayer_library() +/// Returns the Bridge component library. +fn agglayer_bridge_component_library() -> Library { + BRIDGE_COMPONENT_LIBRARY.clone() } -/// Returns the Local Exit Tree Library. -/// -/// Note: This is now the same as agglayer_library() since all agglayer components -/// are compiled into a single library. -pub fn local_exit_tree_library() -> Library { - agglayer_library() +/// Returns the Faucet component library. +fn agglayer_faucet_component_library() -> Library { + FAUCET_COMPONENT_LIBRARY.clone() } -/// Storage slot name for the bridge admin account ID. -pub const BRIDGE_ADMIN_SLOT_NAME: &str = "miden::agglayer::bridge::admin"; - -/// Storage slot name for the global exit root manager account ID. -pub const GER_MANAGER_SLOT_NAME: &str = "miden::agglayer::bridge::ger_manager"; - -/// Creates a Local Exit Tree component with the specified storage slots. -/// -/// This component uses the local_exit_tree library and can be added to accounts -/// that need to manage local exit tree functionality. -pub fn local_exit_tree_component(storage_slots: Vec) -> AccountComponent { - let library = local_exit_tree_library(); - let metadata = AccountComponentMetadata::new("agglayer::local_exit_tree") - .with_description("Local exit tree component for AggLayer") +/// Creates an AggLayer Bridge component with the specified storage slots. +fn bridge_component(storage_slots: Vec) -> AccountComponent { + let library = agglayer_bridge_component_library(); + let metadata = AccountComponentMetadata::new("agglayer::bridge") + .with_description("Bridge component for AggLayer") .with_supports_all_types(); - AccountComponent::new(library, storage_slots, metadata).expect( - "local_exit_tree component should satisfy the requirements of a valid account component", - ) + AccountComponent::new(library, storage_slots, metadata) + .expect("bridge component should satisfy the requirements of a valid account component") } -/// Creates a Bridge Out component with the specified storage slots. -/// -/// This component uses the bridge_out library and can be added to accounts -/// that need to bridge assets out to the AggLayer. -pub fn bridge_out_component(storage_slots: Vec) -> AccountComponent { - let library = bridge_out_library(); - let metadata = AccountComponentMetadata::new("agglayer::bridge_out") - .with_description("Bridge out component for AggLayer") - .with_supports_all_types(); +// AGGLAYER BRIDGE STRUCT +// ================================================================================================ - AccountComponent::new(library, storage_slots, metadata) - .expect("bridge_out component should satisfy the requirements of a valid account component") -} +static GER_MAP_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::ger") + .expect("bridge storage slot name should be valid") +}); +static LET_FRONTIER_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let").expect("LET storage slot name should be valid") +}); +static LET_ROOT_LO_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let::root_lo") + .expect("LET root_lo storage slot name should be valid") +}); +static LET_ROOT_HI_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let::root_hi") + .expect("LET root_hi storage slot name should be valid") +}); +static LET_NUM_LEAVES_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::let::num_leaves") + .expect("LET num_leaves storage slot name should be valid") +}); +static FAUCET_REGISTRY_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::faucet_registry") + .expect("faucet registry storage slot name should be valid") +}); +static BRIDGE_ADMIN_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::admin") + .expect("bridge admin storage slot name should be valid") +}); +static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::ger_manager") + .expect("GER manager storage slot name should be valid") +}); -/// Returns the Bridge In Library. +/// An [`AccountComponent`] implementing the AggLayer Bridge. /// -/// Note: This is now the same as agglayer_library() since all agglayer components -/// are compiled into a single library. -pub fn bridge_in_library() -> Library { - agglayer_library() -} - -/// Creates a Bridge In component with the specified storage slots. +/// It reexports the procedures from `miden::agglayer::bridge`. When linking against this +/// component, the `agglayer` library must be available to the assembler. +/// The procedures of this component are: +/// - `bridge_out`, which bridges an asset from the AggLayer to the destination network. +/// - `verify_leaf_bridge`, which verifies a deposit leaf against one of the stored GERs. +/// - `update_ger`, which injects a new GER into the storage map. +/// - `register_faucet`, which registers a faucet in the bridge. /// -/// This component uses the agglayer library and can be added to accounts -/// that need to bridge assets in from the AggLayer. -pub fn bridge_in_component(storage_slots: Vec) -> AccountComponent { - let library = bridge_in_library(); - let metadata = AccountComponentMetadata::new("agglayer::bridge_in") - .with_description("Bridge in component for AggLayer") - .with_supports_all_types(); +/// ## Storage Layout +/// +/// - [`Self::ger_map_slot_name`]: Stores the GERs. +/// - [`Self::let_frontier_slot_name`]: Stores the Local Exit Tree (LET) frontier. +/// - [`Self::ler_lo_slot_name`]: Stores the lower 32 bits of the LET root. +/// - [`Self::ler_hi_slot_name`]: Stores the upper 32 bits of the LET root. +/// - [`Self::let_num_leaves_slot_name`]: Stores the number of leaves in the LET frontier. +/// - [`Self::faucet_registry_slot_name`]: Stores the faucet registry map. +/// +/// The bridge starts with an empty faucet registry; faucets are registered at runtime via +/// CONFIG_AGG_BRIDGE notes. +#[derive(Debug, Clone)] +pub struct AggLayerBridge { + bridge_admin_id: AccountId, + ger_manager_id: AccountId, +} - AccountComponent::new(library, storage_slots, metadata) - .expect("bridge_in component should satisfy the requirements of a valid account component") +impl AggLayerBridge { + /// Creates a new AggLayer bridge component with the standard configuration. + pub fn new(bridge_admin_id: AccountId, ger_manager_id: AccountId) -> Self { + Self { bridge_admin_id, ger_manager_id } + } + + /// Storage slot name for the GERs map. + pub fn ger_map_slot_name() -> &'static StorageSlotName { + &GER_MAP_SLOT_NAME + } + + /// Storage slot name for the Local Exit Tree (LET) frontier. + pub fn let_frontier_slot_name() -> &'static StorageSlotName { + &LET_FRONTIER_SLOT_NAME + } + + /// Storage slot name for the lower 32 bits of the LET root. + pub fn ler_lo_slot_name() -> &'static StorageSlotName { + &LET_ROOT_LO_SLOT_NAME + } + + /// Storage slot name for the upper 32 bits of the LET root. + pub fn ler_hi_slot_name() -> &'static StorageSlotName { + &LET_ROOT_HI_SLOT_NAME + } + + /// Storage slot name for the number of leaves in the LET frontier. + pub fn let_num_leaves_slot_name() -> &'static StorageSlotName { + &LET_NUM_LEAVES_SLOT_NAME + } + + /// Storage slot name for the faucet registry map. + pub fn faucet_registry_slot_name() -> &'static StorageSlotName { + &FAUCET_REGISTRY_SLOT_NAME + } + + /// Storage slot name for the bridge admin account ID. + pub fn bridge_admin_slot_name() -> &'static StorageSlotName { + &BRIDGE_ADMIN_SLOT_NAME + } + + /// Storage slot name for the GER manager account ID. + pub fn ger_manager_slot_name() -> &'static StorageSlotName { + &GER_MANAGER_SLOT_NAME + } } -/// Returns the Agglayer Faucet Library. -/// -/// Note: This is now the same as agglayer_library() since all agglayer components -/// are compiled into a single library. -pub fn agglayer_faucet_library() -> Library { - agglayer_library() +impl From for AccountComponent { + fn from(bridge: AggLayerBridge) -> Self { + let bridge_admin_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + bridge.bridge_admin_id.suffix(), + bridge.bridge_admin_id.prefix().as_felt(), + ]); + let ger_manager_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + bridge.ger_manager_id.suffix(), + bridge.ger_manager_id.prefix().as_felt(), + ]); + + let bridge_storage_slots = vec![ + StorageSlot::with_empty_map(GER_MAP_SLOT_NAME.clone()), + StorageSlot::with_empty_map(LET_FRONTIER_SLOT_NAME.clone()), + StorageSlot::with_value(LET_ROOT_LO_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_value(LET_ROOT_HI_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_value(LET_NUM_LEAVES_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_empty_map(FAUCET_REGISTRY_SLOT_NAME.clone()), + StorageSlot::with_value(BRIDGE_ADMIN_SLOT_NAME.clone(), bridge_admin_word), + StorageSlot::with_value(GER_MANAGER_SLOT_NAME.clone(), ger_manager_word), + ]; + bridge_component(bridge_storage_slots) + } } /// Creates an Agglayer Faucet component with the specified storage slots. @@ -169,8 +256,8 @@ pub fn agglayer_faucet_library() -> Library { /// This component combines network faucet functionality with bridge validation /// via Foreign Procedure Invocation (FPI). It provides a "claim" procedure that /// validates CLAIM notes against a bridge MMR account before minting assets. -pub fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { - let library = agglayer_faucet_library(); +fn agglayer_faucet_component(storage_slots: Vec) -> AccountComponent { + let library = agglayer_faucet_component_library(); let metadata = AccountComponentMetadata::new("agglayer::faucet") .with_description("AggLayer faucet component with bridge validation") .with_supported_type(AccountType::FungibleFaucet); @@ -180,36 +267,6 @@ pub fn agglayer_faucet_component(storage_slots: Vec) -> AccountComp ) } -/// Creates a combined Bridge Out component that includes both bridge_out and local_exit_tree -/// modules. -/// -/// This is a convenience function that creates a component with multiple modules. -/// For more fine-grained control, use the individual component functions and combine them -/// using the AccountBuilder pattern. -pub fn bridge_out_with_local_exit_tree_component( - storage_slots: Vec, -) -> Vec { - vec![ - bridge_out_component(storage_slots.clone()), - local_exit_tree_component(vec![]), // local_exit_tree typically doesn't need storage slots - ] -} - -/// Creates an Asset Conversion component with the specified storage slots. -/// -/// This component uses the agglayer library (which includes asset_conversion) and can be added to -/// accounts that need to convert assets between Miden and Ethereum formats. -pub fn asset_conversion_component(storage_slots: Vec) -> AccountComponent { - let library = agglayer_library(); - let metadata = AccountComponentMetadata::new("agglayer::asset_conversion") - .with_description("Asset conversion component for Miden/Ethereum formats") - .with_supports_all_types(); - - AccountComponent::new(library, storage_slots, metadata).expect( - "asset_conversion component should satisfy the requirements of a valid account component", - ) -} - // FAUCET CONVERSION STORAGE HELPERS // ================================================================================================ @@ -228,7 +285,7 @@ pub fn asset_conversion_component(storage_slots: Vec) -> AccountCom /// /// # Returns /// A tuple of two `Word` values representing the two storage slot contents. -pub fn agglayer_faucet_conversion_slots( +fn agglayer_faucet_conversion_slots( origin_token_address: &EthAddressFormat, origin_network: u32, scale: u8, @@ -243,6 +300,135 @@ pub fn agglayer_faucet_conversion_slots( (slot1, slot2) } +// AGGLAYER FAUCET STRUCT +// ================================================================================================ + +static AGGLAYER_FAUCET_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::faucet") + .expect("agglayer faucet storage slot name should be valid") +}); +static CONVERSION_INFO_1_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") + .expect("conversion info 1 storage slot name should be valid") +}); +static CONVERSION_INFO_2_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") + .expect("conversion info 2 storage slot name should be valid") +}); + +/// An [`AccountComponent`] implementing the AggLayer Faucet. +/// +/// It reexports the procedures from `miden::agglayer::faucet`. When linking against this +/// component, the `agglayer` library must be available to the assembler. +/// The procedures of this component are: +/// - `claim`, which validates a CLAIM note against one of the stored GERs in the bridge. +/// - `asset_to_origin_asset`, which converts an asset to the origin asset (used in FPI from +/// bridge). +/// - `burn`, which burns an asset. +/// +/// ## Storage Layout +/// +/// - [`Self::metadata_slot`]: Stores [`TokenMetadata`]. +/// - [`Self::bridge_account_id_slot`]: Stores the AggLayer bridge account ID. +/// - [`Self::conversion_info_1_slot`]: Stores the first 4 felts of the origin token address. +/// - [`Self::conversion_info_2_slot`]: Stores the remaining 5th felt of the origin token address + +/// origin network + scale. +#[derive(Debug, Clone)] +pub struct AggLayerFaucet { + metadata: TokenMetadata, + bridge_account_id: AccountId, + origin_token_address: EthAddressFormat, + origin_network: u32, + scale: u8, +} + +impl AggLayerFaucet { + /// Creates a new AggLayer faucet component from the given configuration. + /// + /// # Errors + /// Returns an error if: + /// - The decimals parameter exceeds maximum value of [`TokenMetadata::MAX_DECIMALS`]. + /// - The max supply exceeds maximum possible amount for a fungible asset. + /// - The token supply exceeds the max supply. + pub fn new( + symbol: TokenSymbol, + decimals: u8, + max_supply: Felt, + token_supply: Felt, + bridge_account_id: AccountId, + origin_token_address: EthAddressFormat, + origin_network: u32, + scale: u8, + ) -> Result { + let metadata = TokenMetadata::with_supply(symbol, decimals, max_supply, token_supply)?; + Ok(Self { + metadata, + bridge_account_id, + origin_token_address, + origin_network, + scale, + }) + } + + /// Sets the token supply for an existing faucet (e.g. for testing scenarios). + /// + /// # Errors + /// Returns an error if the token supply exceeds the max supply. + pub fn with_token_supply(mut self, token_supply: Felt) -> Result { + self.metadata = self.metadata.with_token_supply(token_supply)?; + Ok(self) + } + + /// Storage slot name for [`TokenMetadata`]. + pub fn metadata_slot() -> &'static StorageSlotName { + TokenMetadata::metadata_slot() + } + + /// Storage slot name for the AggLayer bridge account ID. + pub fn bridge_account_id_slot() -> &'static StorageSlotName { + &AGGLAYER_FAUCET_SLOT_NAME + } + + /// Storage slot name for the first 4 felts of the origin token address. + pub fn conversion_info_1_slot() -> &'static StorageSlotName { + &CONVERSION_INFO_1_SLOT_NAME + } + + /// Storage slot name for the 5th felt of the origin token address, origin network, and scale. + pub fn conversion_info_2_slot() -> &'static StorageSlotName { + &CONVERSION_INFO_2_SLOT_NAME + } +} + +impl From for AccountComponent { + fn from(faucet: AggLayerFaucet) -> Self { + let metadata_slot = StorageSlot::from(faucet.metadata); + + let bridge_account_id_word = Word::new([ + Felt::ZERO, + Felt::ZERO, + faucet.bridge_account_id.suffix(), + faucet.bridge_account_id.prefix().as_felt(), + ]); + let bridge_slot = + StorageSlot::with_value(AGGLAYER_FAUCET_SLOT_NAME.clone(), bridge_account_id_word); + + let (conversion_slot1_word, conversion_slot2_word) = agglayer_faucet_conversion_slots( + &faucet.origin_token_address, + faucet.origin_network, + faucet.scale, + ); + let conversion_slot1 = + StorageSlot::with_value(CONVERSION_INFO_1_SLOT_NAME.clone(), conversion_slot1_word); + let conversion_slot2 = + StorageSlot::with_value(CONVERSION_INFO_2_SLOT_NAME.clone(), conversion_slot2_word); + + let agglayer_storage_slots = + vec![metadata_slot, bridge_slot, conversion_slot1, conversion_slot2]; + agglayer_faucet_component(agglayer_storage_slots) + } +} + // FAUCET REGISTRY HELPERS // ================================================================================================ @@ -256,20 +442,6 @@ pub fn faucet_registry_key(faucet_id: AccountId) -> Word { // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ -/// Creates a bridge account component with the standard bridge storage slot. -/// -/// This is a convenience function that creates the bridge storage slot with the standard -/// name "miden::agglayer::bridge" and returns the bridge_out component. -/// -/// # Returns -/// Returns an [`AccountComponent`] configured for bridge operations with MMR validation. -pub fn create_bridge_account_component() -> AccountComponent { - let bridge_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge") - .expect("bridge storage slot name should be valid"); - let bridge_storage_slots = vec![StorageSlot::with_empty_map(bridge_storage_slot_name)]; - bridge_out_component(bridge_storage_slots) -} - /// Creates an agglayer faucet account component with the specified configuration. /// /// This function creates all the necessary storage slots for an agglayer faucet: @@ -292,8 +464,8 @@ pub fn create_bridge_account_component() -> AccountComponent { /// Returns an [`AccountComponent`] configured for agglayer faucet operations. /// /// # Panics -/// Panics if the token symbol is invalid or storage slot names are malformed. -pub fn create_agglayer_faucet_component( +/// Panics if the token symbol is invalid or metadata validation fails. +fn create_agglayer_faucet_component( token_symbol: &str, decimals: u8, max_supply: Felt, @@ -303,116 +475,33 @@ pub fn create_agglayer_faucet_component( origin_network: u32, scale: u8, ) -> AccountComponent { - // Create network faucet metadata slot: [token_supply, max_supply, decimals, token_symbol] - let token_symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); - let metadata_word = - Word::new([token_supply, max_supply, Felt::from(decimals), token_symbol.into()]); - let metadata_slot = - StorageSlot::with_value(NetworkFungibleFaucet::metadata_slot().clone(), metadata_word); - - // Create agglayer-specific bridge storage slot - let bridge_account_id_word = Word::new([ - Felt::new(0), - Felt::new(0), - bridge_account_id.suffix(), - bridge_account_id.prefix().as_felt(), - ]); - let agglayer_storage_slot_name = StorageSlotName::new("miden::agglayer::faucet") - .expect("agglayer faucet storage slot name should be valid"); - let bridge_slot = StorageSlot::with_value(agglayer_storage_slot_name, bridge_account_id_word); - - // Create conversion metadata storage slots - let (conversion_slot1_word, conversion_slot2_word) = - agglayer_faucet_conversion_slots(origin_token_address, origin_network, scale); - - let conversion_info_1_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") - .expect("conversion info 1 storage slot name should be valid"); - let conversion_slot1 = StorageSlot::with_value(conversion_info_1_name, conversion_slot1_word); - - let conversion_info_2_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") - .expect("conversion info 2 storage slot name should be valid"); - let conversion_slot2 = StorageSlot::with_value(conversion_info_2_name, conversion_slot2_word); - - // Combine all storage slots for the agglayer faucet component - let agglayer_storage_slots = - vec![metadata_slot, bridge_slot, conversion_slot1, conversion_slot2]; - agglayer_faucet_component(agglayer_storage_slots) + let symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid"); + AggLayerFaucet::new( + symbol, + decimals, + max_supply, + token_supply, + bridge_account_id, + *origin_token_address, + origin_network, + scale, + ) + .expect("agglayer faucet metadata should be valid") + .into() } /// Creates a complete bridge account builder with the standard configuration. /// /// The bridge starts with an empty faucet registry. Faucets are registered at runtime /// via CONFIG_AGG_BRIDGE notes that call `bridge_config::register_faucet`. -/// -/// # Parameters -/// - `seed`: The seed used to derive the account ID. -/// - `bridge_admin_id`: The account ID of the bridge admin. Only notes sent by this account are -/// allowed to update bridge configuration (e.g. register faucets). -/// - `ger_manager_id`: The account ID of the global exit root manager. Only notes sent by this -/// account are allowed to update the GER. -pub fn create_bridge_account_builder( +fn create_bridge_account_builder( seed: Word, bridge_admin_id: AccountId, ger_manager_id: AccountId, ) -> AccountBuilder { - let ger_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger") - .expect("bridge storage slot name should be valid"); - let bridge_in_storage_slots = vec![StorageSlot::with_empty_map(ger_storage_slot_name)]; - - let bridge_in_component = bridge_in_component(bridge_in_storage_slots); - - // Create the "bridge_out" component. - // - The map slot stores the frontier as a double-word array (absent keys return zeros, which is - // the correct initial state for a frontier with no leaves). - // - The root and num_leaves each get their own value slots. - let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let") - .expect("LET storage slot name should be valid"); - let let_root_lo_slot_name = StorageSlotName::new("miden::agglayer::let::root_lo") - .expect("LET root_lo storage slot name should be valid"); - let let_root_hi_slot_name = StorageSlotName::new("miden::agglayer::let::root_hi") - .expect("LET root_hi storage slot name should be valid"); - let let_num_leaves_slot_name = StorageSlotName::new("miden::agglayer::let::num_leaves") - .expect("LET num_leaves storage slot name should be valid"); - let faucet_registry_slot_name = - StorageSlotName::new("miden::agglayer::bridge::faucet_registry") - .expect("faucet registry storage slot name should be valid"); - - let bridge_admin_slot_name = StorageSlotName::new(BRIDGE_ADMIN_SLOT_NAME) - .expect("bridge admin storage slot name should be valid"); - let bridge_admin_word = Word::new([ - Felt::ZERO, - Felt::ZERO, - bridge_admin_id.suffix(), - bridge_admin_id.prefix().as_felt(), - ]); - - let ger_manager_slot_name = StorageSlotName::new(GER_MANAGER_SLOT_NAME) - .expect("GER manager storage slot name should be valid"); - let ger_manager_word = Word::new([ - Felt::ZERO, - Felt::ZERO, - ger_manager_id.suffix(), - ger_manager_id.prefix().as_felt(), - ]); - - let bridge_out_storage_slots = vec![ - StorageSlot::with_empty_map(let_storage_slot_name), - StorageSlot::with_value(let_root_lo_slot_name, Word::empty()), - StorageSlot::with_value(let_root_hi_slot_name, Word::empty()), - StorageSlot::with_value(let_num_leaves_slot_name, Word::empty()), - StorageSlot::with_empty_map(faucet_registry_slot_name), - StorageSlot::with_value(bridge_admin_slot_name, bridge_admin_word), - StorageSlot::with_value(ger_manager_slot_name, ger_manager_word), - ]; - let bridge_out_component = bridge_out_component(bridge_out_storage_slots); - - // Combine the components into a single account(builder). - // Note: bridge_config::register_faucet is also exposed via the agglayer library - // included in bridge_out_component, using the faucet_registry storage slot above. Account::builder(seed.into()) .storage_mode(AccountStorageMode::Network) - .with_component(bridge_out_component) - .with_component(bridge_in_component) + .with_component(AggLayerBridge::new(bridge_admin_id, ger_manager_id)) } /// Creates a new bridge account with the standard configuration. @@ -446,7 +535,7 @@ pub fn create_existing_bridge_account( /// Creates a complete agglayer faucet account builder with the specified configuration. #[allow(clippy::too_many_arguments)] -pub fn create_agglayer_faucet_builder( +fn create_agglayer_faucet_builder( seed: Word, token_symbol: &str, decimals: u8, diff --git a/crates/miden-testing/tests/agglayer/asset_conversion.rs b/crates/miden-testing/tests/agglayer/asset_conversion.rs index 7a7565c743..868c3257de 100644 --- a/crates/miden-testing/tests/agglayer/asset_conversion.rs +++ b/crates/miden-testing/tests/agglayer/asset_conversion.rs @@ -30,7 +30,7 @@ async fn test_scale_up_helper( let script_code = format!( " use miden::core::sys - use miden::agglayer::asset_conversion + use miden::agglayer::common::asset_conversion begin push.{}.{} @@ -102,7 +102,7 @@ async fn test_scale_up_exceeds_max_scale() { // scale_exp = 19 should fail let script_code = " use miden::core::sys - use miden::agglayer::asset_conversion + use miden::agglayer::common::asset_conversion begin push.19.1 @@ -124,7 +124,7 @@ fn build_scale_down_script(x: EthAmount, scale_exp: u32, y: u64) -> String { format!( r#" use miden::core::sys - use miden::agglayer::asset_conversion + use miden::agglayer::common::asset_conversion begin push.{}.{}.{}.{}.{}.{}.{}.{}.{}.{} @@ -314,7 +314,7 @@ async fn test_verify_scale_down_inline() -> anyhow::Result<()> { let script_code = format!( r#" use miden::core::sys - use miden::agglayer::asset_conversion + use miden::agglayer::common::asset_conversion begin # Push y (expected quotient) diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 596ffb926f..5439dfcbb8 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -1,11 +1,17 @@ extern crate alloc; -use core::slice; +use alloc::slice; +use alloc::string::String; +use anyhow::Context; +use miden_agglayer::claim_note::Keccak256Output; use miden_agglayer::{ ClaimNoteStorage, + ExitRoot, OutputNoteData, + SmtNode, UpdateGerNote, + agglayer_library, create_claim_note, create_existing_agglayer_faucet, create_existing_bridge_account, @@ -18,13 +24,72 @@ use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUT use miden_protocol::transaction::OutputNote; use miden_protocol::{Felt, FieldElement}; use miden_standards::account::wallets::BasicWallet; +use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::account_component::IncrNonceAuthComponent; -use miden_standards::testing::mock_account::MockAccountExt; use miden_testing::utils::create_p2id_note_exact; -use miden_testing::{AccountState, Auth, MockChain}; +use miden_testing::{AccountState, Auth, MockChain, TransactionContextBuilder}; +use miden_tx::utils::hex_to_bytes; use rand::Rng; -use super::test_utils::ClaimDataSource; +use super::test_utils::{ + ClaimDataSource, + MerkleProofVerificationFile, + SOLIDITY_MERKLE_PROOF_VECTORS, +}; + +// HELPER FUNCTIONS +// ================================================================================================ + +fn merkle_proof_verification_code( + index: usize, + merkle_paths: &MerkleProofVerificationFile, +) -> String { + let mut store_path_source = String::new(); + for height in 0..32 { + let path_node = merkle_paths.merkle_paths[index * 32 + height].as_str(); + let smt_node = SmtNode::from(hex_to_bytes(path_node).unwrap()); + let [node_lo, node_hi] = smt_node.to_words(); + store_path_source.push_str(&format!( + " + \tpush.{node_lo} mem_storew_be.{} dropw + \tpush.{node_hi} mem_storew_be.{} dropw + ", + height * 8, + height * 8 + 4 + )); + } + + let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); + let [root_lo, root_hi] = root.to_words(); + + let leaf = Keccak256Output::from(hex_to_bytes(&merkle_paths.leaves[index]).unwrap()); + let [leaf_lo, leaf_hi] = leaf.to_words(); + + format!( + r#" + use miden::agglayer::bridge::bridge_in + use miden::core::word + + begin + {store_path_source} + + push.{root_lo} mem_storew_be.256 dropw + push.{root_hi} mem_storew_be.260 dropw + + push.256 + push.{index} + push.0 + push.{leaf_hi} + exec.word::reverse + push.{leaf_lo} + exec.word::reverse + + exec.bridge_in::verify_merkle_proof + assert.err="verification failed" + end + "# + ) +} /// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created. /// @@ -100,6 +165,8 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // For the simulated case, create the destination account so we can consume the P2ID note let destination_account = if matches!(data_source, ClaimDataSource::Simulated) { + use miden_standards::testing::mock_account::MockAccountExt; + let dest = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, IncrNonceAuthComponent); // Ensure the mock account ID matches the destination embedded in the JSON test vector, @@ -260,6 +327,29 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a "destination account balance does not match" ); } + Ok(()) +} + +#[tokio::test] +async fn solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { + let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; + + assert_eq!(merkle_paths.leaves.len(), merkle_paths.roots.len()); + assert_eq!(merkle_paths.leaves.len() * 32, merkle_paths.merkle_paths.len()); + + for leaf_index in 0..32 { + let source = merkle_proof_verification_code(leaf_index, merkle_paths); + let tx_script = CodeBuilder::new() + .with_statically_linked_library(&agglayer_library())? + .compile_tx_script(source)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script.clone()) + .build()? + .execute() + .await + .context(format!("failed to execute transaction with leaf index {leaf_index}"))?; + } Ok(()) } diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index b46c18b416..a83a7174a5 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -2,6 +2,7 @@ extern crate alloc; use miden_agglayer::errors::{ERR_B2AGG_TARGET_ACCOUNT_MISMATCH, ERR_FAUCET_NOT_REGISTERED}; use miden_agglayer::{ + AggLayerBridge, B2AggNote, ConfigAggBridgeNote, EthAddressFormat, @@ -17,7 +18,6 @@ use miden_protocol::account::{ AccountIdVersion, AccountStorageMode, AccountType, - StorageSlotName, }; use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::note::{NoteAssets, NoteScript, NoteType}; @@ -32,26 +32,24 @@ use super::test_utils::SOLIDITY_MMR_FRONTIER_VECTORS; /// Reads the Local Exit Root (double-word) from the bridge account's storage. /// /// The Local Exit Root is stored in two dedicated value slots: -/// - `"miden::agglayer::let::root_lo"` — low word of the root -/// - `"miden::agglayer::let::root_hi"` — high word of the root +/// - [`AggLayerBridge::ler_lo_slot_name`] — low word of the root +/// - [`AggLayerBridge::ler_hi_slot_name`] — high word of the root /// /// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo` (in /// reverse of their storage order), followed by the 4 elements of `root_hi` (also in /// reverse of their storage order). For an empty/uninitialized tree, all elements are /// zeros. fn read_local_exit_root(account: &Account) -> Vec { - let root_lo_slot = - StorageSlotName::new("miden::agglayer::let::root_lo").expect("slot name should be valid"); - let root_hi_slot = - StorageSlotName::new("miden::agglayer::let::root_hi").expect("slot name should be valid"); + let root_lo_slot = AggLayerBridge::ler_lo_slot_name(); + let root_hi_slot = AggLayerBridge::ler_hi_slot_name(); let root_lo = account .storage() - .get_item(&root_lo_slot) + .get_item(root_lo_slot) .expect("should be able to read LET root lo"); let root_hi = account .storage() - .get_item(&root_hi_slot) + .get_item(root_hi_slot) .expect("should be able to read LET root hi"); let mut root = Vec::with_capacity(8); @@ -61,11 +59,10 @@ fn read_local_exit_root(account: &Account) -> Vec { } fn read_let_num_leaves(account: &Account) -> u64 { - let num_leaves_slot = StorageSlotName::new("miden::agglayer::let::num_leaves") - .expect("slot name should be valid"); + let num_leaves_slot = AggLayerBridge::let_num_leaves_slot_name(); let value = account .storage() - .get_item(&num_leaves_slot) + .get_item(num_leaves_slot) .expect("should be able to read LET num leaves"); value.to_vec()[0].as_int() } diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs index 29f863d5f7..8b51eb7212 100644 --- a/crates/miden-testing/tests/agglayer/config_bridge.rs +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -1,13 +1,12 @@ extern crate alloc; -use miden_agglayer::{ConfigAggBridgeNote, create_existing_bridge_account, faucet_registry_key}; -use miden_protocol::account::{ - AccountId, - AccountIdVersion, - AccountStorageMode, - AccountType, - StorageSlotName, +use miden_agglayer::{ + AggLayerBridge, + ConfigAggBridgeNote, + create_existing_bridge_account, + faucet_registry_key, }; +use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; use miden_protocol::{Felt, FieldElement}; @@ -48,9 +47,9 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { ); // Verify the faucet is NOT in the registry before registration - let registry_slot_name = StorageSlotName::new("miden::agglayer::bridge::faucet_registry")?; + let registry_slot_name = AggLayerBridge::faucet_registry_slot_name(); let key = faucet_registry_key(faucet_to_register); - let value_before = bridge_account.storage().get_map_item(®istry_slot_name, key)?; + let value_before = bridge_account.storage().get_map_item(registry_slot_name, key)?; assert_eq!( value_before, [Felt::ZERO; 4].into(), @@ -78,7 +77,7 @@ async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { let mut updated_bridge = bridge_account.clone(); updated_bridge.apply_delta(executed_transaction.account_delta())?; - let value_after = updated_bridge.storage().get_map_item(®istry_slot_name, key)?; + let value_after = updated_bridge.storage().get_map_item(registry_slot_name, key)?; let expected_value = [Felt::new(1), Felt::ZERO, Felt::ZERO, Felt::ZERO].into(); assert_eq!( value_after, expected_value, diff --git a/crates/miden-testing/tests/agglayer/global_index.rs b/crates/miden-testing/tests/agglayer/global_index.rs index ad29d767d0..4e046cd9ab 100644 --- a/crates/miden-testing/tests/agglayer/global_index.rs +++ b/crates/miden-testing/tests/agglayer/global_index.rs @@ -23,7 +23,7 @@ fn assemble_process_global_index_program(global_index: GlobalIndex) -> Program { let script_code = format!( r#" use miden::core::sys - use miden::agglayer::bridge_in + use miden::agglayer::bridge::bridge_in begin push.{g7}.{g6}.{g5}.{g4}.{g3}.{g2}.{g1}.{g0} diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/leaf_utils.rs similarity index 62% rename from crates/miden-testing/tests/agglayer/crypto_utils.rs rename to crates/miden-testing/tests/agglayer/leaf_utils.rs index 28fd421451..7d1d414cf5 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/leaf_utils.rs @@ -1,27 +1,21 @@ extern crate alloc; -use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use anyhow::Context; +use miden_agglayer::agglayer_library; use miden_agglayer::claim_note::Keccak256Output; use miden_agglayer::utils::felts_to_bytes; -use miden_agglayer::{ExitRoot, SmtNode, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_crypto::SequentialCommit; use miden_processor::AdviceInputs; use miden_protocol::{Felt, Word}; -use miden_standards::code_builder::CodeBuilder; -use miden_testing::TransactionContextBuilder; use miden_tx::utils::hex_to_bytes; use super::test_utils::{ LEAF_VALUE_VECTORS_JSON, LeafValueVector, - MerkleProofVerificationFile, - SOLIDITY_MERKLE_PROOF_VECTORS, execute_program_with_default_host, }; @@ -37,73 +31,6 @@ fn felts_to_le_bytes(limbs: &[Felt]) -> Vec { bytes } -fn merkle_proof_verification_code( - index: usize, - merkle_paths: &MerkleProofVerificationFile, -) -> String { - // generate the code which stores the merkle path to the memory - let mut store_path_source = String::new(); - for height in 0..32 { - let path_node = merkle_paths.merkle_paths[index * 32 + height].as_str(); - let smt_node = SmtNode::from(hex_to_bytes(path_node).unwrap()); - let [node_lo, node_hi] = smt_node.to_words(); - // each iteration (each index in leaf/root vector) we rewrite the merkle path nodes, so the - // memory pointers for the merkle path and the expected root never change - store_path_source.push_str(&format!( - " - \tpush.{node_lo} mem_storew_be.{} dropw - \tpush.{node_hi} mem_storew_be.{} dropw - ", - height * 8, - height * 8 + 4 - )); - } - - // prepare the root for the provided index - let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); - let [root_lo, root_hi] = root.to_words(); - - // prepare the leaf for the provided index - let leaf = Keccak256Output::from(hex_to_bytes(&merkle_paths.leaves[index]).unwrap()); - let [leaf_lo, leaf_hi] = leaf.to_words(); - - format!( - r#" - use miden::agglayer::crypto_utils - use miden::core::word - - begin - # store the merkle path to the memory (double word slots from 0 to 248) - {store_path_source} - # => [] - - # store the root to the memory (double word slot 256) - push.{root_lo} mem_storew_be.256 dropw - push.{root_hi} mem_storew_be.260 dropw - # => [] - - # prepare the stack for the `verify_merkle_proof` procedure - push.256 # expected root memory pointer - push.{index} # provided leaf index - push.0 # Merkle path memory pointer - # in practice this is never "pushed" to the stack, but rather an output of `get_leaf_value` - # which returns the leaf value in LE-felt order - push.{leaf_hi} - exec.word::reverse - push.{leaf_lo} - exec.word::reverse - # => [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx, expected_root_ptr] - - exec.crypto_utils::verify_merkle_proof - # => [verification_flag] - - assert.err="verification failed" - # => [] - end - "# - ) -} - // TESTS // ================================================================================================ @@ -176,7 +103,7 @@ async fn pack_leaf_data() -> anyhow::Result<()> { let source = format!( r#" use miden::core::mem - use miden::agglayer::crypto_utils + use miden::agglayer::bridge::leaf_utils const LEAF_DATA_START_PTR = 0 const LEAF_DATA_NUM_WORDS = 8 @@ -188,7 +115,7 @@ async fn pack_leaf_data() -> anyhow::Result<()> { push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS exec.mem::pipe_preimage_to_memory drop - exec.crypto_utils::pack_leaf_data + exec.leaf_utils::pack_leaf_data end "# ); @@ -241,13 +168,12 @@ async fn get_leaf_value() -> anyhow::Result<()> { let source = format!( r#" - use miden::core::mem use miden::core::sys - use miden::agglayer::crypto_utils + use miden::agglayer::bridge::bridge_in begin push.{key} - exec.crypto_utils::get_leaf_value + exec.bridge_in::get_leaf_value exec.sys::truncate_stack end "# @@ -272,30 +198,3 @@ async fn get_leaf_value() -> anyhow::Result<()> { assert_eq!(computed_leaf_value, expected_leaf_value); Ok(()) } -#[tokio::test] -async fn test_solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { - let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; - - // Validate array lengths - assert_eq!(merkle_paths.leaves.len(), merkle_paths.roots.len()); - // paths have 32 nodes for each leaf/root, so the overall paths length should be 32 times longer - // than leaves/roots length - assert_eq!(merkle_paths.leaves.len() * 32, merkle_paths.merkle_paths.len()); - - for leaf_index in 0..32 { - let source = merkle_proof_verification_code(leaf_index, merkle_paths); - - let tx_script = CodeBuilder::new() - .with_statically_linked_library(&agglayer_library())? - .compile_tx_script(source)?; - - TransactionContextBuilder::with_existing_mock_account() - .tx_script(tx_script.clone()) - .build()? - .execute() - .await - .context(format!("failed to execute transaction with leaf index {leaf_index}"))?; - } - - Ok(()) -} diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs index 00bb195e76..083719ced2 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -73,8 +73,9 @@ impl KeccakMmrFrontier32 { async fn test_append_and_update_frontier() -> anyhow::Result<()> { let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); - let mut source = - "use miden::agglayer::mmr_frontier32_keccak use miden::core::word begin".to_string(); + let mut source = "use miden::agglayer::bridge::mmr_frontier32_keccak \ + use miden::core::word begin" + .to_string(); for round in 0..32 { // construct the leaf from the hex representation of the round number @@ -110,8 +111,9 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { let zero_31 = *CANONICAL_ZEROS_32.get(31).expect("zeros should have 32 values total"); let empty_mmr_root = Keccak256::merge(&[zero_31, zero_31]); - let mut source = - "use miden::agglayer::mmr_frontier32_keccak use miden::core::word begin".to_string(); + let mut source = "use miden::agglayer::bridge::mmr_frontier32_keccak \ + use miden::core::word begin" + .to_string(); for round in 1..=32 { // check that pushing the zero leaves into the MMR doesn't change its root diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 7fbb3d38a0..a497f74230 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -2,8 +2,8 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; mod config_bridge; -mod crypto_utils; mod global_index; +mod leaf_utils; mod mmr_frontier; mod solidity_miden_address_conversion; pub mod test_utils; diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index f1bf21ea6d..f9290a6273 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -133,7 +133,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { let script_code = format!( r#" use miden::core::sys - use miden::agglayer::eth_address + use miden::agglayer::common::eth_address begin push.{}.{}.{}.{}.{} diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 907fff9c2d..3d34912300 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -5,7 +5,13 @@ use alloc::sync::Arc; use alloc::vec::Vec; use miden_agglayer::utils::felts_to_bytes; -use miden_agglayer::{ExitRoot, UpdateGerNote, agglayer_library, create_existing_bridge_account}; +use miden_agglayer::{ + AggLayerBridge, + ExitRoot, + UpdateGerNote, + agglayer_library, + create_existing_bridge_account, +}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_core_lib::handlers::bytes_to_packed_u32_felts; @@ -13,7 +19,6 @@ use miden_core_lib::handlers::keccak256::KeccakPreimage; use miden_crypto::hash::rpo::Rpo256 as Hasher; use miden_crypto::{Felt, FieldElement}; use miden_protocol::Word; -use miden_protocol::account::StorageSlotName; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; use miden_protocol::utils::sync::LazyLock; @@ -105,10 +110,10 @@ async fn update_ger_note_updates_storage() -> anyhow::Result<()> { let ger_hash = Hasher::merge(&[ger_upper.into(), ger_lower.into()]); // Look up the GER hash in the map storage - let ger_storage_slot = StorageSlotName::new("miden::agglayer::bridge::ger")?; + let ger_storage_slot = AggLayerBridge::ger_map_slot_name(); let stored_value = updated_bridge_account .storage() - .get_map_item(&ger_storage_slot, ger_hash) + .get_map_item(ger_storage_slot, ger_hash) .expect("GER hash should be stored in the map"); // The stored value should be [GER_KNOWN_FLAG, 0, 0, 0] = [1, 0, 0, 0] @@ -166,7 +171,7 @@ async fn compute_ger() -> anyhow::Result<()> { let source = format!( r#" use miden::core::sys - use miden::agglayer::crypto_utils + use miden::agglayer::bridge::bridge_in begin # Initialize memory with exit roots @@ -174,7 +179,7 @@ async fn compute_ger() -> anyhow::Result<()> { # Call compute_ger with pointer to exit roots push.0 - exec.crypto_utils::compute_ger + exec.bridge_in::compute_ger exec.sys::truncate_stack end "# @@ -249,7 +254,7 @@ async fn test_compute_ger_basic() -> anyhow::Result<()> { let source = format!( r#" use miden::core::sys - use miden::agglayer::crypto_utils + use miden::agglayer::bridge::bridge_in begin # Initialize memory with exit roots @@ -257,7 +262,7 @@ async fn test_compute_ger_basic() -> anyhow::Result<()> { # Call compute_ger with pointer to exit roots push.0 - exec.crypto_utils::compute_ger + exec.bridge_in::compute_ger exec.sys::truncate_stack end "# From b8c3b82eb19a9429edf12f82525d17c428347f4a Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:08:19 +0300 Subject: [PATCH 17/21] refactor: wrap `verify_u128_*` in `verify_u256_*` procedure (#2504) Co-authored-by: Marti --- .../asm/agglayer/common/asset_conversion.masm | 96 ++++++++++++++----- crates/miden-agglayer/src/errors/agglayer.rs | 4 +- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm b/crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm index 1693356c0e..bc455ab001 100644 --- a/crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm +++ b/crates/miden-agglayer/asm/agglayer/common/asset_conversion.masm @@ -7,7 +7,7 @@ use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT # ================================================================================================= const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT="maximum scaling factor is 18" -const ERR_X_TOO_LARGE="x must fit into 128 bits (x4..x7 must be 0)" +const ERR_X_TOO_LARGE="the agglayer bridge in u256 value is larger than 2**128 and cannot be verifiably scaled to u64" const ERR_UNDERFLOW="x < y*10^s (underflow detected)" const ERR_REMAINDER_TOO_LARGE="remainder z must be < 10^s" const ERR_Y_TOO_LARGE="y exceeds max fungible token amount" @@ -198,7 +198,7 @@ proc u128_sub_no_underflow # => [z0, z1, z2, z3] end -#! Verify conversion from an AggLayer U256 amount to a Miden native amount (Felt) +#! Verify conversion from a U128 amount to a Miden native amount (Felt) #! #! Specification: #! Verify that a provided y is the quotient of dividing x by 10^scale_exp: @@ -228,19 +228,16 @@ end #! y_scaled = y * 10^10 = 100 * 10^18 #! z = x - y_scaled = 0 #! -#! NOTE: For efficiency, this verifier enforces x < 2^128 by requiring x4..x7 == 0. -#! -#! Inputs: [x7, x6, x5, x4, x3, x2, x1, x0, scale_exp, y] -#! Where x is encoded as 8 u32 limbs in big-endian order. -#! (x7 is most significant limb and is at the top of the stack) -#! Each limb is expected to contain little-endian bytes. +#! Inputs: [x0, x1, x2, x3, scale_exp, y] +#! Where x is encoded as 4 u32 limbs in little-endian order. +#! (x0 is least significant limb) #! Outputs: [y] #! #! Where: -#! - x: The original AggLayer amount as an unsigned 256-bit integer (U256). -#! It is provided on the operand stack as 8 big-endian u32 limbs: -#! x = x0 + x1·2^32 + x2·2^64 + x3·2^96 + x4·2^128 + x5·2^160 + x6·2^192 + x7·2^224 -#! - x0..x7: 32-bit limbs of x in big-endian order (x0 is least significant). +#! - x: The original amount as an unsigned 128-bit integer (U128). +#! It is provided on the operand stack as 4 little-endian u32 limbs: +#! x = x0 + x1·2^32 + x2·2^64 + x3·2^96 +#! - x0..x3: 32-bit limbs of x in little-endian order (x0 is least significant). #! - scale_exp: The base-10 exponent used for scaling down (an integer in [0, 18]). #! - y: The provided quotient (Miden native amount) as a Felt interpreted as an unsigned u64. #! - y_scaled: The 256-bit value y * 10^scale_exp represented as 8 u32 limbs (big-endian). @@ -250,23 +247,10 @@ end #! Panics if: #! - scale_exp > 18 (asserted in pow10 via scale_native_amount_to_u256) #! - y exceeds the max fungible token amount -#! - x does not fit into 128 bits (x4..x7 are not all zero) #! - x < y * 10^scale_exp (underflow) #! - z does not fit in 64 bits #! - (z1, z0) >= 10^scale_exp (remainder too large) -pub proc verify_u256_to_native_amount_conversion - - # reverse limbs and byte endianness - exec.reverse_limbs_and_change_byte_endianness - # => [x0, x1, x2, x3, x4, x5, x6, x7, scale_exp, y] - - # ============================================================================================= - # Step 0: Enforce x < 2^128 - # Constraint: x4 == x5 == x6 == x7 == 0 - # ============================================================================================= - swapw - exec.word::eqz - assert.err=ERR_X_TOO_LARGE +pub proc verify_u128_to_native_amount_conversion # => [x0, x1, x2, x3, scale_exp, y] # ============================================================================================= @@ -345,3 +329,63 @@ pub proc verify_u256_to_native_amount_conversion assert.err=ERR_REMAINDER_TOO_LARGE # => [y] end + +#! Verify conversion from an AggLayer U256 amount to a Miden native amount (Felt) +#! +#! This procedure first checks that the U256 value fits in 128 bits (x4..x7 == 0), +#! then delegates to verify_u128_to_native_amount_conversion for the actual verification. +#! +#! Specification: +#! Verify that a provided y is the quotient of dividing x by 10^scale_exp: +#! y = floor(x / 10^scale_exp) +#! +#! Example (ETH -> Miden base 1e8): +#! - EVM amount: 100 ETH = 100 * 10^18 +#! - Miden amount: 100 ETH = 100 * 10^8 +#! - Therefore the scale-down factor is: +#! scale = 10^(18 - 8) = 10^10 +#! scale_exp = 10 +#! - Inputs/expected values: +#! x = 100 * 10^18 +#! y = floor(x / 10^10) = 100 * 10^8 +#! y_scaled = y * 10^10 = 100 * 10^18 +#! z = x - y_scaled = 0 +#! +#! Inputs: [x7, x6, x5, x4, x3, x2, x1, x0, scale_exp, y] +#! Where x is encoded as 8 u32 limbs in big-endian order. +#! (x7 is most significant limb and is at the top of the stack) +#! Each limb is expected to contain little-endian bytes. +#! Outputs: [y] +#! +#! Where: +#! - x: The original AggLayer amount as an unsigned 256-bit integer (U256). +#! It is provided on the operand stack as 8 big-endian u32 limbs: +#! x = x0 + x1·2^32 + x2·2^64 + x3·2^96 + x4·2^128 + x5·2^160 + x6·2^192 + x7·2^224 +#! - x0..x7: 32-bit limbs of x in big-endian order (x0 is least significant). +#! - scale_exp: The base-10 exponent used for scaling down (an integer in [0, 18]). +#! - y: The provided quotient (Miden native amount) as a Felt interpreted as an unsigned u64. +#! +#! Panics if: +#! - x does not fit into 128 bits (x4..x7 are not all zero) +#! - scale_exp > 18 (asserted in pow10 via scale_native_amount_to_u256) +#! - y exceeds the max fungible token amount +#! - x < y * 10^scale_exp (underflow) +#! - z does not fit in 64 bits +#! - (z1, z0) >= 10^scale_exp (remainder too large) +pub proc verify_u256_to_native_amount_conversion + + # reverse limbs and byte endianness + exec.reverse_limbs_and_change_byte_endianness + # => [x0, x1, x2, x3, x4, x5, x6, x7, scale_exp, y] + + # Enforce x < 2^128 + # Constraint: x4 == x5 == x6 == x7 == 0 + swapw + exec.word::eqz + assert.err=ERR_X_TOO_LARGE + # => [x0, x1, x2, x3, scale_exp, y] + + # Delegate to verify_u128_to_native_amount_conversion for the remaining verification + exec.verify_u128_to_native_amount_conversion + # => [y] +end diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index a3d27eecf5..ae150ccf8e 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -76,8 +76,8 @@ pub const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_st /// Error Message: "UPDATE_GER script expects exactly 8 note storage items" pub const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("UPDATE_GER script expects exactly 8 note storage items"); -/// Error Message: "x must fit into 128 bits (x4..x7 must be 0)" -pub const ERR_X_TOO_LARGE: MasmError = MasmError::from_static_str("x must fit into 128 bits (x4..x7 must be 0)"); +/// Error Message: "the agglayer bridge in u256 value is larger than 2**128 and cannot be verifiably scaled to u64" +pub const ERR_X_TOO_LARGE: MasmError = MasmError::from_static_str("the agglayer bridge in u256 value is larger than 2**128 and cannot be verifiably scaled to u64"); /// Error Message: "y exceeds max fungible token amount" pub const ERR_Y_TOO_LARGE: MasmError = MasmError::from_static_str("y exceeds max fungible token amount"); From 6bc690345ba524960ba4adfe56292f2e2f10253b Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 25 Feb 2026 09:23:59 +0100 Subject: [PATCH 18/21] refactor: enforce `CLAIM` note consumer via `NetworkAccountTarget` attachment, not `NoteStorage` (#2480) * refactor: enforce CLAIM note consumer via NetworkAccountTarget attachment Replace the storage-based assert_aggfaucet_is_consumer check with the standard network_account_target::active_account_matches_target_account attachment check, unifying how all agglayer note scripts enforce their target consumer. The CLAIM note already carries the target faucet ID in its NetworkAccountTarget attachment (set during note creation in Rust). This change makes the note script use that attachment for the consumer check instead of reading target_faucet_account_id from note storage. Closes #2468 Co-authored-by: marti * refactor: remove target_faucet_account_id from CLAIM NoteStorage Now that the consumer check uses the NetworkAccountTarget attachment, the target_faucet_account_id no longer needs to be serialized into NoteStorage. The field is kept in OutputNoteData (used to build the attachment in Rust) but excluded from to_elements(). The OutputNoteData memory layout changes from: [serial_num(4), faucet_id(2), tag(1), amount(1)] to: [serial_num(4), tag(1), amount(1), padding(2)] Updated MASM constants: - OUTPUT_NOTE_TAG_MEM_ADDR: 574 -> 572 - OUTPUT_NOTE_FAUCET_AMOUNT: 575 -> 573 Co-authored-by: marti * refactor: move target_faucet_id out of OutputNoteData into create_claim_note param The target faucet account ID is only needed to build the NetworkAccountTarget attachment, not as part of the note storage data. Move it from OutputNoteData to a direct parameter of create_claim_note. Co-authored-by: marti * lint --------- Co-authored-by: Cursor Agent Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- .../asm/agglayer/faucet/mod.masm | 7 ++- .../asm/note_scripts/CLAIM.masm | 57 +++++-------------- crates/miden-agglayer/src/claim_note.rs | 29 +++++----- crates/miden-agglayer/src/errors/agglayer.rs | 4 +- .../miden-testing/tests/agglayer/bridge_in.rs | 8 ++- 5 files changed, 39 insertions(+), 66 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index f62567585c..0ad07c295f 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -55,8 +55,8 @@ const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556 const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 -const OUTPUT_NOTE_TAG_MEM_ADDR = 574 -const OUTPUT_NOTE_FAUCET_AMOUNT = 575 +const OUTPUT_NOTE_TAG_MEM_ADDR = 572 +const OUTPUT_NOTE_FAUCET_AMOUNT = 573 const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 @@ -392,8 +392,9 @@ end #! ], #! OUTPUT_NOTE_DATA_KEY => [ #! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) -#! agglayer_faucet_account_id[2], // Agglayer faucet account ID (2 felts, prefix and suffix) #! output_note_tag[1], // P2ID output note tag +#! miden_claim_amount[1], // Miden claim amount (1 felt) +#! padding[2], // padding (2 felts) #! ] #! } #! diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index 6eb1bc3539..0c0dbf9ace 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -1,11 +1,10 @@ use miden::agglayer::faucet -> agg_faucet -use miden::protocol::account_id -use miden::protocol::active_account use miden::protocol::active_note use miden::protocol::note use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 use miden::core::mem +use miden::standards::attachments::network_account_target # CONSTANTS # ================================================================================================= @@ -18,37 +17,10 @@ const PROOF_DATA_START_PTR = 0 const LEAF_DATA_START_PTR = 536 const OUTPUT_NOTE_DATA_START_PTR = 568 -const TARGET_FAUCET_PREFIX_MEM_ADDR = 572 -const TARGET_FAUCET_SUFFIX_MEM_ADDR = 573 - # ERRORS # ================================================================================================= -const ERR_CLAIM_TARGET_ACCT_MISMATCH = "CLAIM's target account address and transaction address do not match" - -#! Asserts that the consuming account matches the target agglayer faucet account. -#! -#! This procedure ensures that only the specified agglayer faucet account can consume -#! this CLAIM note. It assumes that the note storage has already been loaded into memory -#! via active_note::get_storage. -#! -#! Inputs: [] -#! Output: [] -#! -#! Panics if: -#! - The consuming account ID does not match the target faucet account ID stored in memory -proc assert_aggfaucet_is_consumer - # Load target faucet ID (assumes active_note::get_storage has been called) - mem_load.TARGET_FAUCET_SUFFIX_MEM_ADDR mem_load.TARGET_FAUCET_PREFIX_MEM_ADDR - # => [target_faucet_prefix, target_faucet_suffix] - - exec.active_account::get_id - # => [account_id_prefix, account_id_suffix, target_faucet_prefix, target_faucet_suffix] - - # ensure only the specified target faucet can consume this CLAIM note, not any other account - exec.account_id::is_equal assert.err=ERR_CLAIM_TARGET_ACCT_MISMATCH - # => [] -end +const ERR_CLAIM_TARGET_ACCT_MISMATCH = "CLAIM note attachment target account does not match consuming account" #! Reads claim data from memory and inserts it into the advice map under three separate keys. #! @@ -78,12 +50,11 @@ end #! padding[3], // padding (3 felts) #! ] #! -#! TODO: Will be removed in future PR #! OUTPUT_NOTE_DATA_KEY => [ #! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) -#! target_faucet_account_id[2], // Target faucet account ID (2 felts, prefix and suffix) #! output_note_tag[1], // P2ID output note tag #! miden_claim_amount[1], // Miden claim amount (1 felt) +#! padding[2], // padding (2 felts) #! ] #! #! Invocation: exec @@ -139,7 +110,7 @@ end #! Agglayer Faucet CLAIM script: claims assets by calling the agglayer faucet's claim function. #! #! This note can only be consumed by the specific agglayer faucet account whose ID is provided -#! in the note storage (target_faucet_account_id). Upon consumption, it will create a P2ID note. +#! in the note attachment (NetworkAccountTarget). Upon consumption, it will create a P2ID note. #! #! Requires that the account exposes: #! - agglayer::agglayer_faucet::claim procedure. @@ -162,9 +133,9 @@ end #! - metadata [557..564]: 8 felts #! - padding [565..567]: 3 felts #! - output_p2id_serial_num [568..571]: 4 felts -#! - target_faucet_account_id [572..573]: 2 felts -#! - output_note_tag [574] : 1 felt -#! - miden_claim_amount [575] : 1 felt +#! - output_note_tag [572] : 1 felt +#! - miden_claim_amount [573] : 1 felt +#! - padding [574..575]: 2 felts #! #! Where: #! - smtProofLocalExitRoot: SMT proof for local exit root (bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) @@ -183,27 +154,25 @@ end #! - destinationAddress: 20-byte Ethereum address decodable into a Miden AccountId (5 u32 felts) #! - amount: Amount of tokens (uint256 as 8 u32 felts) #! - metadata: ABI encoded metadata (fixed size) -#! - padding (3 felts) #! - output_p2id_serial_num: P2ID note serial number (Word) -#! - target_faucet_account_id: Target agglayer faucet account ID (prefix and suffix). Only this specific -#! account can consume the note - any other account will cause a panic. #! - output_note_tag: P2ID output note tag #! - miden_claim_amount: Scaled-down Miden token amount (Felt). This is the Y value computed from #! scaling down the Ethereum amount (X) by the scale exponent: Y = floor(X / 10^scale_exp) #! #! Panics if: #! - account does not expose claim procedure. -#! - target faucet account ID does not match the consuming account ID. +#! - note attachment target account does not match the consuming account. begin dropw # => [pad(16)] - # Load CLAIM note storage into memory, starting at address 0 - push.0 exec.active_note::get_storage drop drop + # Ensure note attachment targets the consuming faucet account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_CLAIM_TARGET_ACCT_MISMATCH # => [pad(16)] - # Check consuming account == aggfaucet - exec.assert_aggfaucet_is_consumer + # Load CLAIM note storage into memory, starting at address 0 + push.0 exec.active_note::get_storage drop drop # => [pad(16)] exec.write_claim_data_into_advice_map_by_key diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index b90220927d..9913177701 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -172,13 +172,10 @@ impl SequentialCommit for LeafData { /// Output note data for CLAIM note creation. /// Contains note-specific data and can use Miden types. -/// TODO: Remove all but target_faucet_account_id #[derive(Clone)] pub struct OutputNoteData { /// P2ID note serial number (4 felts as Word) pub output_p2id_serial_num: Word, - /// Target agg faucet account ID (2 felts: prefix and suffix) - pub target_faucet_account_id: AccountId, /// P2ID output note tag pub output_note_tag: NoteTag, /// Miden claim amount (scaled-down token amount as Felt) @@ -186,24 +183,26 @@ pub struct OutputNoteData { } impl OutputNoteData { - /// Converts the output note data to a vector of field elements for note storage + /// Converts the output note data to a vector of field elements for note storage. + /// + /// Layout (8 felts = 2 words): + /// `[serial_num(4), tag(1), miden_claim_amount(1), padding(2)]` pub fn to_elements(&self) -> Vec { - const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 8; // 4 + 2 + 1 + 1 (serial_num + account_id + tag + miden_claim_amount) + const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 8; let mut elements = Vec::with_capacity(OUTPUT_NOTE_DATA_ELEMENT_COUNT); // P2ID note serial number (4 felts as Word) elements.extend(self.output_p2id_serial_num); - // Target faucet account ID (2 felts: prefix and suffix) - elements.push(self.target_faucet_account_id.prefix().as_felt()); - elements.push(self.target_faucet_account_id.suffix()); - // Output note tag elements.push(Felt::new(self.output_note_tag.as_u32() as u64)); // Miden claim amount elements.push(self.miden_claim_amount); + // Padding to keep 8 felts (2 words) for pipe_double_words_preimage_to_memory + elements.extend([Felt::ZERO; 2]); + elements } } @@ -245,6 +244,8 @@ impl TryFrom for NoteStorage { /// /// # Parameters /// - `storage`: The core storage for creating the CLAIM note +/// - `target_faucet_id`: The account ID of the agglayer faucet that should consume this note. +/// Encoded as a `NetworkAccountTarget` attachment on the note metadata. /// - `sender_account_id`: The account ID of the CLAIM note creator /// - `rng`: Random number generator for creating the CLAIM note serial number /// @@ -252,17 +253,15 @@ impl TryFrom for NoteStorage { /// Returns an error if note creation fails. pub fn create_claim_note( storage: ClaimNoteStorage, + target_faucet_id: AccountId, sender_account_id: AccountId, rng: &mut R, ) -> Result { let note_storage = NoteStorage::try_from(storage.clone())?; - let attachment = NetworkAccountTarget::new( - storage.output_note_data.target_faucet_account_id, - NoteExecutionHint::Always, - ) - .map_err(|e| NoteError::other(e.to_string()))? - .into(); + let attachment = NetworkAccountTarget::new(target_faucet_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))? + .into(); let metadata = NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index ae150ccf8e..91e98d3725 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -19,8 +19,8 @@ pub const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_s /// Error Message: "bridge not mainnet" pub const ERR_BRIDGE_NOT_MAINNET: MasmError = MasmError::from_static_str("bridge not mainnet"); -/// Error Message: "CLAIM's target account address and transaction address do not match" -pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM's target account address and transaction address do not match"); +/// Error Message: "CLAIM note attachment target account does not match consuming account" +pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM note attachment target account does not match consuming account"); /// Error Message: "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" pub const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE note attachment target account does not match consuming account"); diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 780fec2083..251f9ccc96 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -210,14 +210,18 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a let output_note_data = OutputNoteData { output_p2id_serial_num: serial_num, - target_faucet_account_id: agglayer_faucet.id(), output_note_tag: NoteTag::with_account_target(destination_account_id), miden_claim_amount, }; let claim_inputs = ClaimNoteStorage { proof_data, leaf_data, output_note_data }; - let claim_note = create_claim_note(claim_inputs, sender_account.id(), builder.rng_mut())?; + let claim_note = create_claim_note( + claim_inputs, + agglayer_faucet.id(), + sender_account.id(), + builder.rng_mut(), + )?; // Add the claim note to the builder before building the mock chain builder.add_output_note(OutputNote::Full(claim_note.clone())); From 893c7de07b47f2aaa7aa713f2f876f316ac99193 Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 25 Feb 2026 12:58:54 +0100 Subject: [PATCH 19/21] fix: Move sender validation from note script into bridge procedures (#2511) * refactor: move sender authorization from note scripts into account procedures Sender authorization for UPDATE_GER and CONFIG_AGG_BRIDGE notes was previously enforced in the note scripts via separate calls to assert_sender_is_ger_manager / assert_sender_is_bridge_admin. This was unsafe because any note script consumed by the bridge account could call update_ger or register_faucet directly, bypassing the sender check. Move the authorization into the account procedures themselves (update_ger and register_faucet) so that sender verification is enforced regardless of which note script invokes them. The assertion procedures have a net-zero stack effect, so they can be called first thing without disturbing the procedure's inputs. https://claude.ai/code/session_01JeSHNeBxs1aTUe3Lm8NjVi * refactor: remove superfluous stack-effect comments https://claude.ai/code/session_01JeSHNeBxs1aTUe3Lm8NjVi * Apply suggestions from code review * Update crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm * Update crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm * refactor: remove assert_sender_is_* from bridge component interface These procedures are now internal implementation details called via exec from within update_ger and register_faucet. They no longer need to be part of the bridge account's public interface. https://claude.ai/code/session_01JeSHNeBxs1aTUe3Lm8NjVi --------- Co-authored-by: Claude --- .../asm/agglayer/bridge/bridge_config.masm | 18 ++++++++++++++---- .../miden-agglayer/asm/components/bridge.masm | 2 -- .../asm/note_scripts/CONFIG_AGG_BRIDGE.masm | 6 ------ .../asm/note_scripts/UPDATE_GER.masm | 6 ------ 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm index 5193fb3ed6..2e2d80da89 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm @@ -33,11 +33,17 @@ const IS_FAUCET_REGISTERED_FLAG=1 #! 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. #! +#! Panics if the note sender is not the global exit root manager. +#! #! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)] #! Outputs: [pad(16)] #! #! Invocation: call pub proc update_ger + # assert the note sender is the global exit root manager. + exec.assert_sender_is_ger_manager + # => [GER_LOWER[4], GER_UPPER[4], pad(8)] + # compute hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) # inputs: [B, A] => output: hash(A || B) exec.rpo256::merge @@ -98,19 +104,23 @@ end #! The sentinel value `[1, 0, 0, 0]` distinguishes registered faucets from #! non-existent entries (SMTs return EMPTY_WORD for missing keys). #! +#! Panics if the note sender is not the bridge admin. +#! #! Inputs: [faucet_id_prefix, faucet_id_suffix, pad(14)] #! Outputs: [pad(16)] #! #! Invocation: call pub proc register_faucet + # assert the note sender is the bridge admin. + exec.assert_sender_is_bridge_admin # => [faucet_id_prefix, faucet_id_suffix, pad(14)] # set_map_item expects [slot_id(2), KEY(4), VALUE(4)] and returns [OLD_VALUE(4)]. push.IS_FAUCET_REGISTERED_FLAG - # => [IS_FAUCET_REGISTERED_FLAG, slot_id_prefix, slot_id_suffix, pad(14)] + # => [IS_FAUCET_REGISTERED_FLAG, faucet_id_prefix, faucet_id_suffix, pad(14)] movdn.7 - # => [[slot_id_prefix, slot_id_suffix, 0, 0], [0, 0, 0, IS_FAUCET_REGISTERED_FLAG], pad(9)] + # => [[faucet_id_prefix, faucet_id_suffix, 0, 0], [0, 0, 0, IS_FAUCET_REGISTERED_FLAG], pad(9)] # Place slot ID on top push.FAUCET_REGISTRY_SLOT[0..2] @@ -161,7 +171,7 @@ end #! Panics if: #! - the note sender does not match the bridge admin account ID. #! -#! Invocation: call +#! Invocation: exec pub proc assert_sender_is_bridge_admin # => [pad(16)] @@ -191,7 +201,7 @@ end #! Panics if: #! - the note sender does not match the GER manager account ID. #! -#! Invocation: call +#! Invocation: exec pub proc assert_sender_is_ger_manager # => [pad(16)] diff --git a/crates/miden-agglayer/asm/components/bridge.masm b/crates/miden-agglayer/asm/components/bridge.masm index 2d919b687d..98c8287576 100644 --- a/crates/miden-agglayer/asm/components/bridge.masm +++ b/crates/miden-agglayer/asm/components/bridge.masm @@ -3,8 +3,6 @@ # This is a thin wrapper that re-exports bridge-related procedures from the # agglayer library. -pub use ::miden::agglayer::bridge::bridge_config::assert_sender_is_bridge_admin -pub use ::miden::agglayer::bridge::bridge_config::assert_sender_is_ger_manager pub use ::miden::agglayer::bridge::bridge_config::register_faucet pub use ::miden::agglayer::bridge::bridge_config::update_ger pub use ::miden::agglayer::bridge::bridge_in::verify_leaf_bridge diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm index 2e161f0447..8201c9ae5d 100644 --- a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -25,7 +25,6 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! #! Requires that the account exposes: #! - agglayer::bridge_config::register_faucet procedure. -#! - agglayer::bridge_config::assert_sender_is_bridge_admin procedure. #! #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] @@ -40,7 +39,6 @@ const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note at #! #! Panics if: #! - The note attachment target account does not match the consuming bridge account. -#! - The note sender is not the bridge admin. #! - The note does not contain exactly 2 storage items. #! - The account does not expose the register_faucet procedure. #! @@ -53,10 +51,6 @@ begin assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH # => [pad(16)] - # Ensure the note sender is the bridge admin. - call.bridge_config::assert_sender_is_bridge_admin - # => [pad(16)] - # Load note storage to memory push.STORAGE_START_PTR exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index fe2a632e08..c1f5cb89d4 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -27,7 +27,6 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe #! #! Requires that the account exposes: #! - agglayer::bridge_config::update_ger procedure. -#! - agglayer::bridge_config::assert_sender_is_ger_manager procedure. #! #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] @@ -38,7 +37,6 @@ const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment targe #! Panics if: #! - account does not expose update_ger procedure. #! - target account ID does not match the consuming account ID. -#! - note sender is not the global exit root manager. #! - number of note storage items is not exactly 8. begin dropw @@ -49,10 +47,6 @@ begin assert.err=ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH # => [pad(16)] - # Ensure the note sender is the global exit root manager. - call.bridge_config::assert_sender_is_ger_manager - # => [pad(16)] - # proceed with the GER update logic push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage From 0394ef2a7717390345f00c182f058b03cfca017b Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 25 Feb 2026 13:30:43 +0100 Subject: [PATCH 20/21] docs(AggLayer): fix doc comments and inline comments for bridge components (#2510) * docs: fix doc comments and inline comments for bridge components - Update AggLayerBridge doc comments to list current procedures (assert_sender_is_bridge_admin, assert_sender_is_ger_manager) - Add bridge_admin and ger_manager storage slots to doc comments - Fix faucet_registry_key doc comment to match actual element order - Fix inline comments in tests ("bridge_out" -> "bridge") * Apply suggestions from code review --------- Co-authored-by: Claude Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- crates/miden-agglayer/src/lib.rs | 12 ++++++++---- crates/miden-testing/tests/agglayer/bridge_in.rs | 2 +- crates/miden-testing/tests/agglayer/bridge_out.rs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 5e44056335..91ed3338a1 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -153,10 +153,12 @@ static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { /// It reexports the procedures from `miden::agglayer::bridge`. When linking against this /// component, the `agglayer` library must be available to the assembler. /// The procedures of this component are: -/// - `bridge_out`, which bridges an asset from the AggLayer to the destination network. -/// - `verify_leaf_bridge`, which verifies a deposit leaf against one of the stored GERs. -/// - `update_ger`, which injects a new GER into the storage map. +/// - `assert_sender_is_bridge_admin`, which validates CONFIG note senders. +/// - `assert_sender_is_ger_manager`, which validates UPDATE_GER note senders. /// - `register_faucet`, which registers a faucet in the bridge. +/// - `update_ger`, which injects a new GER into the storage map. +/// - `verify_leaf_bridge`, which verifies a deposit leaf against one of the stored GERs. +/// - `bridge_out`, which bridges an asset out of Miden to the destination network. /// /// ## Storage Layout /// @@ -166,6 +168,8 @@ static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { /// - [`Self::ler_hi_slot_name`]: Stores the upper 32 bits of the LET root. /// - [`Self::let_num_leaves_slot_name`]: Stores the number of leaves in the LET frontier. /// - [`Self::faucet_registry_slot_name`]: Stores the faucet registry map. +/// - [`Self::bridge_admin_slot_name`]: Stores the bridge admin account ID. +/// - [`Self::ger_manager_slot_name`]: Stores the GER manager account ID. /// /// The bridge starts with an empty faucet registry; faucets are registered at runtime via /// CONFIG_AGG_BRIDGE notes. @@ -434,7 +438,7 @@ impl From for AccountComponent { /// Creates a faucet registry map key from a faucet account ID. /// -/// The key format is `[faucet_id_prefix, faucet_id_suffix, 0, 0]`. +/// The key format is `[0, 0, faucet_id_suffix, faucet_id_prefix]`. pub fn faucet_registry_key(faucet_id: AccountId) -> Word { Word::new([Felt::ZERO, Felt::ZERO, faucet_id.suffix(), faucet_id.prefix().as_felt()]) } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 251f9ccc96..8b941d68fa 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -123,7 +123,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; - // CREATE BRIDGE ACCOUNT (with bridge_out component for MMR validation) + // CREATE BRIDGE ACCOUNT // -------------------------------------------------------------------------------------------- let bridge_seed = builder.rng_mut().draw_word(); let bridge_account = diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index a5b888dad3..8b40e17584 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -414,7 +414,7 @@ async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { auth_scheme: AuthScheme::Falcon512Rpo })?; - // Create a bridge account (includes a `bridge_out` component) + // Create a bridge account (includes a `bridge` component) let bridge_account = create_existing_bridge_account( builder.rng_mut().draw_word(), bridge_admin.id(), From 7fe02b673275fd89deb11e302a2e00aff06fdef0 Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:29:44 +0300 Subject: [PATCH 21/21] refactor: remove `OutputNoteData` from `AdviceMap` in `CLAIM` note (#2509) * feat: rm serial_num & output_note_tag from OutputNoteData * feat: rm OutputNoteData from AdviceMap * refactor: cleanup masm & fix pad amounts in stack comments * refactor: update number of NoteStorage values in comment * refactor: update claim_storage size and update comments * refactor: use prefix from proc input & cleanup pad stack comments * revert stack pad comment changes * fix: rm old comment * fix: correct CLAIM stack comments and flatten OutputNoteData into ClaimNoteStorage * fix: rustfmt * fix: fix stack comment pad(x) * chore: dropw -> drop is sufficient --------- Co-authored-by: Marti Co-authored-by: mmagician --- .../asm/agglayer/faucet/mod.masm | 58 ++++++++-------- .../asm/note_scripts/CLAIM.masm | 69 +++++++------------ crates/miden-agglayer/src/claim_note.rs | 59 ++-------------- crates/miden-agglayer/src/lib.rs | 10 +-- .../miden-testing/tests/agglayer/bridge_in.rs | 16 ++--- 5 files changed, 66 insertions(+), 146 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm index 0ad07c295f..d3b2913627 100644 --- a/crates/miden-agglayer/asm/agglayer/faucet/mod.masm +++ b/crates/miden-agglayer/asm/agglayer/faucet/mod.masm @@ -6,12 +6,12 @@ use miden::agglayer::common::eth_address use miden::protocol::active_account use miden::protocol::active_note use miden::standards::faucets +use miden::standards::note_tag use miden::protocol::note use miden::protocol::tx use miden::core::mem use miden::core::word - # ERRORS # ================================================================================================= @@ -54,11 +54,8 @@ const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = 553 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556 -const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 -const OUTPUT_NOTE_TAG_MEM_ADDR = 572 -const OUTPUT_NOTE_FAUCET_AMOUNT = 573 const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 -const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 +const OUTPUT_NOTE_FAUCET_AMOUNT = 568 # Memory locals in claim const CLAIM_PREFIX_MEM_LOC = 8 @@ -69,6 +66,7 @@ const CLAIM_AMOUNT_MEM_LOC_1 = 4 # Memory locals in build_p2id_output_note const BUILD_P2ID_AMOUNT_MEM_LOC_0 = 0 const BUILD_P2ID_AMOUNT_MEM_LOC_1 = 4 +const BUILD_P2ID_PREFIX_MEM_LOC = 8 # Data sizes const PROOF_DATA_WORD_LEN = 134 @@ -243,7 +241,7 @@ proc scale_down_amount repeat.7 drop end end -# Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] +# Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY] # Outputs: [] proc batch_pipe_double_words # 1) Verify PROOF_DATA_KEY @@ -261,14 +259,6 @@ proc batch_pipe_double_words push.LEAF_DATA_START_PTR push.LEAF_DATA_WORD_LEN exec.mem::pipe_double_words_preimage_to_memory drop - - # 3) Verify OUTPUT_NOTE_DATA_KEY - mem_storew_be.OUTPUT_NOTE_DATA_MEM_ADDR - adv.push_mapval - # => [OUTPUT_NOTE_DATA_KEY] - - push.OUTPUT_NOTE_DATA_START_PTR push.OUTPUT_NOTE_DATA_WORD_LEN - exec.mem::pipe_double_words_preimage_to_memory drop end #! Extracts the destination account ID as address[5] from memory. @@ -308,8 +298,11 @@ end #! TODO: Add an explicit assertion that AMOUNT[1] is zero. #! #! Note: This procedure will be refactored in a follow-up to use leaf data to build the output note. -@locals(8) +@locals(9) proc build_p2id_output_note + # save prefix to local memory for later use in note tag computation + dup loc_store.BUILD_P2ID_PREFIX_MEM_LOC + # write destination account id into memory for use in note::build_recipient push.OUTPUT_NOTE_STORAGE_MEM_ADDR add.1 mem_store mem_store.OUTPUT_NOTE_STORAGE_MEM_ADDR @@ -321,7 +314,8 @@ proc build_p2id_output_note procref.::miden::standards::notes::p2id::main # => [SCRIPT_ROOT] - swapw mem_loadw_be.OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR + # Use PROOF_DATA_KEY as the P2ID serial number + swapw mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR # => [SERIAL_NUM, SCRIPT_ROOT] push.P2ID_NOTE_NUM_STORAGE_ITEMS @@ -334,10 +328,14 @@ proc build_p2id_output_note # => [RECIPIENT] push.OUTPUT_NOTE_TYPE_PUBLIC - # => [note_type, RECIPIENT] + # => [note_type, RECIPIENT] - mem_load.OUTPUT_NOTE_TAG_MEM_ADDR - # => [tag, RECIPIENT] + # Compute note tag from destination account prefix (read from local memory) + loc_load.BUILD_P2ID_PREFIX_MEM_LOC + # => [account_id_prefix, note_type, RECIPIENT] + + exec.note_tag::create_account_target + # => [tag, note_type, RECIPIENT] padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 # => [AMOUNT[0], AMOUNT[1], tag, note_type, RECIPIENT] @@ -370,7 +368,7 @@ end #! 2) We can have a mapping in the bridge or in the faucet that stores consumed claim proofs #! as a hash -> bool value (similar to how it's done in the agglayer solidity contract). #! -#! Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY, pad(4)] +#! Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(7)] #! Outputs: [pad(16)] #! #! Advice map: { @@ -390,12 +388,6 @@ end #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) #! metadata[8], // ABI encoded metadata (8 felts, fixed size) #! ], -#! OUTPUT_NOTE_DATA_KEY => [ -#! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) -#! output_note_tag[1], // P2ID output note tag -#! miden_claim_amount[1], // Miden claim amount (1 felt) -#! padding[2], // padding (2 felts) -#! ] #! } #! #! Panics if: @@ -405,26 +397,30 @@ end #! Invocation: call @locals(10) # 2 for prefix and suffix, 8 for amount pub proc claim + # Write output note faucet amount to memory + movup.8 mem_store.OUTPUT_NOTE_FAUCET_AMOUNT + # => [PROOF_DATA_KEY, LEAF_DATA_KEY, pad(7)] + # Check AdviceMap values hash to keys & write CLAIM inputs & DATA_KEYs to global memory exec.batch_pipe_double_words - # => [pad(16)] + # => [pad(7)] # validate_claim will overwrite memory in-place, so we need to load the account and amount # before calling validate_claim and store it in memory locals exec.get_destination_account_id_data loc_store.CLAIM_PREFIX_MEM_LOC loc_store.CLAIM_SUFFIX_MEM_LOC - # => [pad(16)] + # => [pad(7)] exec.get_raw_claim_amount loc_storew_be.CLAIM_AMOUNT_MEM_LOC_0 dropw loc_storew_be.CLAIM_AMOUNT_MEM_LOC_1 dropw - # => [pad(16)] + # => [pad(7)] # VALIDATE CLAIM mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR - # => [PROOF_DATA_KEY, pad(12)] + # => [PROOF_DATA_KEY, pad(7)] swapw mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR - # => [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)] + # => [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(7)] # Errors on invalid proof exec.validate_claim diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index 0c0dbf9ace..8e420ff285 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -15,7 +15,7 @@ const OUTPUT_NOTE_SIZE = 8 const PROOF_DATA_START_PTR = 0 const LEAF_DATA_START_PTR = 536 -const OUTPUT_NOTE_DATA_START_PTR = 568 +const FAUCET_MINT_AMOUNT = 568 # ERRORS # ================================================================================================= @@ -28,7 +28,7 @@ const ERR_CLAIM_TARGET_ACCT_MISMATCH = "CLAIM note attachment target account doe #! into the advice map under separate keys for easier access. #! #! Inputs: [] -#! Outputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] +#! Outputs: [PROOF_DATA_KEY, LEAF_DATA_KEY] #! #! Advice map entries created: #! PROOF_DATA_KEY => [ @@ -50,61 +50,38 @@ const ERR_CLAIM_TARGET_ACCT_MISMATCH = "CLAIM note attachment target account doe #! padding[3], // padding (3 felts) #! ] #! -#! OUTPUT_NOTE_DATA_KEY => [ -#! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) -#! output_note_tag[1], // P2ID output note tag -#! miden_claim_amount[1], // Miden claim amount (1 felt) -#! padding[2], // padding (2 felts) -#! ] -#! #! Invocation: exec proc write_claim_data_into_advice_map_by_key - # 1) Get OUTPUT_NOTE_DATA_KEY - push.OUTPUT_NOTE_SIZE push.OUTPUT_NOTE_DATA_START_PTR - exec.rpo256::hash_elements - # => [OUTPUT_NOTE_DATA_KEY] - - push.OUTPUT_NOTE_SIZE add.OUTPUT_NOTE_DATA_START_PTR push.OUTPUT_NOTE_DATA_START_PTR - movdn.5 movdn.5 - # => [OUTPUT_NOTE_DATA_KEY, start_ptr, end_ptr] - - adv.insert_mem - # OS => [OUTPUT_NOTE_DATA_KEY, start_ptr, end_ptr, pad(16)] - # AM => {OUTPUT_NOTE_DATA_KEY: mem[start_ptr..end_ptr] } - - movup.4 drop movup.4 drop - # => [OUTPUT_NOTE_DATA_KEY] - - # 2) Get LEAF_DATA_KEY + # 1) Get LEAF_DATA_KEY push.LEAF_DATA_SIZE push.LEAF_DATA_START_PTR exec.rpo256::hash_elements - # => [LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # => [LEAF_DATA_KEY] push.LEAF_DATA_SIZE add.LEAF_DATA_START_PTR push.LEAF_DATA_START_PTR movdn.5 movdn.5 - # => [LEAF_DATA_KEY, start_ptr, end_ptr, OUTPUT_NOTE_DATA_KEY] + # => [LEAF_DATA_KEY, start_ptr, end_ptr] adv.insert_mem # OS => [LEAF_DATA_KEY, start_ptr, end_ptr] # AM => {LEAF_DATA_KEY: mem[start_ptr..end_ptr] } movup.4 drop movup.4 drop - # => [LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # => [LEAF_DATA_KEY] - # 3) Get PROOF_DATA_KEY + # 2) Get PROOF_DATA_KEY push.PROOF_DATA_SIZE push.PROOF_DATA_START_PTR exec.rpo256::hash_elements - # => [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # => [PROOF_DATA_KEY, LEAF_DATA_KEY] push.PROOF_DATA_SIZE push.PROOF_DATA_START_PTR movdn.5 movdn.5 - # => [PROOF_DATA_KEY, start_ptr, end_ptr, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # => [PROOF_DATA_KEY, start_ptr, end_ptr, LEAF_DATA_KEY] adv.insert_mem - # OS => [PROOF_DATA_KEY, start_ptr, end_ptr, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # OS => [PROOF_DATA_KEY, start_ptr, end_ptr, LEAF_DATA_KEY] # AM => {PROOF_DATA_KEY: mem[start_ptr..end_ptr] } movup.4 drop movup.4 drop - # => [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] + # => [PROOF_DATA_KEY, LEAF_DATA_KEY] end #! Agglayer Faucet CLAIM script: claims assets by calling the agglayer faucet's claim function. @@ -118,7 +95,7 @@ end #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] #! -#! NoteStorage layout (576 felts total): +#! NoteStorage layout (569 felts total): #! - smtProofLocalExitRoot [0..255] : 256 felts #! - smtProofRollupExitRoot [256..511]: 256 felts #! - globalIndex [512..519]: 8 felts @@ -132,10 +109,7 @@ end #! - amount [549..556]: 8 felts #! - metadata [557..564]: 8 felts #! - padding [565..567]: 3 felts -#! - output_p2id_serial_num [568..571]: 4 felts -#! - output_note_tag [572] : 1 felt -#! - miden_claim_amount [573] : 1 felt -#! - padding [574..575]: 2 felts +#! - miden_claim_amount [568] : 1 felt #! #! Where: #! - smtProofLocalExitRoot: SMT proof for local exit root (bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) @@ -154,8 +128,6 @@ end #! - destinationAddress: 20-byte Ethereum address decodable into a Miden AccountId (5 u32 felts) #! - amount: Amount of tokens (uint256 as 8 u32 felts) #! - metadata: ABI encoded metadata (fixed size) -#! - output_p2id_serial_num: P2ID note serial number (Word) -#! - output_note_tag: P2ID output note tag #! - miden_claim_amount: Scaled-down Miden token amount (Felt). This is the Y value computed from #! scaling down the Ethereum amount (X) by the scale exponent: Y = floor(X / 10^scale_exp) #! @@ -176,12 +148,19 @@ begin # => [pad(16)] exec.write_claim_data_into_advice_map_by_key - # => [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY, pad(4)] + # => [PROOF_DATA_KEY, LEAF_DATA_KEY, pad(16)] + + mem_load.FAUCET_MINT_AMOUNT + # => [faucet_mint_amount, PROOF_DATA_KEY, LEAF_DATA_KEY, pad(16)] + + movdn.8 + # => [PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(16)] - # Call the Aggfaucet Claim procedure + # call the Aggfaucet Claim procedure call.agg_faucet::claim - # => [pad(16), pad(12)] + # => [pad(16), pad(9)] - dropw dropw dropw + # a call invocation consumes and returns 16 elements, but we had trailing padding + dropw dropw drop # => [pad(16)] end diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 9913177701..699deefdd6 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -8,15 +8,7 @@ use miden_protocol::account::AccountId; use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteStorage, - NoteTag, - NoteType, -}; +use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use crate::{EthAddressFormat, EthAmount, GlobalIndex, MetadataHash, claim_script}; @@ -170,43 +162,6 @@ impl SequentialCommit for LeafData { } } -/// Output note data for CLAIM note creation. -/// Contains note-specific data and can use Miden types. -#[derive(Clone)] -pub struct OutputNoteData { - /// P2ID note serial number (4 felts as Word) - pub output_p2id_serial_num: Word, - /// P2ID output note tag - pub output_note_tag: NoteTag, - /// Miden claim amount (scaled-down token amount as Felt) - pub miden_claim_amount: Felt, -} - -impl OutputNoteData { - /// Converts the output note data to a vector of field elements for note storage. - /// - /// Layout (8 felts = 2 words): - /// `[serial_num(4), tag(1), miden_claim_amount(1), padding(2)]` - pub fn to_elements(&self) -> Vec { - const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 8; - let mut elements = Vec::with_capacity(OUTPUT_NOTE_DATA_ELEMENT_COUNT); - - // P2ID note serial number (4 felts as Word) - elements.extend(self.output_p2id_serial_num); - - // Output note tag - elements.push(Felt::new(self.output_note_tag.as_u32() as u64)); - - // Miden claim amount - elements.push(self.miden_claim_amount); - - // Padding to keep 8 felts (2 words) for pipe_double_words_preimage_to_memory - elements.extend([Felt::ZERO; 2]); - - elements - } -} - /// Data for creating a CLAIM note. /// /// This struct groups the core data needed to create a CLAIM note that exactly @@ -217,21 +172,21 @@ pub struct ClaimNoteStorage { pub proof_data: ProofData, /// Leaf data containing network, address, amount, and metadata pub leaf_data: LeafData, - /// Output note data containing note-specific information - pub output_note_data: OutputNoteData, + /// Miden claim amount (scaled-down token amount as Felt) + pub miden_claim_amount: Felt, } impl TryFrom for NoteStorage { type Error = NoteError; fn try_from(storage: ClaimNoteStorage) -> Result { - // proof_data + leaf_data + empty_word + output_note_data - // 536 + 32 + 8 - let mut claim_storage = Vec::with_capacity(576); + // proof_data + leaf_data + miden_claim_amount + // 536 + 32 + 1 + let mut claim_storage = Vec::with_capacity(569); claim_storage.extend(storage.proof_data.to_elements()); claim_storage.extend(storage.leaf_data.to_elements()); - claim_storage.extend(storage.output_note_data.to_elements()); + claim_storage.push(storage.miden_claim_amount); NoteStorage::new(claim_storage) } diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 91ed3338a1..21070e557b 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -34,15 +34,7 @@ pub mod update_ger_note; pub mod utils; pub use b2agg_note::B2AggNote; -pub use claim_note::{ - ClaimNoteStorage, - ExitRoot, - LeafData, - OutputNoteData, - ProofData, - SmtNode, - create_claim_note, -}; +pub use claim_note::{ClaimNoteStorage, ExitRoot, LeafData, ProofData, SmtNode, create_claim_note}; pub use config_note::ConfigAggBridgeNote; pub use eth_types::{ EthAddressFormat, diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 8b941d68fa..1d579d3218 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -8,7 +8,6 @@ use miden_agglayer::claim_note::Keccak256Output; use miden_agglayer::{ ClaimNoteStorage, ExitRoot, - OutputNoteData, SmtNode, UpdateGerNote, agglayer_library, @@ -18,8 +17,9 @@ use miden_agglayer::{ }; use miden_protocol::account::Account; use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; -use miden_protocol::note::{NoteTag, NoteType}; +use miden_protocol::note::NoteType; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use miden_protocol::transaction::OutputNote; use miden_protocol::{Felt, FieldElement}; @@ -199,8 +199,8 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // CREATE CLAIM NOTE // -------------------------------------------------------------------------------------------- - // Generate a serial number for the P2ID note - let serial_num = builder.rng_mut().draw_word(); + // The P2ID serial number is derived from the PROOF_DATA_KEY (RPO hash of proof data) + let serial_num = proof_data.to_commitment(); // Calculate the scaled-down Miden amount using the faucet's scale factor let miden_claim_amount = leaf_data @@ -208,14 +208,12 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a .scale_to_token_amount(scale as u32) .expect("amount should scale successfully"); - let output_note_data = OutputNoteData { - output_p2id_serial_num: serial_num, - output_note_tag: NoteTag::with_account_target(destination_account_id), + let claim_inputs = ClaimNoteStorage { + proof_data, + leaf_data, miden_claim_amount, }; - let claim_inputs = ClaimNoteStorage { proof_data, leaf_data, output_note_data }; - let claim_note = create_claim_note( claim_inputs, agglayer_faucet.id(),