From 7899c2397bfcd86c318bb260b06835d2bb8cf095 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 27 Jan 2026 19:35:50 +0000 Subject: [PATCH 01/14] feat: add `getLeafValue` procedure (#2262) Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- .../miden-agglayer/asm/bridge/bridge_in.masm | 2 +- .../miden-agglayer/asm/bridge/bridge_out.masm | 1 - .../asm/bridge/crypto_utils.masm | 55 ++++--- .../tests/agglayer/asset_conversion.rs | 34 +--- .../tests/agglayer/crypto_utils.rs | 146 ++++++++++++++++++ crates/miden-testing/tests/agglayer/mod.rs | 2 + .../tests/agglayer/test_utils.rs | 35 +++++ 7 files changed, 223 insertions(+), 52 deletions(-) create mode 100644 crates/miden-testing/tests/agglayer/crypto_utils.rs create mode 100644 crates/miden-testing/tests/agglayer/test_utils.rs diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index 1862fc5e35..65996e7608 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -33,7 +33,7 @@ end #! Invocation: call pub proc check_claim_proof exec.get_rollup_exit_root - # => [GER_ROOT[8], CLAIM_NOTE_RPO_COMMITMENT] + # => [GER_ROOT[8], PROOF_DATA_KEY, LEAF_DATA_KEY] # Check CLAIM note proof data against current GER exec.crypto_utils::verify_claim_proof diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index 449955ce6c..5aa9b5d6ca 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -157,4 +157,3 @@ pub proc bridge_out exec.create_burn_note # => [] end - diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 829a90674d..60a06773c3 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -1,32 +1,43 @@ use miden::core::crypto::hashes::keccak256 +use miden::core::mem -#! Given the leaf data returns the leaf value. +const LEAF_DATA_BYTES = 113 +const LEAF_DATA_NUM_WORDS = 8 +const LEAF_DATA_START_PTR = 0 + +#! Given the leaf data key returns the leaf value. #! -#! Inputs: [leaf_type, origin_network, ORIGIN_ADDRESS, destination_network, DESTINATION_ADDRESS, amount, METADATA_HASH] +#! Inputs: +#! Operand stack: [LEAF_DATA_KEY] +#! Advice map: { +#! LEAF_DATA_KEY => [ +#! 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) +#! ], +#! } #! Outputs: [LEAF_VALUE] #! -#! Where: -#! - leaf_type is the leaf type: [0] transfer Ether / ERC20 tokens, [1] message. -#! - origin_network is the origin network identifier. -#! - ORIGIN_ADDRESS is the origin token address (5 elements) -#! - destination_network is the destination network identifier. -#! - DESTINATION_ADDRESS is the destination address (5 elements). -#! - amount is the amount: [0] Amount of tokens/ether, [1] Amount of ether. -#! - METADATA_HASH is the hash of the metadata (8 elements). -#! - LEAF_VALUE is the computed leaf value (8 elements). -#! -#! This function computes the keccak256 hash of the abi.encodePacked data. -#! #! Invocation: exec pub proc get_leaf_value - # TODO: implement getLeafValue() - # https://github.com/agglayer/agglayer-contracts/blob/e468f9b0967334403069aa650d9f1164b1731ebb/contracts/v2/lib/DepositContractV2.sol#L22 + adv.push_mapval + # => [LEAF_DATA_KEY] - # stubbed out: - push.1.1.1.1 - push.1.1.1.1 - - # exec.keccak256::hash_bytes + push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS + exec.mem::pipe_preimage_to_memory drop + # => [] + + push.LEAF_DATA_BYTES push.LEAF_DATA_START_PTR + # => [start_ptr, byte_len] + + exec.keccak256::hash_bytes + # => [LEAF_VALUE[8]] + + # truncate stack + swapdw dropw dropw # => [LEAF_VALUE[8]] end @@ -36,7 +47,7 @@ end #! and that the leaf has not been previously claimed. #! #! Inputs: -#! Operand stack: [GER_ROOT[8], CLAIM_PROOF_RPO_COMMITMENT, pad(12)] +#! Operand stack: [GER_ROOT[8], PROOF_DATA_KEY, LEAF_DATA_KEY, pad(12)] #! Advice map: { #! PROOF_DATA_KEY => [ #! smtProofLocalExitRoot[256], // SMT proof for local exit root (256 felts, bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) diff --git a/crates/miden-testing/tests/agglayer/asset_conversion.rs b/crates/miden-testing/tests/agglayer/asset_conversion.rs index 6cec09d255..c37b1c206b 100644 --- a/crates/miden-testing/tests/agglayer/asset_conversion.rs +++ b/crates/miden-testing/tests/agglayer/asset_conversion.rs @@ -5,12 +5,12 @@ 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, FastProcessor}; -use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; +use miden_processor::fast::ExecutionOutput; use miden_protocol::Felt; -use miden_protocol::transaction::TransactionKernel; use primitive_types::U256; +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"); @@ -26,28 +26,6 @@ fn stack_to_u256(exec_output: &ExecutionOutput) -> U256 { felts_to_u256(felts) } -/// Execute a program with default host -async fn execute_program_with_default_host( - program: Program, -) -> Result { - let mut host = DefaultHost::default(); - - let test_lib = TransactionKernel::library(); - host.load_library(test_lib.mast_forest()).unwrap(); - - let std_lib = CoreLibrary::default(); - host.load_library(std_lib.mast_forest()).unwrap(); - - let asset_conversion_lib = agglayer_library(); - host.load_library(asset_conversion_lib.mast_forest()).unwrap(); - - let stack_inputs = StackInputs::new(vec![]).unwrap(); - let advice_inputs = AdviceInputs::default(); - - let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); - processor.execute(&program, &mut host).await -} - /// Helper function to test convert_felt_to_u256_scaled with given parameters async fn test_convert_to_u256_helper( miden_amount: Felt, @@ -79,7 +57,7 @@ async fn test_convert_to_u256_helper( .assemble_program(&script_code) .unwrap(); - let exec_output = execute_program_with_default_host(program).await?; + 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] = [ @@ -156,7 +134,7 @@ async fn test_convert_to_u256_scaled_eth() -> anyhow::Result<()> { .assemble_program(&script_code) .unwrap(); - let exec_output = execute_program_with_default_host(program).await?; + let exec_output = execute_program_with_default_host(program, None).await?; let expected_result = U256::from_dec_str("100000000000000000000").unwrap(); let actual_result = stack_to_u256(&exec_output); @@ -199,7 +177,7 @@ async fn test_convert_to_u256_scaled_large_amount() -> anyhow::Result<()> { .assemble_program(&script_code) .unwrap(); - let exec_output = execute_program_with_default_host(program).await?; + 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); diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs new file mode 100644 index 0000000000..4899b76490 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -0,0 +1,146 @@ +extern crate alloc; + +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_agglayer::agglayer_library; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_core_lib::handlers::keccak256::KeccakPreimage; +use miden_crypto::FieldElement; +use miden_processor::AdviceInputs; +use miden_protocol::{Felt, Hasher, Word}; + +use super::test_utils::execute_program_with_default_host; + +/// Convert bytes to field elements (u32 words packed into felts) +fn bytes_to_felts(data: &[u8]) -> Vec { + let mut felts = Vec::new(); + + // Pad data to multiple of 4 bytes + let mut padded_data = data.to_vec(); + while !padded_data.len().is_multiple_of(4) { + padded_data.push(0); + } + + // Convert to u32 words in little-endian format + for chunk in padded_data.chunks(4) { + let u32_value = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + felts.push(Felt::new(u32_value as u64)); + } + + // pad to next multiple of 4 felts + while felts.len() % 4 != 0 { + felts.push(Felt::ZERO); + } + + felts +} + +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]; + + 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); + } + + let mut s = String::from("0x"); + for b in out { + s.push_str(&format!("{:02x}", b)); + } + s +} + +// 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(); + } + 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 input_felts = bytes_to_felts(&input_u8); + assert_eq!(input_felts.len(), 32); + + // 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(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 2a6d344c67..65269c8c42 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,4 +1,6 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; +mod crypto_utils; mod solidity_miden_address_conversion; +pub mod test_utils; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs new file mode 100644 index 0000000000..21739e39e2 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -0,0 +1,35 @@ +extern crate alloc; + +use miden_agglayer::agglayer_library; +use miden_core_lib::CoreLibrary; +use miden_processor::fast::{ExecutionOutput, FastProcessor}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; +use miden_protocol::transaction::TransactionKernel; + +/// Execute a program with default host and optional advice inputs +pub async fn execute_program_with_default_host( + program: Program, + advice_inputs: Option, +) -> Result { + let mut host = DefaultHost::default(); + + let test_lib = TransactionKernel::library(); + host.load_library(test_lib.mast_forest()).unwrap(); + + 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)?; + } + + let agglayer_lib = agglayer_library(); + host.load_library(agglayer_lib.mast_forest()).unwrap(); + + let stack_inputs = StackInputs::new(vec![]).unwrap(); + let advice_inputs = advice_inputs.unwrap_or_default(); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); + processor.execute(&program, &mut host).await +} From 1a1513a50244ba90a1c4737b76dab5a0a6213a9a Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:26:29 -0500 Subject: [PATCH 02/14] `CLAIM` note followup: helper functions & refactoring (#2270) --- .../asm/bridge/agglayer_faucet.masm | 42 ++- crates/miden-agglayer/src/claim_note.rs | 263 ++++++++++++++++++ .../{eth_address.rs => eth_types/address.rs} | 0 crates/miden-agglayer/src/eth_types/amount.rs | 151 ++++++++++ crates/miden-agglayer/src/eth_types/mod.rs | 5 + crates/miden-agglayer/src/lib.rs | 31 ++- .../miden-testing/tests/agglayer/bridge_in.rs | 81 ++++-- .../tests/agglayer/test_utils.rs | 79 ++++++ 8 files changed, 607 insertions(+), 45 deletions(-) create mode 100644 crates/miden-agglayer/src/claim_note.rs rename crates/miden-agglayer/src/{eth_address.rs => eth_types/address.rs} (100%) create mode 100644 crates/miden-agglayer/src/eth_types/amount.rs create mode 100644 crates/miden-agglayer/src/eth_types/mod.rs diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index a0822cb4bf..e3a1178f45 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -1,11 +1,13 @@ use miden::agglayer::bridge_in use miden::agglayer::asset_conversion +use miden::agglayer::eth_address use miden::protocol::active_account use miden::protocol::active_note use miden::standards::faucets use miden::protocol::note use miden::protocol::tx use miden::core::mem +use miden::core::word # CONSTANTS @@ -34,6 +36,12 @@ const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 548 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 552 +const DESTINATION_ADDRESS_0 = 543 +const DESTINATION_ADDRESS_1 = 544 +const DESTINATION_ADDRESS_2 = 545 +const DESTINATION_ADDRESS_3 = 546 +const DESTINATION_ADDRESS_4 = 547 + # P2ID output note constants const P2ID_SCRIPT_ROOT = [13362761878458161062, 15090726097241769395, 444910447169617901, 3558201871398422326] const P2ID_NOTE_NUM_STORAGE_ITEMS = 2 @@ -128,6 +136,27 @@ proc batch_pipe_double_words exec.mem::pipe_double_words_preimage_to_memory drop end +#! Extracts the destination account ID as address[5] from memory. +#! +#! This procedure reads the destination address from the leaf data and converts it from +#! Ethereum address format to AccountId format (prefix, suffix). +#! +#! Inputs: [] +#! Outputs: [prefix, suffix] +#! +#! Invocation: exec +proc get_destination_account_id_data + mem_load.DESTINATION_ADDRESS_4 + mem_load.DESTINATION_ADDRESS_3 + mem_load.DESTINATION_ADDRESS_2 + mem_load.DESTINATION_ADDRESS_1 + mem_load.DESTINATION_ADDRESS_0 + # => [address[5]] + + exec.eth_address::to_account_id + # => [prefix, suffix] +end + #! Builds a P2ID output note for the claim recipient. #! #! This procedure expects the claim data to be already written to memory via batch_pipe_double_words. @@ -149,15 +178,16 @@ proc build_p2id_output_note push.P2ID_NOTE_NUM_STORAGE_ITEMS # => [note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - exec.get_destination_account_id - # => [account_id_prefix, account_id_suffix, note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - - mem_store.0 mem_store.1 - # => [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 + # => [] + exec.note::build_recipient # => [RECIPIENT] diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs new file mode 100644 index 0000000000..97f71be559 --- /dev/null +++ b/crates/miden-agglayer/src/claim_note.rs @@ -0,0 +1,263 @@ +use alloc::vec; +use alloc::vec::Vec; + +use miden_core::{Felt, Word}; +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, + NoteInputs, + NoteMetadata, + NoteRecipient, + NoteTag, + NoteType, +}; + +use crate::utils::bytes32_to_felts; +use crate::{EthAddressFormat, EthAmount, claim_script}; + +// CLAIM NOTE STRUCTURES +// ================================================================================================ + +/// SMT node representation (32-byte hash) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SmtNode([u8; 32]); + +impl SmtNode { + /// Creates a new SMT node 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 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) + } +} + +impl From<[u8; 32]> for SmtNode { + 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) + } +} + +impl From<[u8; 32]> for ExitRoot { + fn from(bytes: [u8; 32]) -> Self { + Self::new(bytes) + } +} + +/// Proof data for CLAIM note creation. +/// Contains SMT proofs and root hashes using typed representations. +pub struct ProofData { + /// SMT proof for local exit root (32 SMT nodes) + 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], + /// Mainnet exit root hash + pub mainnet_exit_root: ExitRoot, + /// Rollup exit root hash + pub rollup_exit_root: ExitRoot, +} + +impl SequentialCommit for ProofData { + type Commitment = Word; + + fn to_elements(&self) -> Vec { + const PROOF_DATA_ELEMENT_COUNT: usize = 536; // 32*8 + 32*8 + 8 + 8 + 8 (proofs + global_index + 2 exit roots) + let mut elements = Vec::with_capacity(PROOF_DATA_ELEMENT_COUNT); + + // 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); + } + + for node in self.smt_proof_rollup_exit_root.iter() { + let node_felts = node.to_elements(); + elements.extend(node_felts); + } + + // Global index (uint256 as 8 u32 felts) + elements.extend(self.global_index.iter().map(|&v| Felt::new(v as u64))); + + // 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); + + elements + } +} + +/// Leaf data for CLAIM note creation. +/// Contains network, address, amount, and metadata using typed representations. +pub struct LeafData { + /// Origin network identifier (uint32) + pub origin_network: u32, + /// Origin token address + pub origin_token_address: EthAddressFormat, + /// Destination network identifier (uint32) + pub destination_network: u32, + /// Destination address + pub destination_address: EthAddressFormat, + /// Amount of tokens (uint256) + pub amount: EthAmount, + /// ABI encoded metadata (fixed size of 8 u32 values) + pub metadata: [u32; 8], +} + +impl SequentialCommit for LeafData { + type Commitment = Word; + + fn to_elements(&self) -> Vec { + const LEAF_DATA_ELEMENT_COUNT: usize = 28; // 1 + 5 + 1 + 5 + 8 + 8 (networks + addresses + amount + metadata) + let mut elements = Vec::with_capacity(LEAF_DATA_ELEMENT_COUNT); + + // Origin network + elements.push(Felt::new(self.origin_network as u64)); + + // 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 address (5 u32 felts) + elements.extend(self.destination_address.to_elements()); + + // 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))); + + elements + } +} + +/// Output note data for CLAIM note creation. +/// Contains note-specific data and can use Miden types. +/// TODO: Remove all but target_faucet_account_id +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, +} + +impl OutputNoteData { + /// Converts the output note data to a vector of field elements for note inputs + pub fn to_elements(&self) -> Vec { + const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 7; // 4 + 2 + 1 (serial_num + account_id + tag) + 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)); + + elements + } +} + +/// Inputs for creating a CLAIM note. +/// +/// This struct groups the core data needed to create a CLAIM note that exactly +/// matches the agglayer claimAsset function signature. +pub struct ClaimNoteInputs { + /// Proof data containing SMT proofs and root hashes + 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, +} + +impl TryFrom for NoteInputs { + type Error = NoteError; + + fn try_from(inputs: ClaimNoteInputs) -> Result { + // proof_data + leaf_data + empty_word + output_note_data + // 536 + 28 + 4 + 7 + let mut claim_inputs = Vec::with_capacity(574); + + claim_inputs.extend(inputs.proof_data.to_elements()); + claim_inputs.extend(inputs.leaf_data.to_elements()); + claim_inputs.extend(Word::empty()); + claim_inputs.extend(inputs.output_note_data.to_elements()); + + NoteInputs::new(claim_inputs) + } +} + +// CLAIM NOTE CREATION +// ================================================================================================ + +/// Generates a CLAIM note - a note that instructs an agglayer faucet to validate and mint assets. +/// +/// # Parameters +/// - `inputs`: The core inputs for creating the CLAIM note +/// - `sender_account_id`: The account ID of the CLAIM note creator +/// - `rng`: Random number generator for creating the CLAIM note serial number +/// +/// # Errors +/// Returns an error if note creation fails. +pub fn create_claim_note( + inputs: ClaimNoteInputs, + sender_account_id: AccountId, + rng: &mut R, +) -> Result { + let note_inputs = NoteInputs::try_from(inputs)?; + + // TODO: Make CLAIM note a Network Note once NoteAttachment PR lands + let tag = NoteTag::new(0); + + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, tag); + + let recipient = NoteRecipient::new(rng.draw_word(), claim_script(), note_inputs); + let assets = NoteAssets::new(vec![])?; + + Ok(Note::new(assets, metadata, recipient)) +} diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_types/address.rs similarity index 100% rename from crates/miden-agglayer/src/eth_address.rs rename to crates/miden-agglayer/src/eth_types/address.rs diff --git a/crates/miden-agglayer/src/eth_types/amount.rs b/crates/miden-agglayer/src/eth_types/amount.rs new file mode 100644 index 0000000000..6ac10d1adb --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/amount.rs @@ -0,0 +1,151 @@ +use core::fmt; + +use miden_core::FieldElement; +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 +// ================================================================================================ + +/// Represents an Ethereum uint256 amount as 8 u32 values. +/// +/// 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]); + +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]) + } + } + + /// Converts the amount to a vector of field elements for note inputs. + /// + /// 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 + } +} + +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(()) + } +} diff --git a/crates/miden-agglayer/src/eth_types/mod.rs b/crates/miden-agglayer/src/eth_types/mod.rs new file mode 100644 index 0000000000..c8184cbc8d --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/mod.rs @@ -0,0 +1,5 @@ +pub mod address; +pub mod amount; + +pub use address::EthAddressFormat; +pub use amount::{EthAmount, EthAmountError}; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 1895c471f6..63d241cad6 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -19,28 +19,26 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::TokenSymbol; -use miden_protocol::crypto::rand::FeltRng; -use miden_protocol::errors::NoteError; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteScript, - NoteStorage, - NoteTag, - NoteType, -}; +use miden_protocol::note::NoteScript; use miden_standards::account::auth::NoAuth; use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_utils_sync::LazyLock; +pub mod claim_note; pub mod errors; -pub mod eth_address; +pub mod eth_types; pub mod utils; -pub use eth_address::EthAddressFormat; -use utils::bytes32_to_felts; +pub use claim_note::{ + ClaimNoteInputs, + ExitRoot, + LeafData, + OutputNoteData, + ProofData, + SmtNode, + create_claim_note, +}; +pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError}; // AGGLAYER NOTE SCRIPTS // ================================================================================================ @@ -331,6 +329,7 @@ pub fn create_existing_agglayer_faucet( .build_existing() .expect("Agglayer faucet account should be valid") } +<<<<<<< HEAD // AGGLAYER NOTE CREATION HELPERS // ================================================================================================ @@ -607,3 +606,5 @@ pub fn claim_note_test_inputs( metadata, ) } +======= +>>>>>>> 221ac03f (`CLAIM` note followup: helper functions & refactoring (#2270)) diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index c87bd0d77c..cda09ed148 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -2,9 +2,14 @@ extern crate alloc; use core::slice; +use miden_agglayer::claim_note::{ExitRoot, SmtNode}; use miden_agglayer::{ - ClaimNoteParams, - claim_note_test_inputs, + ClaimNoteInputs, + EthAddressFormat, + EthAmount, + LeafData, + OutputNoteData, + ProofData, create_claim_note, create_existing_agglayer_faucet, create_existing_bridge_account, @@ -28,6 +33,8 @@ 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. #[tokio::test] async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { @@ -69,9 +76,9 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- // Define amount values for the test - let amount_felt = Felt::new(100); + let claim_amount = 100u32; - // Create CLAIM note using the helper function with new agglayer claimAsset inputs + // Create CLAIM note using the new test inputs function let ( smt_proof_local_exit_root, smt_proof_rollup_exit_root, @@ -81,42 +88,66 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { origin_network, origin_token_address, destination_network, - destination_address, - amount_u256, metadata, - ) = claim_note_test_inputs(amount_felt, user_account.id()); + ) = 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(); - let claim_params = ClaimNoteParams { - smt_proof_local_exit_root, - smt_proof_rollup_exit_root, + // Convert amount to EthAmount for the LeafData + let amount_eth = EthAmount::from_u32(claim_amount); + + // 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, - mainnet_exit_root: &mainnet_exit_root, - rollup_exit_root: &rollup_exit_root, + 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: &origin_token_address, + origin_token_address: EthAddressFormat::new(origin_token_address), destination_network, - destination_address: &destination_address, - amount: amount_u256, + destination_address: EthAddressFormat::new(destination_address), + amount: amount_eth, metadata, - claim_note_creator_account_id: user_account.id(), - agglayer_faucet_account_id: agglayer_faucet.id(), + }; + + 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()), - p2id_serial_number: serial_num, - destination_account_id: user_account.id(), - rng: builder.rng_mut(), }; + let claim_inputs = ClaimNoteInputs { 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_inputs = NoteStorage::new(p2id_inputs)?; let p2id_recipient = NoteRecipient::new(serial_num, p2id_script.clone(), note_inputs); - let claim_note = create_claim_note(claim_params)?; - // Add the claim note to the builder before building the mock chain builder.add_output_note(OutputNote::Full(claim_note.clone())); @@ -127,6 +158,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // 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( @@ -141,7 +173,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let tx_context = mock_chain .build_tx_context(agglayer_faucet.id(), &[], &[claim_note])? - .add_note_script(p2id_script) + .add_note_script(p2id_script.clone()) .foreign_accounts(vec![foreign_account_inputs]) .build()?; @@ -155,7 +187,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { 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(), amount_felt.into())?; + let expected_asset = FungibleAsset::new(agglayer_faucet.id(), claim_amount.into())?; // Verify note metadata properties assert_eq!(output_note.metadata().sender(), agglayer_faucet.id()); @@ -171,6 +203,7 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // 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 diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 21739e39e2..8e31578c9d 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -1,5 +1,8 @@ extern crate alloc; +use alloc::vec; +use alloc::vec::Vec; + use miden_agglayer::agglayer_library; use miden_core_lib::CoreLibrary; use miden_processor::fast::{ExecutionOutput, FastProcessor}; @@ -33,3 +36,79 @@ 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 } + +// 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: [u32; 8] +/// - mainnet_exit_root: [u8; 32] +/// - rollup_exit_root: [u8; 32] +/// - origin_network: u32 +/// - origin_token_address: [u8; 20] +/// - destination_network: u32 +/// - metadata: [u32; 8] +pub type ClaimNoteTestInputs = ( + Vec<[u8; 32]>, + Vec<[u8; 32]>, + [u32; 8], + [u8; 32], + [u8; 32], + u32, + [u8; 20], + u32, + [u32; 8], +); + +/// 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]; + let global_index = [12345u32, 0, 0, 0, 0, 0, 0, 0]; + + 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, + ]; + + 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: [u32; 8] = [0; 8]; + + ( + 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, + ) +} From ebf6b30cd14952b2340b1afe4197f6770bed773d Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 23 Jan 2026 08:44:09 +0000 Subject: [PATCH 03/14] Merge branch 'next' into agglayer --- .../asm/note_scripts/B2AGG.masm | 2 +- crates/miden-agglayer/src/claim_note.rs | 32 +- crates/miden-agglayer/src/eth_types/amount.rs | 2 +- crates/miden-agglayer/src/lib.rs | 281 +----------------- .../miden-testing/tests/agglayer/bridge_in.rs | 8 +- 5 files changed, 23 insertions(+), 302 deletions(-) diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index 79075a2a09..a62e213daa 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -36,7 +36,7 @@ const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly #! - destination_address_4: bytes 16-19 #! #! Panics if: -#! - The note does not contain exactly 6 inputs. +#! - The note does not contain exactly 6 storage items. #! - The note does not contain exactly 1 asset. #! begin diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 97f71be559..fb1087f5bd 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -9,9 +9,9 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, - NoteInputs, NoteMetadata, NoteRecipient, + NoteStorage, NoteTag, NoteType, }; @@ -183,7 +183,7 @@ pub struct OutputNoteData { } impl OutputNoteData { - /// Converts the output note data to a vector of field elements for note inputs + /// 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 = 7; // 4 + 2 + 1 (serial_num + account_id + tag) let mut elements = Vec::with_capacity(OUTPUT_NOTE_DATA_ELEMENT_COUNT); @@ -202,11 +202,11 @@ impl OutputNoteData { } } -/// Inputs for creating a CLAIM note. +/// Data for creating a CLAIM note. /// /// This struct groups the core data needed to create a CLAIM note that exactly /// matches the agglayer claimAsset function signature. -pub struct ClaimNoteInputs { +pub struct ClaimNoteStorage { /// Proof data containing SMT proofs and root hashes pub proof_data: ProofData, /// Leaf data containing network, address, amount, and metadata @@ -215,20 +215,20 @@ pub struct ClaimNoteInputs { pub output_note_data: OutputNoteData, } -impl TryFrom for NoteInputs { +impl TryFrom for NoteStorage { type Error = NoteError; - fn try_from(inputs: ClaimNoteInputs) -> Result { + fn try_from(storage: ClaimNoteStorage) -> Result { // proof_data + leaf_data + empty_word + output_note_data // 536 + 28 + 4 + 7 - let mut claim_inputs = Vec::with_capacity(574); + let mut claim_storage = Vec::with_capacity(574); - claim_inputs.extend(inputs.proof_data.to_elements()); - claim_inputs.extend(inputs.leaf_data.to_elements()); - claim_inputs.extend(Word::empty()); - claim_inputs.extend(inputs.output_note_data.to_elements()); + claim_storage.extend(storage.proof_data.to_elements()); + claim_storage.extend(storage.leaf_data.to_elements()); + claim_storage.extend(Word::empty()); + claim_storage.extend(storage.output_note_data.to_elements()); - NoteInputs::new(claim_inputs) + NoteStorage::new(claim_storage) } } @@ -238,25 +238,25 @@ impl TryFrom for NoteInputs { /// Generates a CLAIM note - a note that instructs an agglayer faucet to validate and mint assets. /// /// # Parameters -/// - `inputs`: The core inputs for creating the CLAIM note +/// - `storage`: The core storage for creating the CLAIM note /// - `sender_account_id`: The account ID of the CLAIM note creator /// - `rng`: Random number generator for creating the CLAIM note serial number /// /// # Errors /// Returns an error if note creation fails. pub fn create_claim_note( - inputs: ClaimNoteInputs, + storage: ClaimNoteStorage, sender_account_id: AccountId, rng: &mut R, ) -> Result { - let note_inputs = NoteInputs::try_from(inputs)?; + let note_storage = NoteStorage::try_from(storage)?; // TODO: Make CLAIM note a Network Note once NoteAttachment PR lands let tag = NoteTag::new(0); let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, tag); - let recipient = NoteRecipient::new(rng.draw_word(), claim_script(), note_inputs); + let recipient = NoteRecipient::new(rng.draw_word(), claim_script(), note_storage); let assets = NoteAssets::new(vec![])?; Ok(Note::new(assets, metadata, recipient)) diff --git a/crates/miden-agglayer/src/eth_types/amount.rs b/crates/miden-agglayer/src/eth_types/amount.rs index 6ac10d1adb..dc0b9948cd 100644 --- a/crates/miden-agglayer/src/eth_types/amount.rs +++ b/crates/miden-agglayer/src/eth_types/amount.rs @@ -103,7 +103,7 @@ impl EthAmount { } } - /// Converts the amount to a vector of field elements for note inputs. + /// 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] { diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 63d241cad6..4860295f92 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -30,7 +30,7 @@ pub mod eth_types; pub mod utils; pub use claim_note::{ - ClaimNoteInputs, + ClaimNoteStorage, ExitRoot, LeafData, OutputNoteData, @@ -329,282 +329,3 @@ pub fn create_existing_agglayer_faucet( .build_existing() .expect("Agglayer faucet account should be valid") } -<<<<<<< HEAD - -// AGGLAYER NOTE CREATION HELPERS -// ================================================================================================ - -/// Parameters for creating a CLAIM note. -/// -/// This struct groups all the parameters needed to create a CLAIM note that exactly -/// matches the agglayer claimAsset function signature. -pub struct ClaimNoteParams<'a, R: FeltRng> { - /// AGGLAYER claimAsset function parameters - /// SMT proof for local exit root (bytes32\[_DEPOSIT_CONTRACT_TREE_DEPTH\]) - pub smt_proof_local_exit_root: Vec, - /// SMT proof for rollup exit root (bytes32\[_DEPOSIT_CONTRACT_TREE_DEPTH\]) - pub smt_proof_rollup_exit_root: Vec, - /// Global index (uint256 as 8 u32 felts) - pub global_index: [Felt; 8], - /// Mainnet exit root hash (bytes32 as 32-byte array) - pub mainnet_exit_root: &'a [u8; 32], - /// Rollup exit root hash (bytes32 as 32-byte array) - pub rollup_exit_root: &'a [u8; 32], - /// Origin network identifier (uint32) - pub origin_network: Felt, - /// Origin token address (address as 20-byte array) - pub origin_token_address: &'a [u8; 20], - /// Destination network identifier (uint32) - pub destination_network: Felt, - /// Destination address (address as 20-byte array) - pub destination_address: &'a [u8; 20], - /// Amount of tokens (uint256 as 8 u32 felts) - pub amount: [Felt; 8], - /// ABI encoded metadata (fixed size of 8 felts) - pub metadata: [Felt; 8], - /// CLAIM note required parameters - /// CLAIM note sender account id - pub claim_note_creator_account_id: AccountId, - /// Agglayer faucet AccountId - pub agglayer_faucet_account_id: AccountId, - /// Output P2ID note tag - pub output_note_tag: NoteTag, - /// P2ID note serial number (4 felts as Word) - pub p2id_serial_number: Word, - /// TODO: remove and use destination_address: [u8; 20] - pub destination_account_id: AccountId, - /// RNG for creating CLAIM note serial number - pub rng: &'a mut R, -} - -/// Generates a CLAIM note - a note that instructs an agglayer faucet to validate and mint assets. -/// -/// # Parameters -/// - `params`: The parameters for creating the CLAIM note (including RNG) -/// -/// # Errors -/// Returns an error if note creation fails. -pub fn create_claim_note(params: ClaimNoteParams<'_, R>) -> Result { - // Validate SMT proof lengths - each should be 256 felts (32 bytes32 values * 8 u32 per bytes32) - if params.smt_proof_local_exit_root.len() != 256 { - return Err(NoteError::other(alloc::format!( - "SMT proof local exit root must be exactly 256 felts, got {}", - params.smt_proof_local_exit_root.len() - ))); - } - if params.smt_proof_rollup_exit_root.len() != 256 { - return Err(NoteError::other(alloc::format!( - "SMT proof rollup exit root must be exactly 256 felts, got {}", - params.smt_proof_rollup_exit_root.len() - ))); - } - // Create claim inputs matching exactly the agglayer claimAsset function parameters - let mut claim_storage_items = vec![]; - - // 1) PROOF DATA - // smtProofLocalExitRoot (256 felts) - first SMT proof parameter - claim_storage_items.extend(params.smt_proof_local_exit_root); - // smtProofRollupExitRoot (256 felts) - second SMT proof parameter - claim_storage_items.extend(params.smt_proof_rollup_exit_root); - - // globalIndex (uint256 as 8 u32 felts) - claim_storage_items.extend(params.global_index); - - // mainnetExitRoot (bytes32 as 8 u32 felts) - let mainnet_exit_root_felts = bytes32_to_felts(params.mainnet_exit_root); - claim_storage_items.extend(mainnet_exit_root_felts); - - // rollupExitRoot (bytes32 as 8 u32 felts) - let rollup_exit_root_felts = bytes32_to_felts(params.rollup_exit_root); - claim_storage_items.extend(rollup_exit_root_felts); - - // 2) LEAF DATA - // originNetwork (uint32 as Felt) - claim_storage_items.push(params.origin_network); - - // originTokenAddress (address as 5 u32 felts) - let origin_token_address_felts = - EthAddressFormat::new(*params.origin_token_address).to_elements().to_vec(); - claim_storage_items.extend(origin_token_address_felts); - - // destinationNetwork (uint32 as Felt) - claim_storage_items.push(params.destination_network); - - // destinationAddress (address as 5 u32 felts) - // Use AccountId prefix and suffix directly to get [suffix, prefix, 0, 0, 0] - // TODO: refactor to use destination_address: [u8; 20] instead once conversion function - // exists [u8; 20] -> [address as 5 Felts] - let destination_address_felts = vec![ - params.destination_account_id.prefix().as_felt(), - params.destination_account_id.suffix(), - Felt::new(0), - Felt::new(0), - Felt::new(0), - ]; - claim_storage_items.extend(destination_address_felts); - - // amount (uint256 as 8 u32 felts) - claim_storage_items.extend(params.amount); - - // metadata (fixed size of 8 felts) - claim_storage_items.extend(params.metadata); - - let padding = vec![Felt::ZERO; 4]; - claim_storage_items.extend(padding); - - // 3) CLAIM NOTE DATA - // TODO: deterministically compute serial number of p2id hash(GER, leaf index) - // output_p2id_serial_num (4 felts as Word) - claim_storage_items.extend(params.p2id_serial_number); - - // agglayer_faucet_account_id (2 felts: prefix and suffix) - claim_storage_items.push(params.agglayer_faucet_account_id.prefix().as_felt()); - claim_storage_items.push(params.agglayer_faucet_account_id.suffix()); - - // output note tag - claim_storage_items.push(params.output_note_tag.as_u32().into()); - - let inputs = NoteStorage::new(claim_storage_items)?; - - let tag = NoteTag::with_account_target(params.agglayer_faucet_account_id); - - let claim_script = claim_script(); - let serial_num = params.rng.draw_word(); - - let note_type = NoteType::Public; - - // Use a default sender since we don't have sender anymore - create from destination address - let metadata = NoteMetadata::new(params.claim_note_creator_account_id, note_type, tag); - let assets = NoteAssets::new(vec![])?; - let recipient = NoteRecipient::new(serial_num, claim_script, inputs); - - Ok(Note::new(assets, metadata, recipient)) -} - -// TESTING HELPERS -// ================================================================================================ - -#[cfg(any(feature = "testing", test))] -/// Type alias for the complex return type of claim_note_test_inputs. -/// -/// Contains: -/// - smt_proof_local_exit_root: `Vec` (256 felts) -/// - smt_proof_rollup_exit_root: `Vec` (256 felts) -/// - global_index: [Felt; 8] -/// - mainnet_exit_root: [u8; 32] -/// - rollup_exit_root: [u8; 32] -/// - origin_network: Felt -/// - origin_token_address: [u8; 20] -/// - destination_network: Felt -/// - destination_address: [u8; 20] -/// - amount: [Felt; 8] -/// - metadata: [Felt; 8] -pub type ClaimNoteTestInputs = ( - Vec, - Vec, - [Felt; 8], - [u8; 32], - [u8; 32], - Felt, - [u8; 20], - Felt, - [u8; 20], - [Felt; 8], - [Felt; 8], -); - -#[cfg(any(feature = "testing", test))] -/// Returns dummy test inputs for creating CLAIM notes. -/// -/// This is a convenience function for testing that provides realistic dummy data -/// for all the agglayer claimAsset function inputs. -/// -/// # Parameters -/// - `amount`: The amount as a single Felt for Miden operations -/// - `destination_account_id`: The destination account ID to convert to address bytes -/// -/// # Returns -/// A tuple containing: -/// - smt_proof_local_exit_root: `Vec` (256 felts) -/// - smt_proof_rollup_exit_root: `Vec` (256 felts) -/// - global_index: [Felt; 8] -/// - mainnet_exit_root: [u8; 32] -/// - rollup_exit_root: [u8; 32] -/// - origin_network: Felt -/// - origin_token_address: [u8; 20] -/// - destination_network: Felt -/// - destination_address: [u8; 20] -/// - amount: [Felt; 8] -/// - metadata: [Felt; 8] -pub fn claim_note_test_inputs( - amount: Felt, - destination_account_id: AccountId, -) -> ClaimNoteTestInputs { - // Create SMT proofs with 256 felts each (32 bytes32 values * 8 u32 per bytes32) - let smt_proof_local_exit_root = vec![Felt::new(0); 256]; - let smt_proof_rollup_exit_root = vec![Felt::new(0); 256]; - let global_index = [ - Felt::new(12345), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - ]; - - 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, - ]; - - let origin_network = Felt::new(1); - - 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 = Felt::new(2); - - // Convert AccountId to destination address bytes - let destination_address = - EthAddressFormat::from_account_id(destination_account_id).into_bytes(); - - // Convert amount Felt to u256 array for agglayer - let amount_u256 = [ - amount, - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - Felt::new(0), - ]; - let metadata: [Felt; 8] = [Felt::new(0); 8]; - - ( - 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, - destination_address, - amount_u256, - metadata, - ) -} -======= ->>>>>>> 221ac03f (`CLAIM` note followup: helper functions & refactoring (#2270)) diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index cda09ed148..ade0adce5d 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -4,7 +4,7 @@ use core::slice; use miden_agglayer::claim_note::{ExitRoot, SmtNode}; use miden_agglayer::{ - ClaimNoteInputs, + ClaimNoteStorage, EthAddressFormat, EthAmount, LeafData, @@ -138,15 +138,15 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { output_note_tag: NoteTag::with_account_target(user_account.id()), }; - let claim_inputs = ClaimNoteInputs { proof_data, leaf_data, output_note_data }; + 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_inputs = NoteStorage::new(p2id_inputs)?; - let p2id_recipient = NoteRecipient::new(serial_num, p2id_script.clone(), note_inputs); + let note_storage = NoteStorage::new(p2id_inputs)?; + let p2id_recipient = NoteRecipient::new(serial_num, p2id_script.clone(), note_storage); // Add the claim note to the builder before building the mock chain builder.add_output_note(OutputNote::Full(claim_note.clone())); From 0b23645e7cbe50f757609b9eb4d018dc7eecf5cb Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Fri, 23 Jan 2026 12:46:47 +0300 Subject: [PATCH 04/14] feat: implement Keccak-based MMR frontier (#2245) * feat: impl fist frontier version, add (yet buggy) test * test: fix the test, fix the bug in algorithm * chore: update changelog * docs: add docs for the MMR frontier in the masm file * refactor: update the doc comments, slightly update code * refactor: update docs and comments, add overflow check, update test * test: add more leaves * test: add zero root test * chore: rename `root` -> `expected_root` in leaf assertion tests * chore: lint * chore: revert to using old dir structure * fix: update max leaves constants and comments * chore: regen errors file * fix: first assert valid u32, only then u32lte --------- Co-authored-by: Marti --- CHANGELOG.md | 1 + Cargo.lock | 1 + crates/miden-agglayer/Cargo.toml | 1 + .../asm/bridge/canonical_zeros.masm | 142 ++++++++ .../asm/bridge/mmr_frontier32_keccak.masm | 344 ++++++++++++++++++ crates/miden-agglayer/build.rs | 78 +++- crates/miden-agglayer/src/errors/agglayer.rs | 3 + crates/miden-testing/Cargo.toml | 1 + .../tests/agglayer/mmr_frontier.rs | 180 +++++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + 10 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 crates/miden-agglayer/asm/bridge/canonical_zeros.masm create mode 100644 crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm create mode 100644 crates/miden-testing/tests/agglayer/mmr_frontier.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 036725b614..d326de2626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - [BREAKING] Refactored storage slots to be accessed by names instead of indices ([#1987](https://github.com/0xMiden/miden-base/pull/1987), [#2025](https://github.com/0xMiden/miden-base/pull/2025), [#2149](https://github.com/0xMiden/miden-base/pull/2149), [#2150](https://github.com/0xMiden/miden-base/pull/2150), [#2153](https://github.com/0xMiden/miden-base/pull/2153), [#2154](https://github.com/0xMiden/miden-base/pull/2154), [#2160](https://github.com/0xMiden/miden-base/pull/2160), [#2161](https://github.com/0xMiden/miden-base/pull/2161), [#2170](https://github.com/0xMiden/miden-base/pull/2170)). - [BREAKING] Allowed account components to share identical account code procedures ([#2164](https://github.com/0xMiden/miden-base/pull/2164)). - Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). +- Add Keccak-based MMR frontier structure to the Agglayer library ([#2245](https://github.com/0xMiden/miden-base/pull/2245)). - Add `read_foreign_account_inputs()`, `read_vault_asset_witnesses()`, and `read_storage_map_witness()` for `TransactionInputs` ([#2246](https://github.com/0xMiden/miden-base/pull/2246)). - [BREAKING] Introduced `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249), [#2252](https://github.com/0xMiden/miden-base/pull/2252), [#2260](https://github.com/0xMiden/miden-base/pull/2260), [#2268](https://github.com/0xMiden/miden-base/pull/2268), [#2279](https://github.com/0xMiden/miden-base/pull/2279)). - Added `AccountSchemaCommitment` component to expose account storage schema commitments ([#2253](https://github.com/0xMiden/miden-base/pull/2253)). diff --git a/Cargo.lock b/Cargo.lock index 5d7cd1389c..76704df33f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1388,6 +1388,7 @@ dependencies = [ "miden-assembly", "miden-core", "miden-core-lib", + "miden-crypto", "miden-protocol", "miden-standards", "miden-utils-sync", diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml index 019379d7fb..7541b7ea8d 100644 --- a/crates/miden-agglayer/Cargo.toml +++ b/crates/miden-agglayer/Cargo.toml @@ -35,6 +35,7 @@ fs-err = { workspace = true } miden-assembly = { workspace = true } miden-core = { workspace = true } miden-core-lib = { workspace = true } +miden-crypto = { workspace = true } miden-protocol = { features = ["testing"], workspace = true } miden-standards = { workspace = true } regex = { version = "1.11" } diff --git a/crates/miden-agglayer/asm/bridge/canonical_zeros.masm b/crates/miden-agglayer/asm/bridge/canonical_zeros.masm new file mode 100644 index 0000000000..e693c4fa16 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/canonical_zeros.masm @@ -0,0 +1,142 @@ +# This file is generated by build.rs, do not modify + +# This file contains the canonical zeros for the Keccak hash function. +# Zero of height `n` (ZERO_N) is the root of the binary tree of height `n` with leaves equal zero. +# +# Since the Keccak hash is represented by eight u32 values, each constant consists of two Words. + +const ZERO_0_L = [0, 0, 0, 0] +const ZERO_0_R = [0, 0, 0, 0] + +const ZERO_1_L = [3042949783, 3846789184, 2990541491, 2447652395] +const ZERO_1_R = [2532382527, 1151697986, 3453220726, 3056087725] + +const ZERO_2_L = [806175122, 2661877378, 3993486975, 3704028736] +const ZERO_2_R = [1186125340, 4132056164, 2406448277, 1360642484] + +const ZERO_3_L = [2243606276, 2319049635, 2778422344, 3686444836] +const ZERO_3_R = [836748766, 3055947948, 1063027030, 2746866977] + +const ZERO_4_L = [1150525734, 2360852476, 3881358125, 3462706719] +const ZERO_4_R = [224004420, 1513564138, 4058651434, 3010037733] + +const ZERO_5_L = [768598281, 293668224, 2114802790, 2680951561] +const ZERO_5_R = [523052921, 3386889228, 1344794057, 3206459406] + +const ZERO_6_L = [1746508463, 578821813, 283579568, 4134788524] +const ZERO_6_R = [756088757, 1715252246, 1087590535, 3173153928] + +const ZERO_7_L = [2205136186, 3475749318, 613780937, 1818541875] +const ZERO_7_R = [40140559, 91932979, 4234379492, 1459738623] + +const ZERO_8_L = [2941712185, 3321779339, 1227307046, 4069577285] +const ZERO_8_R = [611590243, 2128798138, 2473269631, 1607231384] + +const ZERO_9_L = [3763621903, 1154705673, 1903710296, 1972812290] +const ZERO_9_R = [4216691121, 4275626407, 3113795592, 3855940302] + +const ZERO_10_L = [2781069751, 774786966, 4112065289, 2182953470] +const ZERO_10_R = [3567589455, 861991663, 1356863200, 2134826233] + +const ZERO_11_L = [2465787000, 4149924453, 2720076317, 1467765009] +const ZERO_11_R = [1838648827, 866654147, 167150306, 1228583416] + +const ZERO_12_L = [2631517602, 171349786, 79648606, 4164671431] +const ZERO_12_R = [270336915, 2195882716, 3960096235, 3469119540] + +const ZERO_13_L = [3152187846, 1895984889, 2047814617, 1944734805] +const ZERO_13_R = [3551827087, 82830058, 326416580, 3649232833] + +const ZERO_14_L = [3435063385, 3598841737, 2762164692, 1894305546] +const ZERO_14_R = [3658789242, 3755895333, 49531590, 3618465628] + +const ZERO_15_L = [3525744215, 708101859, 2574387782, 3790037114] +const ZERO_15_R = [3700193742, 843132861, 3055060558, 2681109466] + +const ZERO_16_L = [530120689, 2718529082, 3981742412, 4194160956] +const ZERO_16_R = [4065390056, 824943129, 4207046226, 266679079] + +const ZERO_17_L = [2062522595, 650244466, 598998238, 1099357850] +const ZERO_17_R = [1543068721, 3603315816, 3833704967, 3367359457] + +const ZERO_18_L = [2692314236, 1072797208, 2923625471, 4157324078] +const ZERO_18_R = [746357617, 2400147060, 3144187786, 181284186] + +const ZERO_19_L = [2691355510, 1491476508, 3986541574, 2665487122] +const ZERO_19_R = [1032730592, 1039549588, 4164965877, 3056102068] + +const ZERO_20_L = [3803705507, 1732703975, 3478010394, 1535003327] +const ZERO_20_R = [4242360534, 719184416, 3062253412, 1167482566] + +const ZERO_21_L = [3655320222, 899251086, 3853444828, 1001466509] +const ZERO_21_R = [4045815225, 971767692, 1168258541, 2290434548] + +const ZERO_22_L = [2011403911, 3698331664, 3934089079, 946955861] +const ZERO_22_R = [3411854989, 1866109879, 418371072, 3692469338] + +const ZERO_23_L = [1390808632, 3168994683, 4234662665, 2053609922] +const ZERO_23_R = [2805567324, 2651248336, 696388782, 1078982733] + +const ZERO_24_L = [4011431532, 565969590, 1910056709, 4220355468] +const ZERO_24_R = [1681176506, 4292988995, 276516087, 2502281165] + +const ZERO_25_L = [2371989742, 3318538162, 999806777, 2066155765] +const ZERO_25_R = [1956437264, 2768897524, 1475191156, 3378167562] + +const ZERO_26_L = [3498569445, 3649628337, 1786802573, 2038831148] +const ZERO_26_R = [1678762243, 2385297319, 4030198639, 74763704] + +const ZERO_27_L = [516194684, 3360338824, 2165369292, 1916245748] +const ZERO_27_R = [3748991331, 1513828739, 3418759627, 1431735427] + +const ZERO_28_L = [787185022, 1571753335, 2366459736, 3067898230] +const ZERO_28_R = [79972070, 2975955312, 3165837101, 3722718822] + +const ZERO_29_L = [581144193, 3146618532, 1244629930, 2215341298] +const ZERO_29_R = [2551087773, 3876094376, 1909551909, 246581816] + +const ZERO_30_L = [903308566, 578217418, 2128594844, 1787682571] +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 + +#! Inputs: [zeros_ptr] +#! Outputs: [] +pub proc load_zeros_to_memory + push.ZERO_0_L.ZERO_0_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_1_L.ZERO_1_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_2_L.ZERO_2_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_3_L.ZERO_3_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_4_L.ZERO_4_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_5_L.ZERO_5_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_6_L.ZERO_6_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_7_L.ZERO_7_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_8_L.ZERO_8_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_9_L.ZERO_9_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_10_L.ZERO_10_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_11_L.ZERO_11_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_12_L.ZERO_12_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_13_L.ZERO_13_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_14_L.ZERO_14_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_15_L.ZERO_15_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_16_L.ZERO_16_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_17_L.ZERO_17_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_18_L.ZERO_18_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_19_L.ZERO_19_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_20_L.ZERO_20_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_21_L.ZERO_21_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_22_L.ZERO_22_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_23_L.ZERO_23_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_24_L.ZERO_24_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_25_L.ZERO_25_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_26_L.ZERO_26_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_27_L.ZERO_27_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_28_L.ZERO_28_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_29_L.ZERO_29_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_30_L.ZERO_30_R exec.mem_store_double_word dropw dropw add.8 + push.ZERO_31_L.ZERO_31_R exec.mem_store_double_word dropw dropw add.8 + drop +end diff --git a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm new file mode 100644 index 0000000000..61d86cb99f --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm @@ -0,0 +1,344 @@ +use miden::core::crypto::hashes::keccak256 +use ::miden::agglayer::canonical_zeros::load_zeros_to_memory + +# An MMR Frontier is a data structure based on an MMR, which combines some features of an MMR and an +# SMT. +# +# # Basics & Terminology +# +# +# The main entity in this structure is a _frontier_: it is a set of roots of all individual trees in +# the MMR. Let's consider the tree below as an example. +# +# 7 +# / \ +# 3 6 10 +# / \ / \ / \ +# 1 2 4 5 8 9 11 +# +# The frontier will consist of nodes 7, 10, and 11, because they represent roots of each subtree and +# they are sufficient to compute the root of the entire MMR. If we add another node, the tree will +# become a full binary one and will look like so: +# +# 15 +# / \ +# / \ +# / \ +# 7 14 +# / \ / \ +# 3 6 10 13 +# / \ / \ / \ / \ +# 1 2 4 5 8 9 11 12 +# +# So in that case the frontier will consist of just one node 15. +# +# An MMR frontier consists of the current number of leaves in the range and the array containing the +# frontier. +# For the sake of simplicity, this array has a fixed length, equal to the maximum tree height. +# Indexes of 1's in the binary representation of the total leaves number show the indexes of the +# relevant frontier values in the frontier array for the current height. For example, if we have 10 +# leaves (1010 in binary representation), relevant frontier values will be stored at frontier[1] and +# frontier[3]. +# +# To compute the hash of two MMR nodes, a Keccak256 hash function is used. +# +# Each node in this MMR is represented by the Keccak256Digest. Notice that this hash is canonically +# represented on the stack by the 8 u32 values, or two words. So each node of the MMR will occupy +# two words on the stack, while being only a 256 bit value. +# +# Each state of the MMR frontier is represented by the root. This root is essentially equal to the +# root of the SMT which has the height equal to the maximum height of the current MMR (for this +# implementation this maximum height is set to 32), and the leaves equal to the MMR frontier leaves +# plus the "zero hash" leaves (Keccak256::hash(&[0u8; 32])) for all other ones. +# +# # Layout +# +# The memory layout of the MMR frontier looks like so: +# +# [num_leaves, 0, 0, 0, [FRONTIER_VALUE_DW]] +# +# Where: +# - num_leaves is the number of leaves in the MMR before adding the new leaf. +# - [FRONTIER_VALUE_DW] is an array containing the double words which represent the frontier MMR +# nodes. Notice that the index of a frontier value in this array represent its height in the tree. +# +# Zero hashes which are used during the root computation are stored in the local memory of the +# `append_and_update_frontier` procedure. + +# ERRORS +# ================================================================================================= + +const ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT = "number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)" + +# CONSTANTS +# ================================================================================================= + +# The maximum number of leaves which could be added to the MMR. +# +# If the height is 32, the leaves num will be equal to 4294967295 (2**32 - 1) +const MAX_LEAVES_NUM = 4294967295 +const MAX_LEAVES_MINUS_1 = 4294967294 + +# The total height of the full MMR tree, whose root represents the commitment to the current +# frontier. +const TREE_HEIGHT = 32 + +# The number of the stack elements which one node occupy. +const NODE_SIZE = 8 + +# The offset of the number of leaves in the current MMR state. +const NUM_LEAVES_OFFSET = 0 + +# The offset of the array of the frontier nodes of respective heights. +const FRONTIER_OFFSET = 4 # 32 double words, 256 felts in total + +# The offset of the first half of the current Keccak256 hash value in the local memory of the +# `append_and_update_frontier` procedure. +const CUR_HASH_LO_LOCAL = 0 + +# The offset of the second half of the current Keccak256 hash value in the local memory of the +# `append_and_update_frontier` procedure. +const CUR_HASH_HI_LOCAL = 4 + +# The offset of the canonical zeros stored in the local memory of the `append_and_update_frontier` +# procedure. +const CANONICAL_ZEROES_LOCAL = 8 + +# PUBLIC API +# ================================================================================================= + +#! Updates the existing frontier with the new leaf, returns a new leaf count and a new MMR root. +#! +#! The memory layout at the `mmr_frontier_ptr` is expected to be: +#! [num_leaves, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +#! Empty uninitialized memory is a valid state for the frontier in the case where there are no +#! leaves. +#! +#! The layout of the local memory of this `append_and_update_frontier` procedure looks like so: +#! [CUR_HASH_LO, CUR_HASH_HI, [[CANONICAL_ZERO_LO, CANONICAL_ZERO_HI]; 32]] +#! So the first 8 felt values is occupied by the current Keccak256 hash, and next 32 * 8 felt values +#! is occupied by the canonical zeros, 8 values each, 32 zeros total. +#! +#! Inputs: [NEW_LEAF_LO, NEW_LEAF_HI, mmr_frontier_ptr] +#! Outputs: [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] +#! +#! Where: +#! - [NEW_LEAF_LO, NEW_LEAF_HI] is the new leaf, represented as Keccak256 hash, which will be added +#! to the MMR. +#! - mmr_frontier_ptr is the pointer to the memory where the MMR Frontier structure is located. +#! - [NEW_ROOT_LO, NEW_ROOT_HI] is the new root of the MMR, represented as Keccak256 hash. +#! - new_leaf_count is the number of leaves in the MMR after the new leaf was added. +#! +#! Panics if: +#! - The number of leaves in the MMR has reached the maximum limit of 2^32. +@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 + # => [mmr_frontier_ptr] + + # get the current leaves number + dup add.NUM_LEAVES_OFFSET mem_load + # => [num_leaves, mmr_frontier_ptr] + + # make sure that the MMR is not full yet and we still can store the new leaf + # the MMR is full when the number of leaves is equal to 2^TREE_HEIGHT - 1 (as per the + # Solidity implementation), so the last call to this procedure will be when the number of + # leaves would be equal to 2^32 - 2. + # first assert that the number of leaves is a valid u32, else the u32lt assertion is undefined + u32assert.err=ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT + dup u32lte.MAX_LEAVES_MINUS_1 assert.err=ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT + # => [num_leaves, mmr_frontier_ptr] + + # get the memory pointer where the canonical zeros will be stored + locaddr.CANONICAL_ZEROES_LOCAL + # => [zeros_ptr, num_leaves, mmr_frontier_ptr] + + # load the canonical zeros into the memory + exec.load_zeros_to_memory + # => [num_leaves, mmr_frontier_ptr] + + # update the leaves number and store it into the memory + dup add.1 dup.2 add.NUM_LEAVES_OFFSET + # => [num_leaves_ptr, num_leaves+1, num_leaves, mmr_frontier_ptr] + + mem_store + # => [num_leaves, mmr_frontier_ptr] + + # iterate `TREE_HEIGHT` times to get the root of the tree + # + # iter_counter in that case will show the current tree height + push.0 push.1 + # => [loop_flag=1, iter_counter=0, num_leaves, mmr_frontier_ptr] + + while.true + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + + # get the pointer to the frontier node of the current height + # + # notice that the initial state of the frontier array is zeros + dup.2 add.FRONTIER_OFFSET dup.1 mul.NODE_SIZE add + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + + # determine whether the last `num_leaves` bit is 1 (is `num_leaves` odd) + dup.2 u32and.1 + # => [ + # is_odd, frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr + # ] + + if.true + # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] + # + # this height already had a subtree root stored in frontier[curr_tree_height], merge + # into parent. + exec.mem_load_double_word + # => [ + # FRONTIER[curr_tree_height]_LO, FRONTIER[curr_tree_height]_HI, curr_tree_height, + # num_leaves, mmr_frontier_ptr + # ] + + # 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 + swapdw + # => [ + # FRONTIER[curr_tree_height]_LO, FRONTIER[curr_tree_height]_HI, CUR_HASH_LO, + # CUR_HASH_HI, curr_tree_height, num_leaves, mmr_frontier_ptr + # ] + + # merge the frontier node of this height with the current hash to get the current hash + # of the next height (merge(frontier[h], cur)) + exec.keccak256::merge + # => [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 + # => [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 + # => [ + # CUR_HASH_LO, CUR_HASH_HI, frontier[curr_tree_height]_ptr, curr_tree_height, + # num_leaves, mmr_frontier_ptr + # ] + + # store the CUR_HASH to the frontier[curr_tree_height]_ptr + exec.mem_store_double_word movup.8 drop + # => [CUR_HASH_LO, CUR_HASH_HI, curr_tree_height, num_leaves, mmr_frontier_ptr] + + # get the pointer to the canonical zero node of the current height + locaddr.CANONICAL_ZEROES_LOCAL dup.9 mul.NODE_SIZE add + # => [ + # zeros[curr_tree_height], CUR_HASH_LO, CUR_HASH_HI, curr_tree_height, num_leaves, + # mmr_frontier_ptr + # ] + + # load the zero node to the stack + exec.mem_load_double_word swapdw + # => [ + # CUR_HASH_LO, CUR_HASH_HI, ZERO_H_LO, ZERO_H_HI, curr_tree_height, num_leaves, + # mmr_frontier_ptr + # ] + + # merge the current hash with the zero node of this height to get the current hash of + # the next height (merge(cur, zeroes[h])) + exec.keccak256::merge + # => [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 + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + end + # => [curr_tree_height, num_leaves, mmr_frontier_ptr] + + # update the current tree height + add.1 + # => [curr_tree_height+1, num_leaves, mmr_frontier_ptr] + + # update the `num_leaves` (shift it right by 1 bit) + swap u32shr.1 swap + # => [curr_tree_height+1, num_leaves>>1, mmr_frontier_ptr] + + # compute the cycle flag + dup neq.TREE_HEIGHT + # => [loop_flag, curr_tree_height+1, num_leaves>>1, mmr_frontier_ptr] + end + # => [curr_tree_height=TREE_HEIGHT, num_leaves=0, mmr_frontier_ptr] + + # clean the stack + drop drop + # => [mmr_frontier_ptr] + + # load the final number of leaves onto the stack + add.NUM_LEAVES_OFFSET mem_load + # => [new_leaf_count] + + # The current (final) hash represents the root of the whole tree. + # + # Notice that there is no need to update the frontier[tree_height] value, which in theory could + # represent the frontier in case the tree is full. The frontier nodes are used only for the + # computation of the next height hash, but if the tree is full, there is no next hash to + # 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 + # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] +end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Stores the canonical zeros from the advice map to the memory at the provided address. +#! +#! Inputs: [zeros_ptr] +#! Outputs: [] +proc store_canonical_zeros + # prepare the stack for the adv_pipe instruction + padw padw padw + # => [PAD, PAD, PAD, zeros_ptr] + + # TODO: use constant once constant usage will be implemented + repeat.32 + adv_pipe + # => [ZERO_I_L, ZERO_I_R, PAD, zeros_ptr+8] + end + # => [ZERO_31_L, ZERO_31_R, PAD, zeros_ptr+256] + + # clean the stack + dropw dropw dropw drop + # => [] +end + +#! Stores two words to the provided global memory address. +#! +#! Inputs: [WORD_1, WORD_2, ptr] +#! Outputs: [WORD_1, WORD_2, ptr] +pub proc mem_store_double_word + dup.8 mem_storew_be swapw + # => [WORD_2, WORD_1, ptr] + + dup.8 add.4 mem_storew_be swapw + # => [WORD_1, WORD_2, ptr] +end + +#! Loads two words from the provided global memory address. +#! +#! Inputs: [ptr] +#! Outputs: [WORD_1, WORD_2] +proc mem_load_double_word + padw dup.4 add.4 mem_loadw_be + # => [WORD_2, ptr] + + padw movup.8 mem_loadw_be + # => [WORD_1, WORD_2] +end diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index fbd2cd06e7..d57b3c3ed7 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -5,6 +5,7 @@ use fs_err as fs; use miden_assembly::diagnostics::{IntoDiagnostic, Result, WrapErr}; use miden_assembly::utils::Serializable; use miden_assembly::{Assembler, Library, Report}; +use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; use miden_protocol::transaction::TransactionKernel; // CONSTANTS @@ -38,6 +39,10 @@ fn main() -> Result<()> { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 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))?; + let dst = Path::new(&build_dir).to_path_buf(); shared::copy_directory(src, &dst, ASM_DIR)?; @@ -134,7 +139,7 @@ fn compile_note_scripts( // COMPILE ACCOUNT COMPONENTS (DEPRECATED) // ================================================================================================ -/// Compiles the bridge components in `source_dir` into MASL libraries and stores the compiled +/// Compiles the agglayer library 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 @@ -230,6 +235,77 @@ fn generate_error_constants(asm_source_dir: &Path) -> Result<()> { Ok(()) } +// CANONICAL ZEROS FILE GENERATION +// ================================================================================================ + +fn generate_canonical_zeros(target_dir: &Path) -> Result<()> { + if !BUILD_GENERATED_FILES_IN_SRC { + return Ok(()); + } + + const TREE_HEIGHT: u8 = 32; + + let mut zeros_by_height = Vec::with_capacity(TREE_HEIGHT as usize); + + // Push the zero of height 0 to the zeros vec. This is done separately because the zero of + // height 0 is just a plain zero array ([0u8; 32]), it doesn't require to perform any hashing. + zeros_by_height.push(Keccak256Digest::default()); + + // Compute the canonical zeros for each height from 1 to TREE_HEIGHT + // Zero of height `n` is computed as: `ZERO_N = Keccak256::merge(ZERO_{N-1}, ZERO_{N-1})` + for _ in 1..TREE_HEIGHT { + let current_height_zero = + Keccak256::merge(&[*zeros_by_height.last().unwrap(), *zeros_by_height.last().unwrap()]); + zeros_by_height.push(current_height_zero); + } + + // convert the keccak digest into the sequence of u32 values and create two word constants from + // them to represent the hash + let mut zero_constants = String::from( + "# This file is generated by build.rs, do not modify\n +# This file contains the canonical zeros for the Keccak hash function. +# Zero of height `n` (ZERO_N) is the root of the binary tree of height `n` with leaves equal zero. +# +# Since the Keccak hash is represented by eight u32 values, each constant consists of two Words.\n", + ); + + for (height, zero) in zeros_by_height.iter().enumerate() { + let zero_as_u32_vec = zero + .chunks(4) + .map(|chunk_u32| u32::from_le_bytes(chunk_u32.try_into().unwrap()).to_string()) + .rev() + .collect::>(); + + zero_constants.push_str(&format!( + "\nconst ZERO_{height}_L = [{}]\n", + zero_as_u32_vec[..4].join(", ") + )); + zero_constants + .push_str(&format!("const ZERO_{height}_R = [{}]\n", zero_as_u32_vec[4..].join(", "))); + } + + // remove once CANONICAL_ZEROS advice map is available + zero_constants.push_str( + " +use ::miden::agglayer::mmr_frontier32_keccak::mem_store_double_word + +#! Inputs: [zeros_ptr] +#! Outputs: [] +pub proc load_zeros_to_memory\n", + ); + + for zero_index in 0..32 { + zero_constants.push_str(&format!("\tpush.ZERO_{zero_index}_L.ZERO_{zero_index}_R exec.mem_store_double_word dropw dropw add.8\n")); + } + + zero_constants.push_str("\tdrop\nend\n"); + + // write the resulting masm content into the file + fs::write(target_dir.join("canonical_zeros.masm"), zero_constants).into_diagnostic()?; + + Ok(()) +} + /// This module should be kept in sync with the copy in miden-protocol's and miden-standards' /// build.rs. mod shared { diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 2de157dd1b..61f70e3f08 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -26,6 +26,9 @@ pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combine /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); +/// 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: "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-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index 23bea98751..7c5314be96 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -27,6 +27,7 @@ miden-tx-batch-prover = { features = ["testing"], workspace = true } # Miden dependencies miden-assembly = { workspace = true } miden-core-lib = { workspace = true } +miden-crypto = { workspace = true } miden-processor = { workspace = true } # External dependencies diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs new file mode 100644 index 0000000000..b4e800703c --- /dev/null +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -0,0 +1,180 @@ +use alloc::format; +use alloc::string::ToString; + +use miden_agglayer::agglayer_library; +use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; +use miden_protocol::Felt; +use miden_protocol::utils::sync::LazyLock; +use miden_standards::code_builder::CodeBuilder; +use miden_testing::TransactionContextBuilder; + +// KECCAK MMR FRONTIER +// ================================================================================================ + +static CANONICAL_ZEROS_32: LazyLock> = LazyLock::new(|| { + let mut zeros_by_height = Vec::with_capacity(32); + + // Push the zero of height 0 to the zeros vec. This is done separately because the zero of + // height 0 is just a plain zero array ([0u8; 32]), it doesn't require to perform any hashing. + zeros_by_height.push(Keccak256Digest::default()); + + // Compute the canonical zeros for each height from 1 to 32 + // Zero of height `n` is computed as: `ZERO_N = Keccak256::merge(ZERO_{N-1}, ZERO_{N-1})` + for _ in 1..32 { + let last_zero = zeros_by_height.last().expect("zeros vec should have at least one value"); + let current_height_zero = Keccak256::merge(&[*last_zero, *last_zero]); + zeros_by_height.push(current_height_zero); + } + + zeros_by_height +}); + +struct KeccakMmrFrontier32 { + num_leaves: u32, + frontier: [Keccak256Digest; TREE_HEIGHT], +} + +impl KeccakMmrFrontier32 { + pub fn new() -> Self { + Self { + num_leaves: 0, + frontier: [Keccak256Digest::default(); TREE_HEIGHT], + } + } + + pub fn append_and_update_frontier(&mut self, new_leaf: Keccak256Digest) -> Keccak256Digest { + let mut curr_hash = new_leaf; + let mut idx = self.num_leaves; + self.num_leaves += 1; + + for height in 0..TREE_HEIGHT { + if (idx & 1) == 0 { + // This height wasn't "occupied" yet: store cur as the subtree root at height h. + self.frontier[height] = curr_hash; + + // Pair it with the canonical zero subtree on the right at this height. + curr_hash = Keccak256::merge(&[curr_hash, CANONICAL_ZEROS_32[height]]); + } else { + // This height already had a subtree root stored in frontier[h], merge into parent. + curr_hash = Keccak256::merge(&[self.frontier[height], curr_hash]) + } + + idx >>= 1; + } + + // curr_hash at this point is equal to the root of the full tree + curr_hash + } +} + +// TESTS +// ================================================================================================ + +#[tokio::test] +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(); + + for round in 0..32 { + // construct the leaf from the hex representation of the round number + let leaf = Keccak256Digest::try_from(format!("{:#066x}", round).as_str()).unwrap(); + 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("end"); + + 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?; + + Ok(()) +} + +#[tokio::test] +async fn test_check_empty_mmr_root() -> anyhow::Result<()> { + let zero_leaf = Keccak256Digest::default(); + 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(); + + 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("end"); + + 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?; + + Ok(()) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Transforms the `[Keccak256Digest]` into two word strings: (`a, b, c, d`, `e, f, g, h`) +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::>(); + + (double_word[0..4].join(", "), double_word[4..8].join(", ")) +} + +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); + + format!( + r#" + # load the provided leaf onto the stack + push.[{leaf_hi}] + push.[{leaf_lo}] + + # 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}] + movdnw.3 + # => [EXPECTED_ROOT_LO, NEW_ROOT_LO, NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] + + assert_eqw.err="MMR root (LO) is incorrect" + # => [NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] + + assert_eqw.err="MMR root (HI) is incorrect" + # => [new_leaf_count] + + # assert the new number of leaves + push.{num_leaves} + assert_eq.err="new leaf count is incorrect" + "# + ) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 65269c8c42..44e687a15c 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -2,5 +2,6 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; mod crypto_utils; +mod mmr_frontier; mod solidity_miden_address_conversion; pub mod test_utils; From c596be2f41d5ab8232372019146ccce2e367719a Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 23 Jan 2026 11:15:02 +0100 Subject: [PATCH 05/14] feat(AggLayer): Solidity compatibility tests for MMR frontier code (#2312) * feat: impl fist frontier version, add (yet buggy) test * test: fix the test, fix the bug in algorithm * chore: update changelog * docs: add docs for the MMR frontier in the masm file * refactor: update the doc comments, slightly update code * refactor: update docs and comments, add overflow check, update test * test: add more leaves * test: add zero root test * chore: rename `root` -> `expected_root` in leaf assertion tests * chore: lint * chore: revert to using old dir structure * feat: generate test vectors with foundry * feat: use agglayer submodule instead of copying source files * chore: use generated test vectors in compat tests * chore: remove the human-readable fn * chore: split test vectors into leaf<>root<>count paris; and canonical zeros * chore: remove unnecessary metadata * chore: cleanup readme * changelog * chore: ignore submodule from typos check * chore: exclude submodule from toml fmt * fix: update max leaves constants and comments * chore: regen errors file * refactor: use foundry cheatcode (#2314) * chore: use workspace serde dep * chore: cleanup changelog --------- Co-authored-by: Andrey Khmuro Co-authored-by: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> --- .gitmodules | 6 + .taplo.toml | 2 + CHANGELOG.md | 5 +- Cargo.lock | 2 + Makefile | 7 ++ _typos.toml | 3 + .../miden-agglayer/solidity-compat/.gitignore | 9 ++ .../miden-agglayer/solidity-compat/README.md | 50 +++++++++ .../solidity-compat/foundry.lock | 11 ++ .../solidity-compat/foundry.toml | 14 +++ .../solidity-compat/lib/agglayer-contracts | 1 + .../solidity-compat/lib/forge-std | 1 + .../test-vectors/canonical_zeros.json | 36 ++++++ .../test-vectors/mmr_frontier_vectors.json | 104 ++++++++++++++++++ .../solidity-compat/test/MMRTestVectors.t.sol | 74 +++++++++++++ crates/miden-testing/Cargo.toml | 2 + .../tests/agglayer/mmr_frontier.rs | 90 +++++++++++++++ 17 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 100644 crates/miden-agglayer/solidity-compat/.gitignore create mode 100644 crates/miden-agglayer/solidity-compat/README.md create mode 100644 crates/miden-agglayer/solidity-compat/foundry.lock create mode 100644 crates/miden-agglayer/solidity-compat/foundry.toml create mode 160000 crates/miden-agglayer/solidity-compat/lib/agglayer-contracts create mode 160000 crates/miden-agglayer/solidity-compat/lib/forge-std create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json create mode 100644 crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..b02c269a3f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "crates/miden-agglayer/solidity-compat/lib/forge-std"] + path = crates/miden-agglayer/solidity-compat/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[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 diff --git a/.taplo.toml b/.taplo.toml index b735451f6e..b10bcd148d 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -1,3 +1,5 @@ +exclude = ["crates/miden-agglayer/solidity-compat/lib/*"] + [formatting] align_entries = true column_width = 120 diff --git a/CHANGELOG.md b/CHANGELOG.md index d326de2626..eb2d08f989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,9 +33,10 @@ - Add `AccountId::parse()` helper function to parse both hex and bech32 formats ([#2223](https://github.com/0xMiden/miden-base/pull/2223)). - Add Keccak-based MMR frontier structure to the Agglayer library ([#2245](https://github.com/0xMiden/miden-base/pull/2245)). - Add `read_foreign_account_inputs()`, `read_vault_asset_witnesses()`, and `read_storage_map_witness()` for `TransactionInputs` ([#2246](https://github.com/0xMiden/miden-base/pull/2246)). -- [BREAKING] Introduced `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249), [#2252](https://github.com/0xMiden/miden-base/pull/2252), [#2260](https://github.com/0xMiden/miden-base/pull/2260), [#2268](https://github.com/0xMiden/miden-base/pull/2268), [#2279](https://github.com/0xMiden/miden-base/pull/2279)). +- [BREAKING] Introduce `NoteAttachment` as part of `NoteMetadata` and remove `aux` and `execution_hint` ([#2249](https://github.com/0xMiden/miden-base/pull/2249), [#2252](https://github.com/0xMiden/miden-base/pull/2252), [#2260](https://github.com/0xMiden/miden-base/pull/2260), [#2268](https://github.com/0xMiden/miden-base/pull/2268), [#2279](https://github.com/0xMiden/miden-base/pull/2279)). +- Introduce standard `NetworkAccountTarget` attachment for use in network transactions which replaces `NoteTag::NetworkAccount` ([#2257](https://github.com/0xMiden/miden-base/pull/2257)). +- Add a foundry test suite for verifying AggLayer contracts compatibility ([#2312](https://github.com/0xMiden/miden-base/pull/2312)). - Added `AccountSchemaCommitment` component to expose account storage schema commitments ([#2253](https://github.com/0xMiden/miden-base/pull/2253)). -- Introduced standard `NetworkAccountTarget` attachment for use in network transactions which replaces `NoteTag::NetworkAccount` ([#2257](https://github.com/0xMiden/miden-base/pull/2257)). - Added an `AccountBuilder` extension trait to help build the schema commitment; added `AccountComponentMetadata` to `AccountComponent` ([#2269](https://github.com/0xMiden/miden-base/pull/2269)). - Added `miden::standards::access::ownable` standard module for component ownership management, and integrated it into the `network_fungible` faucet (including new tests). ([#2228](https://github.com/0xMiden/miden-base/pull/2228)). diff --git a/Cargo.lock b/Cargo.lock index 76704df33f..96857fc1b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1744,6 +1744,8 @@ dependencies = [ "rand", "rand_chacha", "rstest", + "serde", + "serde_json", "thiserror", "tokio", "winter-rand-utils", diff --git a/Makefile b/Makefile index 10f3f8823a..6cdc6e3299 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,13 @@ build-no-std: ## Build without the standard library build-no-std-testing: ## Build without the standard library. Includes the `testing` feature $(BUILD_GENERATED_FILES_IN_SRC) cargo build --no-default-features --target wasm32-unknown-unknown --workspace --exclude bench-transaction --features testing +# --- test vectors -------------------------------------------------------------------------------- + +.PHONY: generate-solidity-test-vectors +generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Foundry + 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 + # --- benchmarking -------------------------------------------------------------------------------- .PHONY: bench-tx diff --git a/_typos.toml b/_typos.toml index b3babf7b56..6bc0c6f202 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,2 +1,5 @@ [default] extend-ignore-identifiers-re = [".*1st.*", ".*2nd.*", ".*3rd.*"] + +[files] +extend-exclude = ["crates/miden-agglayer/solidity-compat/lib"] diff --git a/crates/miden-agglayer/solidity-compat/.gitignore b/crates/miden-agglayer/solidity-compat/.gitignore new file mode 100644 index 0000000000..16fe32f772 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/.gitignore @@ -0,0 +1,9 @@ +# Foundry artifacts +/out/ +/cache/ + +# Foundry broadcast files +/broadcast/ + +# Environment +.env diff --git a/crates/miden-agglayer/solidity-compat/README.md b/crates/miden-agglayer/solidity-compat/README.md new file mode 100644 index 0000000000..f93f83b5bc --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/README.md @@ -0,0 +1,50 @@ +# Solidity Compatibility Tests + +This directory contains Foundry tests for generating test vectors to verify +that the Miden MMR Frontier implementation is compatible with the Solidity +`DepositContractBase.sol` from [agglayer-contracts v2](https://github.com/agglayer/agglayer-contracts). + +## Prerequisites + +Install [Foundry](https://book.getfoundry.sh/getting-started/installation): + +```bash +curl -L https://foundry.paradigm.xyz | bash +foundryup +``` + +## Generating Test Vectors + +From the repository root, you can regenerate both canonical zeros and MMR frontier test vectors with: + +```bash +make generate-solidity-test-vectors +``` + +Or from this directory: + +```bash +# Install dependencies (first time only) +forge install + +# Generate canonical zeros (test-vectors/canonical_zeros.json) +forge test -vv --match-test test_generateCanonicalZeros + +# Generate MMR frontier vectors (test-vectors/mmr_frontier_vectors.json) +forge test -vv --match-test test_generateVectors +``` + +## Generated Files + +- `test-vectors/canonical_zeros.json` - Canonical zeros for each tree height (ZERO_n = keccak256(ZERO_{n-1} || ZERO_{n-1})) +- `test-vectors/mmr_frontier_vectors.json` - Leaf-root pairs after adding leaves 0..31 + +### Canonical Zeros + +The canonical zeros should match the constants in: +`crates/miden-agglayer/asm/bridge/canonical_zeros.masm` + +### MMR Frontier Vectors + +The `test_generateVectors` adds leaves `0, 1, 2, ...` (as left-padded 32-byte values) +and outputs the root after each addition. diff --git a/crates/miden-agglayer/solidity-compat/foundry.lock b/crates/miden-agglayer/solidity-compat/foundry.lock new file mode 100644 index 0000000000..8aa165ad75 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/foundry.lock @@ -0,0 +1,11 @@ +{ + "lib/agglayer-contracts": { + "rev": "e468f9b0967334403069aa650d9f1164b1731ebb" + }, + "lib/forge-std": { + "tag": { + "name": "v1.14.0", + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" + } + } +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/foundry.toml b/crates/miden-agglayer/solidity-compat/foundry.toml new file mode 100644 index 0000000000..c22ad7e3f6 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -0,0 +1,14 @@ +[profile.default] +libs = ["lib"] +out = "out" +solc = "0.8.20" +src = "src" + +remappings = ["@agglayer/=lib/agglayer-contracts/contracts/"] + +# Emit extra output for test vector generation +ffi = false +verbosity = 2 + +# Allow writing test vectors to file +fs_permissions = [{ access = "read-write", path = "test-vectors" }] diff --git a/crates/miden-agglayer/solidity-compat/lib/agglayer-contracts b/crates/miden-agglayer/solidity-compat/lib/agglayer-contracts new file mode 160000 index 0000000000..e468f9b096 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/lib/agglayer-contracts @@ -0,0 +1 @@ +Subproject commit e468f9b0967334403069aa650d9f1164b1731ebb diff --git a/crates/miden-agglayer/solidity-compat/lib/forge-std b/crates/miden-agglayer/solidity-compat/lib/forge-std new file mode 160000 index 0000000000..f61e4dd133 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/lib/forge-std @@ -0,0 +1 @@ +Subproject commit f61e4dd133379a4536a54ee57a808c9c00019b60 diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json b/crates/miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json new file mode 100644 index 0000000000..fbf41c38bc --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json @@ -0,0 +1,36 @@ +{ + "canonical_zeros": [ + "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" + ] +} \ 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 new file mode 100644 index 0000000000..e51ea4e4e9 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json @@ -0,0 +1,104 @@ +{ + "counts": [ + 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 + ], + "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" + ], + "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" + ] +} \ 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 new file mode 100644 index 0000000000..2e5b016232 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractBase.sol"; + +/** + * @title MMRTestVectors + * @notice Test contract that generates test vectors for verifying compatibility + * between Solidity's DepositContractBase and Miden's MMR Frontier implementation. + * + * 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 { + + /** + * @notice Generates the canonical zeros and saves to JSON file. + * ZERO_0 = 0x0...0 (32 zero bytes) + * ZERO_n = keccak256(ZERO_{n-1} || ZERO_{n-1}) + * + * Output file: test-vectors/canonical_zeros.json + */ + function test_generateCanonicalZeros() public { + bytes32[] memory zeros = new bytes32[](32); + + bytes32 z = bytes32(0); + for (uint256 i = 0; i < 32; i++) { + zeros[i] = z; + z = keccak256(abi.encodePacked(z, z)); + } + + // 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. + * Uses parallel arrays instead of array of objects for cleaner serialization. + * 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); + + for (uint256 i = 0; i < 32; i++) { + bytes32 leaf = bytes32(i); + _addLeaf(leaf); + + leaves[i] = leaf; + roots[i] = getRoot(); + counts[i] = depositCount; + } + + // 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); + + // Save to file + string memory outputPath = "test-vectors/mmr_frontier_vectors.json"; + vm.writeJson(json, outputPath); + console.log("Saved MMR frontier vectors to:", outputPath); + } +} diff --git a/crates/miden-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index 7c5314be96..37196eeab1 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -46,5 +46,7 @@ miden-crypto = { workspace = true } miden-protocol = { features = ["std"], workspace = true } primitive-types = { workspace = true } rstest = { workspace = true } +serde = { features = ["derive"], workspace = true } +serde_json = { version = "1.0" } tokio = { features = ["macros", "rt"], workspace = true } winter-rand-utils = { version = "0.13" } diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs index b4e800703c..a849b085c9 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -7,6 +7,7 @@ use miden_protocol::Felt; use miden_protocol::utils::sync::LazyLock; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; +use serde::Deserialize; // KECCAK MMR FRONTIER // ================================================================================================ @@ -128,6 +129,95 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { Ok(()) } +// SOLIDITY COMPATIBILITY TESTS +// ================================================================================================ +// These tests verify that the Rust KeccakMmrFrontier32 implementation produces identical +// results to the Solidity DepositContractBase.sol implementation. +// 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") +}); + +/// Verifies that the Rust KeccakMmrFrontier32 produces the same canonical zeros as Solidity. +#[test] +fn test_solidity_canonical_zeros_compatibility() { + for (height, expected_hex) in SOLIDITY_CANONICAL_ZEROS.canonical_zeros.iter().enumerate() { + let expected = Keccak256Digest::try_from(expected_hex.as_str()).unwrap(); + let actual = CANONICAL_ZEROS_32[height]; + + assert_eq!( + actual, expected, + "canonical zero mismatch at height {}: expected {}, got {:?}", + height, expected_hex, actual + ); + } +} + +/// Verifies that the Rust KeccakMmrFrontier32 produces the same roots as Solidity's +/// DepositContractBase after adding each leaf. +#[test] +fn test_solidity_mmr_frontier_compatibility() { + let v = &*SOLIDITY_MMR_FRONTIER_VECTORS; + + // Validate parallel arrays have same length + assert_eq!(v.leaves.len(), v.roots.len()); + assert_eq!(v.leaves.len(), v.counts.len()); + + let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); + + for i in 0..v.leaves.len() { + let leaf = Keccak256Digest::try_from(v.leaves[i].as_str()).unwrap(); + let expected_root = Keccak256Digest::try_from(v.roots[i].as_str()).unwrap(); + + let actual_root = mmr_frontier.append_and_update_frontier(leaf); + let actual_count = mmr_frontier.num_leaves; + + assert_eq!( + actual_count, v.counts[i], + "leaf count mismatch after adding leaf {}: expected {}, got {}", + v.leaves[i], v.counts[i], actual_count + ); + + assert_eq!( + actual_root, expected_root, + "root mismatch after adding leaf {} (count={}): expected {}, got {:?}", + v.leaves[i], v.counts[i], v.roots[i], actual_root + ); + } +} + // HELPER FUNCTIONS // ================================================================================================ From bdb456b0ebcf35ebb3e9d4795017133a129d9846 Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 23 Jan 2026 18:50:34 +0100 Subject: [PATCH 06/14] feat(AggLayer): add `leafType` param to `CLAIM` note (#2290) * feat: add leafType param fix after merge * changelog --- CHANGELOG.md | 1 + .../asm/bridge/agglayer_faucet.masm | 23 ++++++--------- .../miden-agglayer/asm/bridge/bridge_in.masm | 1 + .../asm/note_scripts/CLAIM.masm | 28 +++++++++++-------- crates/miden-agglayer/src/claim_note.rs | 22 +++++++++++---- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb2d08f989..abf6d6fc3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Added `AccountSchemaCommitment` component to expose account storage schema commitments ([#2253](https://github.com/0xMiden/miden-base/pull/2253)). - Added an `AccountBuilder` extension trait to help build the schema commitment; added `AccountComponentMetadata` to `AccountComponent` ([#2269](https://github.com/0xMiden/miden-base/pull/2269)). - Added `miden::standards::access::ownable` standard module for component ownership management, and integrated it into the `network_fungible` faucet (including new tests). ([#2228](https://github.com/0xMiden/miden-base/pull/2228)). +- [BREAKING] Add `leaf_value` to `CLAIM` note inputs ([#2290](https://github.com/0xMiden/miden-base/pull/2290)). ### Changes diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index e3a1178f45..c18e3fbab3 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -17,7 +17,7 @@ use miden::core::word const BRIDGE_ID_SLOT = word("miden::agglayer::faucet") const PROOF_DATA_WORD_LEN = 134 -const LEAF_DATA_WORD_LEN = 6 +const LEAF_DATA_WORD_LEN = 8 const OUTPUT_NOTE_DATA_WORD_LEN = 2 const PROOF_DATA_START_PTR = 0 @@ -33,14 +33,14 @@ 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 = 548 -const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 552 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 552 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 556 -const DESTINATION_ADDRESS_0 = 543 -const DESTINATION_ADDRESS_1 = 544 -const DESTINATION_ADDRESS_2 = 545 -const DESTINATION_ADDRESS_3 = 546 -const DESTINATION_ADDRESS_4 = 547 +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 # P2ID output note constants const P2ID_SCRIPT_ROOT = [13362761878458161062, 15090726097241769395, 444910447169617901, 3558201871398422326] @@ -102,12 +102,6 @@ proc scale_down_amount repeat.7 drop end end -# Inputs: [] -# Outputs: [prefix, suffix] -proc get_destination_account_id - mem_load.543 mem_load.544 -end - # Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY, OUTPUT_NOTE_DATA_KEY] # Outputs: [] proc batch_pipe_double_words @@ -238,7 +232,6 @@ 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 #! ], #! OUTPUT_NOTE_DATA_KEY => [ #! output_p2id_serial_num[4], // P2ID note serial number (4 felts, Word) diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index 65996e7608..3c86a53f3b 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -20,6 +20,7 @@ end #! 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) diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index 57c356ada2..e213a9f1ff 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -11,7 +11,7 @@ use miden::core::mem # ================================================================================================= const PROOF_DATA_SIZE = 536 -const LEAF_DATA_SIZE = 24 +const LEAF_DATA_SIZE = 32 const OUTPUT_NOTE_SIZE = 8 const PROOF_DATA_START_PTR = 0 @@ -68,13 +68,14 @@ 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) -#! EMPTY_WORD // padding #! ] #! #! TODO: Will be removed in future PR @@ -82,6 +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) #! ] #! #! Invocation: exec @@ -145,22 +147,24 @@ end #! Inputs: [ARGS, pad(12)] #! Outputs: [pad(16)] #! -#! NoteStorage layout (575 felts total): +#! NoteStorage layout (576 felts total): #! - smtProofLocalExitRoot [0..255] : 256 felts #! - smtProofRollupExitRoot [256..511]: 256 felts #! - globalIndex [512..519]: 8 felts #! - mainnetExitRoot [520..527]: 8 felts #! - rollupExitRoot [528..535]: 8 felts -#! - originNetwork [536] : 1 felt -#! - originTokenAddress [537..541]: 5 felts -#! - destinationNetwork [542] : 1 felt -#! - destinationAddress [543..547]: 5 felts -#! - amount [548..555]: 8 felts -#! - metadata [556..563]: 8 felts -#! - EMPTY_WORD [564..567]: 4 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 #! - 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 #! #! Where: #! - smtProofLocalExitRoot: SMT proof for local exit root (bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH]) @@ -172,17 +176,19 @@ end #! - Top 191 bits are ignored (not required to be zero), so indexers must decode it exactly like the contract does #! - mainnetExitRoot: Mainnet exit root hash (bytes32 as 8 u32 felts) #! - rollupExitRoot: Rollup exit root hash (bytes32 as 8 u32 felts) +#! - leafType: Leaf type (uint32): [0] transfer Ether / ERC20 tokens, [1] message #! - 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) #! - amount: Amount of tokens (uint256 as 8 u32 felts) #! - metadata: ABI encoded metadata (fixed size) -#! - EMPTY_WORD: Padding word +#! - 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 +#! - padding (1 felt) #! #! Panics if: #! - account does not expose claim procedure. diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index fb1087f5bd..584fd0573c 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -1,7 +1,7 @@ use alloc::vec; use alloc::vec::Vec; -use miden_core::{Felt, Word}; +use miden_core::{Felt, FieldElement, Word}; use miden_protocol::account::AccountId; use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; @@ -145,9 +145,17 @@ impl SequentialCommit for LeafData { type Commitment = Word; fn to_elements(&self) -> Vec { - const LEAF_DATA_ELEMENT_COUNT: usize = 28; // 1 + 5 + 1 + 5 + 8 + 8 (networks + addresses + amount + metadata) + const LEAF_DATA_ELEMENT_COUNT: usize = 32; // 1 + 3 + 1 + 5 + 1 + 5 + 8 + 8 (leafType + padding + networks + addresses + amount + metadata) let mut elements = Vec::with_capacity(LEAF_DATA_ELEMENT_COUNT); + // LeafType (uint32 as Felt): 0u32 for transfer Ether / ERC20 tokens, 1u32 for message + // passing. + // 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)); @@ -185,7 +193,7 @@ pub struct OutputNoteData { 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 = 7; // 4 + 2 + 1 (serial_num + account_id + tag) + const OUTPUT_NOTE_DATA_ELEMENT_COUNT: usize = 8; // 4 + 2 + 1 + 1 (serial_num + account_id + tag + padding) let mut elements = Vec::with_capacity(OUTPUT_NOTE_DATA_ELEMENT_COUNT); // P2ID note serial number (4 felts as Word) @@ -198,6 +206,9 @@ impl OutputNoteData { // Output note tag elements.push(Felt::new(self.output_note_tag.as_u32() as u64)); + // Padding + elements.extend(vec![Felt::ZERO; 1]); + elements } } @@ -220,12 +231,11 @@ impl TryFrom for NoteStorage { fn try_from(storage: ClaimNoteStorage) -> Result { // proof_data + leaf_data + empty_word + output_note_data - // 536 + 28 + 4 + 7 - let mut claim_storage = Vec::with_capacity(574); + // 536 + 32 + 8 + let mut claim_storage = Vec::with_capacity(576); claim_storage.extend(storage.proof_data.to_elements()); claim_storage.extend(storage.leaf_data.to_elements()); - claim_storage.extend(Word::empty()); claim_storage.extend(storage.output_note_data.to_elements()); NoteStorage::new(claim_storage) From a5ac23e306eb912319db26640c3283d79ba85bb5 Mon Sep 17 00:00:00 2001 From: Marti Date: Sat, 24 Jan 2026 11:18:18 +0100 Subject: [PATCH 07/14] feat(AggLayer): `UPDATE_GER` note (#2333) * feat: UPDATE_GER note outline * feat: working update ger note * chore: swap upper, lower GER parts * lint: regen error file --- .../miden-agglayer/asm/bridge/bridge_in.masm | 29 ++++++++++ .../asm/note_scripts/UPDATE_GER.masm | 50 ++++++++++++++++ crates/miden-agglayer/src/errors/agglayer.rs | 3 + crates/miden-agglayer/src/lib.rs | 46 ++++++++++----- crates/miden-agglayer/src/update_ger_note.rs | 47 +++++++++++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + .../tests/agglayer/update_ger.rs | 58 +++++++++++++++++++ 7 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm create mode 100644 crates/miden-agglayer/src/update_ger_note.rs create mode 100644 crates/miden-testing/tests/agglayer/update_ger.rs diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index 3c86a53f3b..75e06d4646 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -1,4 +1,33 @@ use miden::agglayer::crypto_utils +use miden::protocol::active_account +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================= +const GER_UPPER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_upper") +const GER_LOWER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_lower") + +# Inputs: [GER_LOWER[4], GER_UPPER[4]] +# Outputs: [] +pub proc update_ger + push.GER_LOWER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_LOWER[4], GER_UPPER[4]] + + exec.native_account::set_item + # => [OLD_VALUE, GER_UPPER[4]] + + dropw + # => [GER_UPPER[4]] + + push.GER_UPPER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_UPPER[4]] + + exec.native_account::set_item + # => [OLD_VALUE] + + dropw + # => [] +end # Inputs: [] # Output: [GER_ROOT[8]] diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm new file mode 100644 index 0000000000..baa8ebae66 --- /dev/null +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -0,0 +1,50 @@ +use miden::agglayer::bridge_in +use miden::protocol::active_note + +# CONSTANTS +# ================================================================================================= +const UPDATE_GER_NOTE_NUM_STORAGE_ITEMS = 8 +const STORAGE_PTR_GER_LOWER = 0 +const STORAGE_PTR_GER_UPPER = 4 + +# ERRORS +# ================================================================================================= +const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "UPDATE_GER script expects exactly 8 note storage items" + +#! 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). +#! +#! Requires that the account exposes: +#! - agglayer::bridge_in::update_ger procedure. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! NoteStorage layout (8 felts total): +#! - GER_LOWER [0..3] +#! - GER_UPPER [4..7] +#! +#! Panics if: +#! - account does not expose update_ger procedure. +#! - target account ID does not match the consuming account ID. +#! - number of note storage items is not exactly 8. +begin + dropw + # => [pad(16)] + + push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage + # => [num_storage_items, dest_ptr, pad(16)] + + push.UPDATE_GER_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS drop + # => [pad(16)] + + # Load GER_LOWER and GER_UPPER from note storage + mem_loadw_be.STORAGE_PTR_GER_UPPER + swapw mem_loadw_be.STORAGE_PTR_GER_LOWER + # => [GER_LOWER[4], GER_UPPER[4]] + + call.bridge_in::update_ger + # => [] + +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 61f70e3f08..38c7f11b6f 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -34,3 +34,6 @@ pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is n /// 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: "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"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 4860295f92..535c4f3cbf 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 claim_note; pub mod errors; pub mod eth_types; +pub mod update_ger_note; pub mod utils; pub use claim_note::{ @@ -39,6 +40,7 @@ pub use claim_note::{ create_claim_note, }; pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError}; +pub use update_ger_note::create_update_ger_note; // AGGLAYER NOTE SCRIPTS // ================================================================================================ @@ -66,6 +68,19 @@ pub fn claim_script() -> NoteScript { CLAIM_SCRIPT.clone() } +// Initialize the UPDATE_GER note script only once +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"); + NoteScript::new(program) +}); + +/// Returns the UPDATE_GER note script. +pub fn update_ger_script() -> NoteScript { + UPDATE_GER_SCRIPT.clone() +} + // AGGLAYER ACCOUNT COMPONENTS // ================================================================================================ @@ -191,20 +206,6 @@ pub fn asset_conversion_component(storage_slots: Vec) -> AccountCom // 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: @@ -253,10 +254,23 @@ 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 { - let bridge_component = create_bridge_account_component(); + let ger_upper_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_upper") + .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_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_component = bridge_in_component(bridge_storage_slots); + + let bridge_out_component = bridge_out_component(vec![]); + Account::builder(seed.into()) .storage_mode(AccountStorageMode::Public) - .with_component(bridge_component) + .with_component(bridge_out_component) + .with_component(bridge_in_component) } /// Creates a new bridge account with the standard configuration. diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs new file mode 100644 index 0000000000..86dd464501 --- /dev/null +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -0,0 +1,47 @@ +extern crate alloc; + +use alloc::vec; + +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteMetadata, + NoteRecipient, + NoteStorage, + NoteTag, + NoteType, +}; + +use crate::{ExitRoot, update_ger_script}; + +/// Creates an UPDATE_GER note with the given GER (Global Exit Root) data. +/// +/// The note storage contains 8 felts: GER[0..7] +pub fn create_update_ger_note( + ger: ExitRoot, + sender_account_id: miden_protocol::account::AccountId, + rng: &mut R, +) -> Result { + let update_ger_script = update_ger_script(); + + // Create note storage with 8 felts: GER[0..7] + let storage_values = ger.to_elements().to_vec(); + + 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, update_ger_script, note_storage); + + // Create note metadata - use a simple public tag + let note_tag = NoteTag::new(0); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, note_tag); + + // UPDATE_GER notes don't carry assets + let assets = NoteAssets::new(vec![])?; + + Ok(Note::new(assets, metadata, recipient)) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 44e687a15c..dc47af1bbf 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -5,3 +5,4 @@ mod crypto_utils; mod mmr_frontier; mod solidity_miden_address_conversion; pub mod test_utils; +mod update_ger; diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs new file mode 100644 index 0000000000..06c35ad6ee --- /dev/null +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -0,0 +1,58 @@ +use miden_agglayer::{ExitRoot, create_existing_bridge_account, create_update_ger_note}; +use miden_protocol::Word; +use miden_protocol::account::StorageSlotName; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::transaction::OutputNote; +use miden_testing::MockChain; + +#[tokio::test] +async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ACCOUNT + // -------------------------------------------------------------------------------------------- + let bridge_seed = builder.rng_mut().draw_word(); + let bridge_account = create_existing_bridge_account(bridge_seed); + builder.add_account(bridge_account.clone())?; + + // CREATE UPDATE_GER NOTE WITH 8 STORAGE ITEMS + // -------------------------------------------------------------------------------------------- + + let ger_bytes: [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 ger = ExitRoot::from(ger_bytes); + let update_ger_note = create_update_ger_note(ger, bridge_account.id(), builder.rng_mut())?; + + builder.add_output_note(OutputNote::Full(update_ger_note.clone())); + let mock_chain = builder.build()?; + + // EXECUTE UPDATE_GER NOTE AGAINST BRIDGE ACCOUNT + // -------------------------------------------------------------------------------------------- + let tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? + .build()?; + let executed_transaction = tx_context.execute().await?; + + // VERIFY GER WAS UPDATED IN STORAGE + // -------------------------------------------------------------------------------------------- + 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 + .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); + + Ok(()) +} From 1f5e907c049d17caaec077df4550c2e2a55cf3c1 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 27 Jan 2026 21:45:38 +0000 Subject: [PATCH 08/14] feat: port of #2352 to next --- crates/miden-agglayer/src/claim_note.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 584fd0573c..9f90a34dc0 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -1,3 +1,4 @@ +use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; @@ -9,12 +10,14 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{ Note, NoteAssets, + NoteExecutionHint, NoteMetadata, NoteRecipient, NoteStorage, NoteTag, NoteType, }; +use miden_standards::note::NetworkAccountTarget; use crate::utils::bytes32_to_felts; use crate::{EthAddressFormat, EthAmount, claim_script}; @@ -78,6 +81,7 @@ impl From<[u8; 32]> for ExitRoot { /// Proof data for CLAIM note creation. /// Contains SMT proofs and root hashes using typed representations. +#[derive(Clone)] pub struct ProofData { /// SMT proof for local exit root (32 SMT nodes) pub smt_proof_local_exit_root: [SmtNode; 32], @@ -126,6 +130,7 @@ impl SequentialCommit for ProofData { /// Leaf data for CLAIM note creation. /// Contains network, address, amount, and metadata using typed representations. +#[derive(Clone)] pub struct LeafData { /// Origin network identifier (uint32) pub origin_network: u32, @@ -181,6 +186,7 @@ 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, @@ -217,6 +223,7 @@ impl OutputNoteData { /// /// This struct groups the core data needed to create a CLAIM note that exactly /// matches the agglayer claimAsset function signature. +#[derive(Clone)] pub struct ClaimNoteStorage { /// Proof data containing SMT proofs and root hashes pub proof_data: ProofData, @@ -259,12 +266,19 @@ pub fn create_claim_note( sender_account_id: AccountId, rng: &mut R, ) -> Result { - let note_storage = NoteStorage::try_from(storage)?; + let note_storage = NoteStorage::try_from(storage.clone())?; - // TODO: Make CLAIM note a Network Note once NoteAttachment PR lands - let tag = NoteTag::new(0); + let tag = NoteTag::with_account_target(storage.output_note_data.target_faucet_account_id); - let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, tag); + let attachment = NetworkAccountTarget::new( + storage.output_note_data.target_faucet_account_id, + NoteExecutionHint::Always, + ) + .map_err(|e| NoteError::other(e.to_string()))? + .into(); + + let metadata = + NoteMetadata::new(sender_account_id, NoteType::Public, tag).with_attachment(attachment); let recipient = NoteRecipient::new(rng.draw_word(), claim_script(), note_storage); let assets = NoteAssets::new(vec![])?; From de62ecfc9d84fbf3c30da46dc18117cf24e2a9a2 Mon Sep 17 00:00:00 2001 From: Marti Date: Thu, 29 Jan 2026 08:04:17 +0100 Subject: [PATCH 09/14] chore: use `bytes_to_packed_u32_felts` instead of defining our own in `miden-base` (#2359) * chore: use the helper from vm * Fix test failure and clippy warning in crypto_utils after adopting bytes_to_packed_u32_felts (#2360) * Initial plan * Fix lint and test failures: pad felts and keep FieldElement import Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> * Add constant for LEAF_DATA_FELTS to improve code maintainability Co-authored-by: mmagician <8402446+mmagician@users.noreply.github.com> * Apply rustfmt formatting to comment line length 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> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- .../tests/agglayer/crypto_utils.rs | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 4899b76490..2c42114533 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -7,6 +7,7 @@ use alloc::vec::Vec; use miden_agglayer::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_processor::AdviceInputs; @@ -14,29 +15,9 @@ use miden_protocol::{Felt, Hasher, Word}; use super::test_utils::execute_program_with_default_host; -/// Convert bytes to field elements (u32 words packed into felts) -fn bytes_to_felts(data: &[u8]) -> Vec { - let mut felts = Vec::new(); - - // Pad data to multiple of 4 bytes - let mut padded_data = data.to_vec(); - while !padded_data.len().is_multiple_of(4) { - padded_data.push(0); - } - - // Convert to u32 words in little-endian format - for chunk in padded_data.chunks(4) { - let u32_value = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - felts.push(Felt::new(u32_value as u64)); - } - - // pad to next multiple of 4 felts - while felts.len() % 4 != 0 { - felts.push(Felt::ZERO); - } - - felts -} +// 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; fn u32_words_to_solidity_bytes32_hex(words: &[u64]) -> String { assert_eq!(words.len(), 8, "expected 8 u32 words = 32 bytes"); @@ -101,8 +82,10 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { assert_eq!(len_bytes, 113); let preimage = KeccakPreimage::new(input_u8.clone()); - let input_felts = bytes_to_felts(&input_u8); - assert_eq!(input_felts.len(), 32); + 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); From 768b4960d286292e36c24cec34bf0be89b0107d7 Mon Sep 17 00:00:00 2001 From: Marti Date: Sun, 1 Feb 2026 22:24:05 +0100 Subject: [PATCH 10/14] feat(AggLayer): implement `verify_leaf_bridge` (#2288) * feat: implement verify_leaf_bridge * chore: stack is empty, can swap instead of pad * chore: load leaf first, proof later * feat(AggLayer): implement `verify_leaf` (#2295) * feat: verify_leaf stubbed feat: fill some TODOs * chore: test global index processing * chore: update comments * self review * changelog * docs: add comment about exit root pointer locations in compute_ger Addresses PR feedback: clarify that mainnet exit root is at exit_roots_ptr and rollup exit root is at exit_roots_ptr + 8. Co-authored-by: marti * refactor: remove unused verify_claim_proof procedure This procedure has been replaced by verify_merkle_proof and is no longer used. Co-authored-by: marti * style: add section headers for better organization in bridge_in.masm Addresses PR feedback: organize the file with clear section headers for: - Constants (storage slots and memory layout) - Errors - Public interface procedures - Helper procedures Co-authored-by: marti * fix: get leaf value is exec, should not truncate stack * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: add padding to stack comments * chore: make padding explicit for update ger * chore: update_ger proc is call not exec * chore: assert_valid_ger is not public * docs: make panics explicit; add verify steps * chore: fix indent for inline comment * chore: re-org procedures: pub/helper * chore: Word prefer empty over default * changelog: move entry 0.13 -> 0.14 --------- Co-authored-by: Cursor Agent Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + .../asm/bridge/agglayer_faucet.masm | 39 ++-- .../miden-agglayer/asm/bridge/bridge_in.masm | 177 +++++++++++++++--- .../asm/bridge/crypto_utils.masm | 74 +++----- .../asm/note_scripts/UPDATE_GER.masm | 8 +- crates/miden-agglayer/src/errors/agglayer.rs | 9 + crates/miden-agglayer/src/lib.rs | 23 ++- .../tests/agglayer/global_index.rs | 82 ++++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + .../tests/agglayer/test_utils.rs | 3 +- 10 files changed, 313 insertions(+), 104 deletions(-) create mode 100644 crates/miden-testing/tests/agglayer/global_index.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cc656fd107..5e496e520f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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)). +- 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)). diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index c18e3fbab3..303ba48c8a 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -55,7 +55,7 @@ const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" -#! Inputs: [PROOF_DATA_KEY, LEAF_DATA_KEY] +#! Inputs: [LEAF_DATA_KEY, PROOF_DATA_KEY] #! Outputs: [] #! #! Panics if: @@ -65,28 +65,23 @@ const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" #! #! Invocation: exec proc validate_claim - # Get bridge_in::check_claim_proof procedure MAST root - procref.bridge_in::check_claim_proof - # => [BRIDGE_PROC_MAST_ROOT] + # get bridge_in::verify_leaf_bridge procedure MAST root + procref.bridge_in::verify_leaf_bridge + # => [BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY] push.BRIDGE_ID_SLOT[0..2] - # => [bridge_id_idx, BRIDGE_PROC_MAST_ROOT] + # => [bridge_id_idx, BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY] - # Get Bridge AccountId + # get bridge account ID exec.active_account::get_item - # => [bridge_account_id_prefix, bridge_account_id_suffix, 0, 0, BRIDGE_PROC_MAST_ROOT] + # => [bridge_account_id_prefix, bridge_account_id_suffix, 0, 0, BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY] movup.2 drop movup.2 drop - # => [bridge_account_id_prefix, bridge_account_id_suffix, BRIDGE_PROC_MAST_ROOT] + # => [bridge_account_id_prefix, bridge_account_id_suffix, BRIDGE_PROC_MAST_ROOT, LEAF_DATA_KEY, PROOF_DATA_KEY] - # Call check_claim_proof procedure on Bridge - # Calling: bridge_in::check_claim_proof + # call bridge_in::verify_leaf_bridge exec.tx::execute_foreign_procedure - # => [validation_result] - - # Assert valid proof data - assert.err=ERR_INVALID_CLAIM_PROOF drop - # => [] + # => [] end # Inputs: [] @@ -248,20 +243,22 @@ end 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 - mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR padw - mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR - # => [PROOF_DATA_KEY, LEAF_DATA_KEY] + mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR + # => [PROOF_DATA_KEY, pad(12)] + swapw + mem_loadw_be.LEAF_DATA_KEY_MEM_ADDR + # => [LEAF_DATA_KEY, PROOF_DATA_KEY, pad(8)] # Errors on invalid proof exec.validate_claim - # => [] + # => [pad(16)] # Create P2ID output note exec.build_p2id_output_note - # => [] + # => [pad(16)] end #! Burns the fungible asset from the active note. diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index 75e06d4646..bf36201c60 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -1,45 +1,70 @@ use miden::agglayer::crypto_utils +use miden::core::crypto::hashes::keccak256 +use miden::core::mem use miden::protocol::active_account use miden::protocol::native_account + # 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 = GLOBAL_INDEX_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") -# Inputs: [GER_LOWER[4], GER_UPPER[4]] -# Outputs: [] +# ERRORS +# ================================================================================================= + +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" + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Updates the Global Exit Root (GER) in the bridge account storage. +#! +#! 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]] + # => [slot_id_prefix, slot_id_suffix, GER_LOWER[4], GER_UPPER[4], pad(8)] exec.native_account::set_item - # => [OLD_VALUE, GER_UPPER[4]] + # => [OLD_VALUE, GER_UPPER[4], pad(8)] dropw - # => [GER_UPPER[4]] + # => [GER_UPPER[4], pad(12)] push.GER_UPPER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_UPPER[4]] + # => [slot_id_prefix, slot_id_suffix, GER_UPPER[4], pad(12)] exec.native_account::set_item - # => [OLD_VALUE] + # => [OLD_VALUE, pad(12)] dropw - # => [] + # => [pad(16)] end -# Inputs: [] -# Output: [GER_ROOT[8]] -pub proc get_rollup_exit_root - # Push dummy GER (8 elements) - push.0.0.0.0.0.0.0.0 # dummy GER -end - -#! Checks the validity of the GET proof +#! 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: [PROOF_DATA_KEY, LEAF_DATA_KEY, pad(8)] +#! 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]) @@ -60,14 +85,118 @@ end #! ], #! } #! +#! 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 check_claim_proof - exec.get_rollup_exit_root - # => [GER_ROOT[8], PROOF_DATA_KEY, LEAF_DATA_KEY] +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 + # TODO verify that GER is in storage + dropw dropw +end + +#! Assert the global index is valid for a mainnet deposit. +#! +#! 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 a u32 mainnet flag bit + # enforce that this limb is one + # => [mainnet_flag, GLOBAL_INDEX[6..8], LEAF_VALUE[8]] + assert.err=ERR_BRIDGE_NOT_MAINNET + + # the next element is a u32 rollup index, must be zero for a mainnet deposit + assertz.err=ERR_ROLLUP_INDEX_NON_ZERO + + # finally, the leaf index = lowest 32 bits = last limb + # => [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] - # Check CLAIM note proof data against current GER - exec.crypto_utils::verify_claim_proof - # => [is_valid_claim_proof] + # load the pointers to the merkle proof and root, to pass to `verify_merkle_proof` + push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR + push.MAINNET_EXIT_ROOT_PTR - swap drop + # => [mainnet_exit_root_ptr, smt_proof_local_exit_root_ptr, leaf_index, LEAF_VALUE[8]] + movdn.10 + # => [smt_proof_local_exit_root_ptr, leaf_index, LEAF_VALUE[8], mainnet_exit_root_ptr] + # delegate verification to crypto_utils::verify_merkle_proof (stubbed for now) + exec.crypto_utils::verify_merkle_proof end diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 60a06773c3..e6484c2da9 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -19,7 +19,7 @@ const LEAF_DATA_START_PTR = 0 #! metadata[8], // ABI encoded metadata (8 felts, fixed size) #! ], #! } -#! Outputs: [LEAF_VALUE] +#! Outputs: [LEAF_VALUE[8]] #! #! Invocation: exec pub proc get_leaf_value @@ -35,59 +35,33 @@ pub proc get_leaf_value exec.keccak256::hash_bytes # => [LEAF_VALUE[8]] - - # truncate stack - swapdw dropw dropw - # => [LEAF_VALUE[8]] end -#! Verify leaf and checks that it has not been claimed. -#! -#! This procedure verifies that a claim proof is valid against the Global Exit Tree (GET) -#! and that the leaf has not been previously claimed. +#! Computes the Global Exit Tree (GET) root from the mainnet and rollup exit roots. #! -#! Inputs: -#! Operand stack: [GER_ROOT[8], PROOF_DATA_KEY, LEAF_DATA_KEY, pad(12)] -#! 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 => [ -#! 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) -#! EMPTY_WORD // padding -#! ], -#! } -#! Outputs: -#! Operand stack: [is_valid] +#! The mainnet exit root is expected at `exit_roots_ptr` and +#! the rollup exit root is expected at `exit_roots_ptr + 8`. #! -#! Where: -#! - RPO_CLAIM_NOTE_STORAGE_COMMITMENT is the RPO hash commitment of all claim note storage -#! - leafType is the leaf type: [0] transfer Ether / ERC20 tokens, [1] message -#! - originNetwork is the origin network identifier (u32 as Felt) -#! - originAddress is the origin address (5 felts representing address) -#! - destinationNetwork is the destination network identifier (u32 as Felt) -#! - destinationAddress is the destination address (5 felts representing address) -#! - amount is the amount of tokens (u256 as Felt) -#! - metadata is the metadata (4 felts representing 4 u32 0 values) -#! - index is the index of the leaf (u32 as Felt) -#! - claimRoot is the claim root (8 felts representing bytes32) -#! - smtProof is the SMT proof data (570 felts) -#! - is_valid is 1 if the leaf is valid and not claimed, 0 otherwise +#! Inputs: [exit_roots_ptr] +#! Outputs: [GER_ROOT[8]] #! #! Invocation: exec -pub proc verify_claim_proof - # TODO: Implement actual Global Exit Tree proof verification - - # For now, drop all inputs and return 1 (valid) - dropw dropw dropw dropw - push.1 +pub proc compute_ger + push.64 swap + # => [exit_roots_ptr, len_bytes] + exec.keccak256::hash_bytes + # => [GER_ROOT[8]] +end + +#! Verifies a Merkle proof for a leaf value against a root. +#! +#! Inputs: [smt_proof_ptr, leaf_index, LEAF_VALUE[8], root_ptr] +#! Outputs: [] +#! +pub proc verify_merkle_proof + # TODO pending https://github.com/0xMiden/miden-base/issues/2278 + drop + drop + dropw dropw + drop end diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index baa8ebae66..0e4ddd8161 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -41,10 +41,12 @@ begin # Load GER_LOWER and GER_UPPER from note storage mem_loadw_be.STORAGE_PTR_GER_UPPER - swapw mem_loadw_be.STORAGE_PTR_GER_LOWER - # => [GER_LOWER[4], GER_UPPER[4]] + # => [GER_UPPER[4], pad(12)] + swapw + mem_loadw_be.STORAGE_PTR_GER_LOWER + # => [GER_LOWER[4], GER_UPPER[4], pad(8)] call.bridge_in::update_ger - # => [] + # => [pad(16)] 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 38c7f11b6f..895bff49fa 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -17,6 +17,9 @@ pub const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::f /// Error Message: "B2AGG script requires exactly 1 note asset" pub const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("B2AGG script requires exactly 1 note asset"); +/// 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"); @@ -26,12 +29,18 @@ pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combine /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); +/// Error Message: "leading bits of global index must be zero" +pub const ERR_LEADING_BITS_NON_ZERO: MasmError = MasmError::from_static_str("leading bits of global index must be zero"); + /// 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: "address limb is not u32" pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); +/// 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"); + /// Error Message: "maximum scaling factor is 18" pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_str("maximum scaling factor is 18"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 535c4f3cbf..15e1053d4d 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -206,6 +206,20 @@ pub fn asset_conversion_component(storage_slots: Vec) -> AccountCom // 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: @@ -263,14 +277,13 @@ pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { StorageSlot::with_value(ger_lower_storage_slot_name, Word::empty()), ]; - let bridge_in_component = bridge_in_component(bridge_storage_slots); - - let bridge_out_component = bridge_out_component(vec![]); + let bridge_in_comp = bridge_in_component(bridge_storage_slots); + let bridge_out_comp = bridge_out_component(vec![]); Account::builder(seed.into()) .storage_mode(AccountStorageMode::Public) - .with_component(bridge_out_component) - .with_component(bridge_in_component) + .with_component(bridge_in_comp) + .with_component(bridge_out_comp) } /// Creates a new bridge account with the standard configuration. diff --git a/crates/miden-testing/tests/agglayer/global_index.rs b/crates/miden-testing/tests/agglayer/global_index.rs new file mode 100644 index 0000000000..92c53ef61e --- /dev/null +++ b/crates/miden-testing/tests/agglayer/global_index.rs @@ -0,0 +1,82 @@ +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_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_processor::Program; +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; + + let script_code = format!( + r#" + use miden::core::sys + use miden::agglayer::bridge_in + + begin + push.{g7}.{g6}.{g5}.{g4}.{g3}.{g2}.{g1}.{g0} + exec.bridge_in::process_global_index_mainnet + exec.sys::truncate_stack + end + "# + ); + + Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_library()) + .unwrap() + .assemble_program(&script_code) + .unwrap() +} + +#[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); + + let exec_output = execute_program_with_default_host(program, None).await?; + + assert_eq!(exec_output.stack[0].as_int(), 2); + Ok(()) +} + +#[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 err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); + assert_execution_error!(err, ERR_LEADING_BITS_NON_ZERO); +} + +#[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 err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); + assert_execution_error!(err, ERR_BRIDGE_NOT_MAINNET); +} + +#[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 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/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index dc47af1bbf..f96326ffa3 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -2,6 +2,7 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; mod crypto_utils; +mod global_index; mod mmr_frontier; mod solidity_miden_address_conversion; pub mod test_utils; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 8e31578c9d..a7de4ba21d 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -75,7 +75,8 @@ 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]; - let global_index = [12345u32, 0, 0, 0, 0, 0, 0, 0]; + // 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]; let mainnet_exit_root: [u8; 32] = [ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, From 8154910e55b9651e23fcb2774c2f86aedef669ff Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 2 Feb 2026 13:11:46 +0000 Subject: [PATCH 11/14] fix: address merge leftovers --- crates/miden-agglayer/src/claim_note.rs | 4 +--- crates/miden-agglayer/src/update_ger_note.rs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 9f90a34dc0..cb313634bd 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -268,8 +268,6 @@ pub fn create_claim_note( ) -> Result { let note_storage = NoteStorage::try_from(storage.clone())?; - let tag = NoteTag::with_account_target(storage.output_note_data.target_faucet_account_id); - let attachment = NetworkAccountTarget::new( storage.output_note_data.target_faucet_account_id, NoteExecutionHint::Always, @@ -278,7 +276,7 @@ pub fn create_claim_note( .into(); let metadata = - NoteMetadata::new(sender_account_id, NoteType::Public, tag).with_attachment(attachment); + NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); let recipient = NoteRecipient::new(rng.draw_word(), claim_script(), note_storage); let assets = NoteAssets::new(vec![])?; diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index 86dd464501..cc35226240 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -10,7 +10,6 @@ use miden_protocol::note::{ NoteMetadata, NoteRecipient, NoteStorage, - NoteTag, NoteType, }; @@ -37,8 +36,7 @@ pub fn create_update_ger_note( let recipient = NoteRecipient::new(serial_num, update_ger_script, note_storage); // Create note metadata - use a simple public tag - let note_tag = NoteTag::new(0); - let metadata = NoteMetadata::new(sender_account_id, NoteType::Public, note_tag); + let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); // UPDATE_GER notes don't carry assets let assets = NoteAssets::new(vec![])?; From a96e029117e09ccb5e10768dac09d117c197b0b8 Mon Sep 17 00:00:00 2001 From: Marti Date: Mon, 2 Feb 2026 13:14:30 +0000 Subject: [PATCH 12/14] lint --- crates/miden-agglayer/src/update_ger_note.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index cc35226240..d0f0b608b3 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -4,14 +4,7 @@ use alloc::vec; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::errors::NoteError; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteStorage, - NoteType, -}; +use miden_protocol::note::{Note, NoteAssets, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; use crate::{ExitRoot, update_ger_script}; From 6f6b40e8addc661116d4d0cbc0f09b03f549b97a Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Wed, 4 Feb 2026 15:21:43 +0300 Subject: [PATCH 13/14] feat: Implement `verify_merkle_proof` for `miden::agglayer` (#2361) * feat: initial implementation * test: implement test for verify_merkle_proof * refactor: move verify_merkle_proof to crypto_utils * refactor: use DoubleWord type, update doc comments * refactor: move vectors generation to its own sol file, update the generation * docs: update the inline comments in solidity file * refactor: use local memory for loop flag, add comstants for local memory ops * refactor: update path generation, return bool from verify_merkle_proof * test: move test for the verify_merkle_proof to crypto_utils * refactor: remove outdated panic doc comment, remove unused import * refactor: add type signatures, update their formatting --- Makefile | 1 + .../miden-agglayer/asm/bridge/bridge_in.masm | 37 +- .../asm/bridge/crypto_utils.masm | 150 ++- .../asm/bridge/mmr_frontier32_keccak.masm | 26 +- crates/miden-agglayer/asm/bridge/utils.masm | 38 + .../test-vectors/merkle_proof_vectors.json | 1096 +++++++++++++++++ .../test/SMTMerkleProofVectors.t.sol | 85 ++ crates/miden-agglayer/src/errors/agglayer.rs | 3 + .../tests/agglayer/crypto_utils.rs | 121 +- .../tests/agglayer/mmr_frontier.rs | 14 +- .../tests/agglayer/test_utils.rs | 20 +- 11 files changed, 1526 insertions(+), 65 deletions(-) create mode 100644 crates/miden-agglayer/asm/bridge/utils.masm create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json create mode 100644 crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol diff --git a/Makefile b/Makefile index 6cdc6e3299..b2d5c45df3 100644 --- a/Makefile +++ b/Makefile @@ -135,6 +135,7 @@ build-no-std-testing: ## Build without the standard library. Includes the `testi generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Foundry 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 # --- benchmarking -------------------------------------------------------------------------------- diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index bf36201c60..734c2a2fd9 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -4,6 +4,13 @@ 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_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 # ================================================================================================= @@ -13,18 +20,11 @@ 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 = GLOBAL_INDEX_PTR # it's the first exit root +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") -# ERRORS -# ================================================================================================= - -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" - # PUBLIC INTERFACE # ================================================================================================= @@ -188,15 +188,24 @@ proc verify_leaf # 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_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 - push.MAINNET_EXIT_ROOT_PTR + # => [smt_proof_ptr, leaf_index, mainnet_exit_root_ptr, LEAF_VALUE[8]] - # => [mainnet_exit_root_ptr, smt_proof_local_exit_root_ptr, leaf_index, LEAF_VALUE[8]] - movdn.10 - # => [smt_proof_local_exit_root_ptr, leaf_index, LEAF_VALUE[8], mainnet_exit_root_ptr] - # delegate verification to crypto_utils::verify_merkle_proof (stubbed for now) + # 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/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index e6484c2da9..4a9534882d 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -1,10 +1,33 @@ use miden::core::crypto::hashes::keccak256 +use miden::core::word +use miden::agglayer::utils use miden::core::mem +# 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 +# ================================================================================================= + const LEAF_DATA_BYTES = 113 const LEAF_DATA_NUM_WORDS = 8 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. +const CUR_HASH_LO_LOCAL = 0 + +# The offset of the second half of the current Keccak256 hash value in the local memory of the +# `calculate_root` procedure. +const CUR_HASH_HI_LOCAL = 4 + +# PUBLIC INTERFACE +# ================================================================================================= + #! Given the leaf data key returns the leaf value. #! #! Inputs: @@ -22,7 +45,7 @@ const LEAF_DATA_START_PTR = 0 #! Outputs: [LEAF_VALUE[8]] #! #! Invocation: exec -pub proc get_leaf_value +pub proc get_leaf_value(leaf_data_key: BeWord) -> DoubleWord adv.push_mapval # => [LEAF_DATA_KEY] @@ -46,7 +69,7 @@ end #! Outputs: [GER_ROOT[8]] #! #! Invocation: exec -pub proc compute_ger +pub proc compute_ger(exit_roots_ptr: MemoryAddress) -> DoubleWord push.64 swap # => [exit_roots_ptr, len_bytes] exec.keccak256::hash_bytes @@ -55,13 +78,120 @@ end #! Verifies a Merkle proof for a leaf value against a root. #! -#! Inputs: [smt_proof_ptr, leaf_index, LEAF_VALUE[8], root_ptr] -#! Outputs: [] +#! Verifies that the root, computed using the provided Merkle path and the leaf with its index, +#! matches the provided root. +#! +#! Inputs: [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx, expected_root_ptr] +#! Outputs: [verification_flag] +#! +#! Where: +#! - expected_root_ptr is the pointer to the memory where the expected SMT root is stored. +#! - [LEAF_VALUE_LO, LEAF_VALUE_HI] is the leaf for the provided Merkle path. +#! - merkle_path_ptr is the pointer to the memory where the merkle path is stored. This path is +#! 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. +#! - verification_flag is the binary flag indicating whether the verification was successful. +pub proc verify_merkle_proof( + leaf_value: DoubleWord, + merkle_path_ptr: MemoryAddress, + leaf_idx: u32, + expected_root_ptr: MemoryAddress +) -> i1 + # calculate the root of the SMT + exec.calculate_root + # => [CALCULATED_ROOT_LO, CALCULATED_ROOT_HI, expected_root_ptr] + + # load the expected root onto the stack + movup.8 exec.utils::mem_load_double_word + # => [EXPECTED_ROOT_LO, EXPECTED_ROOT_HI, CALCULATED_ROOT_LO, CALCULATED_ROOT_HI] + + # assert the roots are equal + swapw.3 exec.word::eq + # => [exp_hi_equal_calc_hi, CALCULATED_ROOT_LO, EXPECTED_ROOT_LO] + + movdn.8 exec.word::eq and + # => [verification_flag] +end + +# HELPER PROCEDURES +# ================================================================================================= + +#! 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] +#! Outputs: [ROOT_LO, ROOT_HI] #! -pub proc verify_merkle_proof - # TODO pending https://github.com/0xMiden/miden-base/issues/2278 - drop - drop - dropw dropw - drop +#! Where: +#! - [LEAF_VALUE_LO, LEAF_VALUE_HI] is the leaf for the provided Merkle path. +#! - merkle_path_ptr is the pointer to the memory where the merkle path is stored. This path is +#! 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 +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 + # => [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] + + # 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] + + # 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] + + # 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] + + # 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] + + # 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 + # right + swapdw + # => [PATH_NODE_LO, PATH_NODE_HI, CURR_HASH_LO, CURR_HASH_HI, PAD, 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] + + # 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] + + # 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] + 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 + # => [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 61d86cb99f..b789847639 100644 --- a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm +++ b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm @@ -1,5 +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 # An MMR Frontier is a data structure based on an MMR, which combines some features of an MMR and an # SMT. @@ -318,27 +320,3 @@ proc store_canonical_zeros dropw dropw dropw drop # => [] end - -#! Stores two words to the provided global memory address. -#! -#! Inputs: [WORD_1, WORD_2, ptr] -#! Outputs: [WORD_1, WORD_2, ptr] -pub proc mem_store_double_word - dup.8 mem_storew_be swapw - # => [WORD_2, WORD_1, ptr] - - dup.8 add.4 mem_storew_be swapw - # => [WORD_1, WORD_2, ptr] -end - -#! Loads two words from the provided global memory address. -#! -#! Inputs: [ptr] -#! Outputs: [WORD_1, WORD_2] -proc mem_load_double_word - padw dup.4 add.4 mem_loadw_be - # => [WORD_2, ptr] - - padw movup.8 mem_loadw_be - # => [WORD_1, WORD_2] -end diff --git a/crates/miden-agglayer/asm/bridge/utils.masm b/crates/miden-agglayer/asm/bridge/utils.masm new file mode 100644 index 0000000000..598a392509 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/utils.masm @@ -0,0 +1,38 @@ +# Utility module containing helper procedures for the double word handling. + +# 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 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Stores two words to the provided global memory address. +#! +#! Inputs: [WORD_1, WORD_2, ptr] +#! Outputs: [WORD_1, WORD_2, ptr] +pub proc mem_store_double_word( + double_word_to_store: DoubleWord, + mem_ptr: MemoryAddress +) -> (DoubleWord, MemoryAddress) + dup.8 mem_storew_be swapw + # => [WORD_2, WORD_1, ptr] + + dup.8 add.4 mem_storew_be swapw + # => [WORD_1, WORD_2, ptr] +end + +#! Loads two words from the provided global memory address. +#! +#! 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 + # => [WORD_2, ptr] + + padw movup.8 mem_loadw_be + # => [WORD_1, WORD_2] +end diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json new file mode 100644 index 0000000000..32e6008245 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json @@ -0,0 +1,1096 @@ +{ + "leaves": [ + "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", + "0x0000000000000000000000000000000000000000000000000000000000000020" + ], + "merkle_paths": [ + "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", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000003", + "0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xa9bb8c3f1f12e9aa903a50c47f314b57610a3ab32f2d463293f58836def38d36", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000005", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xa9bb8c3f1f12e9aa903a50c47f314b57610a3ab32f2d463293f58836def38d36", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xbfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938b", + "0xa9bb8c3f1f12e9aa903a50c47f314b57610a3ab32f2d463293f58836def38d36", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000007", + "0xbfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938b", + "0xa9bb8c3f1f12e9aa903a50c47f314b57610a3ab32f2d463293f58836def38d36", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000009", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x825eb4cda6b8b44578c55770496c59e6dc3cf2235f690bcdaf51a61898ceb284", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x000000000000000000000000000000000000000000000000000000000000000b", + "0x825eb4cda6b8b44578c55770496c59e6dc3cf2235f690bcdaf51a61898ceb284", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0x41c242dcf7d95e223b291ac50602debef77ded7ede32e6f8ffe959dcf7252a7a", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x000000000000000000000000000000000000000000000000000000000000000d", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0x41c242dcf7d95e223b291ac50602debef77ded7ede32e6f8ffe959dcf7252a7a", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x9d0231707eb2041153c28e130d22114ee38b252cf17233585036af02278e4181", + "0x41c242dcf7d95e223b291ac50602debef77ded7ede32e6f8ffe959dcf7252a7a", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x9d0231707eb2041153c28e130d22114ee38b252cf17233585036af02278e4181", + "0x41c242dcf7d95e223b291ac50602debef77ded7ede32e6f8ffe959dcf7252a7a", + "0x6f4feb766c4e9e71bf038b8df02f0966e2bf98fe1eaacfd96e5d036664ca1b3c", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000011", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x6b485436b9b234417e59960d9ab1366322cfad1c365f281a05863557ce7f5ce4", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000013", + "0x6b485436b9b234417e59960d9ab1366322cfad1c365f281a05863557ce7f5ce4", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xc7be30f88cf2ecf57e79e1e1710c411ee9e22587a8053db312e33d3c0cd6c9d4", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000015", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xc7be30f88cf2ecf57e79e1e1710c411ee9e22587a8053db312e33d3c0cd6c9d4", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe3be994f4df1ce307d3423eeebbbb0cd25598c1b97b848f71392674439a00e37", + "0xc7be30f88cf2ecf57e79e1e1710c411ee9e22587a8053db312e33d3c0cd6c9d4", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000017", + "0xe3be994f4df1ce307d3423eeebbbb0cd25598c1b97b848f71392674439a00e37", + "0xc7be30f88cf2ecf57e79e1e1710c411ee9e22587a8053db312e33d3c0cd6c9d4", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000019", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x86c475ea737f13a5a246616792c8fe016fa512aa588a8c13275a280b7d0cb15a", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x000000000000000000000000000000000000000000000000000000000000001b", + "0x86c475ea737f13a5a246616792c8fe016fa512aa588a8c13275a280b7d0cb15a", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xebc2f26506912a873346107a90e0212c5f00f04e541bbba11cd17e0804ae0abf", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x000000000000000000000000000000000000000000000000000000000000001d", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xebc2f26506912a873346107a90e0212c5f00f04e541bbba11cd17e0804ae0abf", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x764104d099bd8eeca4083ffb17dea07928f5b33ecb9e5f5855689fa69803386d", + "0xebc2f26506912a873346107a90e0212c5f00f04e541bbba11cd17e0804ae0abf", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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", + "0x000000000000000000000000000000000000000000000000000000000000001f", + "0x764104d099bd8eeca4083ffb17dea07928f5b33ecb9e5f5855689fa69803386d", + "0xebc2f26506912a873346107a90e0212c5f00f04e541bbba11cd17e0804ae0abf", + "0xe5422bc118f48ba443c276c6eece85e41b7dbeb30f209266d208f1a4f6b289b0", + "0x59b52a20e3252cc46cdac45bb75f28e521319d3109ae473cc6001c9b748d48e6", + "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" + ], + "roots": [ + "0x21db8421fb719c4d28af3cda6aeee3388f75e2cc467bfc7b950d32a425f7d355", + "0xed910e47f3c21d47debc7c730c32e06da6c54ba6b88b2378a61018f206903982", + "0x9384545e9aa4ebf1b8beb19916049a38744f06ef954a3f45560632d84ce6d533", + "0x7f19d9d94592351c6243e6bcce3a26858f74a31b6254789b2150744068c605ff", + "0x5f213be2fc640249a552ca4702de66233832cb68b9df97e230cc872d5f6cb9f9", + "0x6a0f4c48041afeac25806059b6e6179035e67bde1df0b089a4eaf0ac0df6bede", + "0x351a89e8327bb977532b302b57e2245ad68d05e2750f3324179920200a0f638a", + "0x38549f7c3628e3dbbfff8a696bf672c575b832d162491d610c1b8a6a0bc7561f", + "0xe14b66b351fd28eda1af3c03ba2a9dfd60484f70fd9f81c78a5cb6811dac5ea4", + "0x0637e5960d1ee7a5b1bb0c3857ef05acc23b4a709585e4959514fd79e91ce87c", + "0x5464040bc626006595b5e766a3f9304a0cf4d273c462cc708e3aade24d5e702a", + "0xb8b51ef34cf4e32d0805ea2c9fe4852d922053bf3684938c4fa7f43379d4e343", + "0xeb44a97bc5197c63c6c93bf6fdc7f4580a3dacd4578052d38538b42efbc9d3c0", + "0x26eee5cb8da8799016b92c262a6abc344e03174f79168197be5b9db6999deae3", + "0xaa6e8163229da7b95fe79ec7c96265b959a592ed895b632823b005a112359a53", + "0xdff63dcf9fc201fb5574ff828e88832305b83eecc072470b97de44a33b3240f6", + "0x2807a065ab1528d94a6e4331d5e8cb24ceab45ace87d76b8f0a8d077d7453067", + "0x1780d5b9898266e1fdf29518d871fef4cf37bfd8fe85e605c2adb2e5e40184a4", + "0x12910a95f5e6e9ab88c460109b6dac9958f440da307e6295823ba0b8f7d08113", + "0x6bcc607d23337c15d7c38f24b637a20730e039ea7e9e3b038a633e4a28dbac30", + "0xc2ad09f3e28f9c4b33d9c24382ccdb40f4b363bc4279b04c1ce9f86fc5158966", + "0x0f3a1ab79e3ae7ec6c194316de70d30216418323387ed2699b85e0e179ca9ae4", + "0xfa21bda7ccf431411a9f1e280871f6844dd57d49b4c3da89cacc559188b71e01", + "0x1353b7507a48e5368938a0509e49b930e9a4b16593485c82ba23cf7bdbd2f755", + "0x3d54e0d77b266fe0cd3c08a6234cbb5da409c369b608c5229ee7f555576666b4", + "0x6f5eb8c44bd89bd453e8276c780bb8cade32e914320f2f8269f8da512ed5c26c", + "0x5d83580ced8a725c6940898df7206e114078b722fc0f9331fc674ec41c79f39d", + "0xbd8f0f4d65d34fa78ed76b127ac25c6f173303dae7c283c2110d1622c1562378", + "0xf8aa0f2cf2e576ca7458b02981f29feda322684d6de4c4974c7d7b746c691b4d", + "0xda53b0a86569aed1c624f93df12ed4b9f6731a3b12d83e91aa7b60b0a3f81861", + "0x47a023340aabdc3853af05d3db190ee5be1b47133935b3b5d289bec6a144fb03", + "0xa6e29137a2d8363c701b8d8e3dc4015b2998386cfa2013e921cbf37ad20eaa51" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol new file mode 100644 index 0000000000..5867414ec6 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/SMTMerkleProofVectors.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +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 + * merkle paths array. + */ + function test_generateVerificationProofData() public { + 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. + 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)); + } + + // generate leaves, roots, and merkle_paths arrays + for (uint256 i = 0; i < 32; i++) { + // use bytes32(i + 1) as leaf here just to avoid the zero leaf + bytes32 leaf = bytes32(i + 1); + + // Merkle path in the _branch array during the `i`th iteration actually corresponds to + // the leaf and root with indexes `i - 1` (because the merkle path is computed based on + // 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. + 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]; + } + } + + _addLeaf(leaf); + + leaves[i] = leaf; + roots[i] = getRoot(); + + // perform the sanity check to make sure that the generated data is valid + assert(this.verifyMerkleProof(leaves[i], current_path, uint32(i), roots[i])); + } + + // Serialize parallel arrays to JSON + string memory obj = "root"; + vm.serializeBytes32(obj, "leaves", leaves); + vm.serializeBytes32(obj, "roots", roots); + string memory json = vm.serializeBytes32(obj, "merkle_paths", merkle_paths); + + // Save to file + string memory outputPath = "test-vectors/merkle_proof_vectors.json"; + vm.writeJson(json, outputPath); + console.log("Saved Merkle path vectors to:", outputPath); + } +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 895bff49fa..308e40e3db 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -44,5 +44,8 @@ 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: "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: "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"); diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 2c42114533..392795086c 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -4,21 +4,47 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; +use anyhow::Context; use miden_agglayer::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::hash::keccak::Keccak256Digest; use miden_processor::AdviceInputs; +use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Hasher, Word}; +use miden_standards::code_builder::CodeBuilder; +use miden_testing::TransactionContextBuilder; +use serde::Deserialize; -use super::test_utils::execute_program_with_default_host; +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"); + +/// 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") +}); + 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]; @@ -127,3 +153,96 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { 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)?; + + 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(()) +} + +// HELPER FUNCTIONS +// ================================================================================================ + +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 = + 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); + // 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 + ", + height * 8, + height * 8 + 4 + )); + } + + // 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); + + // 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); + + 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_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 + # => [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 + "# + ) +} diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs index a849b085c9..367d221cc5 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -3,12 +3,13 @@ use alloc::string::ToString; use miden_agglayer::agglayer_library; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; -use miden_protocol::Felt; 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 // ================================================================================================ @@ -221,17 +222,6 @@ fn test_solidity_mmr_frontier_compatibility() { // HELPER FUNCTIONS // ================================================================================================ -/// Transforms the `[Keccak256Digest]` into two word strings: (`a, b, c, d`, `e, f, g, h`) -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::>(); - - (double_word[0..4].join(", "), double_word[4..8].join(", ")) -} - fn leaf_assertion_code( leaf: Keccak256Digest, expected_root: Keccak256Digest, diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index a7de4ba21d..b77d99e1bf 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -5,10 +5,22 @@ use alloc::vec::Vec; use miden_agglayer::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, Program, StackInputs}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Felt, Program, StackInputs}; use miden_protocol::transaction::TransactionKernel; +/// 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::>(); + + (double_word[0..4].join(", "), double_word[4..8].join(", ")) +} + /// Execute a program with default host and optional advice inputs pub async fn execute_program_with_default_host( program: Program, @@ -79,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] = [ - 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, + 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, ]; let rollup_exit_root: [u8; 32] = [ From a1f3b48a2bf384c522fe175fd24ccb82af81b0b2 Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 4 Feb 2026 14:04:43 +0100 Subject: [PATCH 14/14] feat(AggLayer): `B2AGG` note consumption check (#2334) * feat: put target account in attachment * feat: extract b2agg creation to helper * lint: regen error file * feat: use NetworkAccountTarget for attachments - make bridge a network account - move the ID check to non-reclaim branch * fix: still need LET slot * chore: test the target mismatch logic * chore: apply target checks to UPDATE_GER * Apply suggestion from @mmagician * chore: add TODOs * chore: lint * docs(masm): trim dangling B2AGG note description Co-authored-by: marti * docs(masm): clarify B2AGG attachment layout Co-authored-by: marti * feat: use network account target helper Co-authored-by: marti * feat: make B2AGG notes always public Co-authored-by: marti * feat: mark B2AGG notes always consumable Co-authored-by: marti * style: use NoteScript import for B2AGG Co-authored-by: marti * docs: add changelog entry for PR 2334 Co-authored-by: marti * Apply suggestions from code review * chore: move helper to network_account_target * chore: note script section headers * chore: comment simplify and wrap * lint * fix: use `with_account_target` instead of `0` for `NoteTag` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: add missing panic to get_id * chore: mention active note * docs: how active_account_matches_target_account panics * Revert "fix: use `with_account_target` instead of `0` for `NoteTag`" This reverts commit 02826728fc826092672b673c264161d8fc031ff0. * feat: extract assert_is_network_account_target helper * feat: change is_network_account_target to return bool, not panic * feat: swap kind/scheme in signatures * lint: regen errors * feat: encapsulate note logic in structs * chore: rename file to bagg_note * chore: remove redundant comments * Revert "feat: swap kind/scheme in signatures" This reverts commit aad47e5d6b5d5f32c0e9da451a667aec57912e61. --------- Co-authored-by: Cursor Agent Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + .../asm/note_scripts/B2AGG.masm | 18 +- .../asm/note_scripts/UPDATE_GER.masm | 17 +- crates/miden-agglayer/src/b2agg_note.rs | 131 ++++++++++++ crates/miden-agglayer/src/errors/agglayer.rs | 4 + crates/miden-agglayer/src/lib.rs | 46 ++--- crates/miden-agglayer/src/update_ger_note.rs | 119 +++++++++-- .../attachments/network_account_target.masm | 77 ++++++- .../miden-standards/src/errors/standards.rs | 8 +- .../src/standards/network_account_target.rs | 16 ++ .../tests/agglayer/bridge_out.rs | 188 +++++++++++------- .../tests/agglayer/update_ger.rs | 14 +- 12 files changed, 492 insertions(+), 147 deletions(-) create mode 100644 crates/miden-agglayer/src/b2agg_note.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c48cb95db..f267b36208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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)). +- Added B2AGG and UPDATE_GER note attachment target checks ([#2334](https://github.com/0xMiden/miden-base/pull/2334)). - Added double-word array data structure abstraction over storage maps ([#2299](https://github.com/0xMiden/miden-base/pull/2299)). - 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)). diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index a62e213daa..9523160e9c 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -2,6 +2,8 @@ use miden::agglayer::bridge_out use miden::protocol::account_id use miden::protocol::active_account use miden::protocol::active_note +use miden::protocol::note +use miden::standards::attachments::network_account_target use miden::standards::wallets::basic->basic_wallet # CONSTANTS @@ -12,16 +14,18 @@ const B2AGG_NOTE_NUM_STORAGE_ITEMS=6 # ERRORS # ================================================================================================= const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS="B2AGG script requires exactly 1 note asset" - const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly 6 note storage items" +const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account does not match consuming account" + +# NOTE SCRIPT +# ================================================================================================= #! Bridge-to-AggLayer (B2AGG) note script: bridges assets from Miden to an AggLayer-connected chain. #! #! This note can be consumed in two ways: #! - If the consuming account is the sender (reclaim): the note's assets are added back to the consuming account. -#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note, +#! - If the consuming account is the Agglayer Bridge: the note's assets are moved to a BURN note, #! and the note details are hashed into a leaf and appended to the Local Exit Tree. -#! global exit root (GER) merkle tree structure. #! #! Inputs: [] #! Outputs: [] @@ -34,10 +38,13 @@ const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS="B2AGG script expects exactly #! - destination_address_2: bytes 8-11 #! - destination_address_3: bytes 12-15 #! - destination_address_4: bytes 16-19 +#! Note attachment is constructed from a NetworkAccountTarget standard: +#! - [0, exec_hint_tag, target_id_prefix, target_id_suffix] #! #! Panics if: #! - The note does not contain exactly 6 storage items. #! - The note does not contain exactly 1 asset. +#! - The note attachment does not target the consuming account. #! begin dropw @@ -58,6 +65,11 @@ begin exec.basic_wallet::add_assets_to_account # => [pad(16)] else + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_B2AGG_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + # Store note storage -> mem[8..14] push.8 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 0e4ddd8161..1ca3d1ab9d 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -1,5 +1,9 @@ use miden::agglayer::bridge_in use miden::protocol::active_note +use miden::protocol::active_account +use miden::protocol::account_id +use miden::protocol::note +use miden::standards::attachments::network_account_target # CONSTANTS # ================================================================================================= @@ -10,6 +14,10 @@ const STORAGE_PTR_GER_UPPER = 4 # ERRORS # ================================================================================================= const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS = "UPDATE_GER script expects exactly 8 note storage items" +const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH = "UPDATE_GER note attachment target account does not match consuming account" + +# NOTE SCRIPT +# ================================================================================================= #! Agglayer Bridge UPDATE_GER script: updates the GER by calling the bridge_in::update_ger function. #! @@ -33,6 +41,13 @@ begin dropw # => [pad(16)] + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + + # proceed with the GER update logic + push.STORAGE_PTR_GER_LOWER exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] @@ -49,4 +64,4 @@ begin call.bridge_in::update_ger # => [pad(16)] -end \ No newline at end of file +end diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs new file mode 100644 index 0000000000..49451976ab --- /dev/null +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -0,0 +1,131 @@ +//! Bridge Out note creation utilities. +//! +//! This module provides helpers for creating B2AGG (Bridge to AggLayer) notes, +//! which are used to bridge assets out from Miden to the AggLayer network. + +use alloc::string::ToString; +use alloc::vec::Vec; + +use miden_assembly::utils::Deserializable; +use miden_core::{Felt, 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, + NoteExecutionHint, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteType, +}; +use miden_standards::note::NetworkAccountTarget; +use miden_utils_sync::LazyLock; + +use crate::EthAddressFormat; + +// NOTE SCRIPT +// ================================================================================================ + +// 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"); + NoteScript::new(program) +}); + +// B2AGG NOTE +// ================================================================================================ + +/// B2AGG (Bridge to AggLayer) note. +/// +/// This note is used to bridge assets from Miden to another network via the AggLayer. +/// When consumed by a bridge account, the assets are burned and a corresponding +/// claim can be made on the destination network. B2AGG notes are always public. +pub struct B2AggNote; + +impl B2AggNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for a B2AGG note. + pub const NUM_STORAGE_ITEMS: usize = 6; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the B2AGG (Bridge to AggLayer) note script. + pub fn script() -> NoteScript { + B2AGG_SCRIPT.clone() + } + + /// Returns the B2AGG note script root. + pub fn script_root() -> Word { + B2AGG_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Creates a B2AGG (Bridge to AggLayer) note. + /// + /// This note is used to bridge assets from Miden to another network via the AggLayer. + /// When consumed by a bridge account, the assets are burned and a corresponding + /// claim can be made on the destination network. B2AGG notes are always public. + /// + /// # Parameters + /// - `destination_network`: The AggLayer-assigned network ID for the destination chain + /// - `destination_address`: The Ethereum address on the destination network + /// - `assets`: The assets to bridge (must be fungible assets from a network faucet) + /// - `target_account_id`: The account ID that will consume this note (bridge account) + /// - `sender_account_id`: The account ID of the note creator + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + destination_network: u32, + destination_address: EthAddressFormat, + assets: NoteAssets, + target_account_id: AccountId, + sender_account_id: AccountId, + rng: &mut R, + ) -> Result { + let note_storage = build_note_storage(destination_network, destination_address)?; + + 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); + + let recipient = NoteRecipient::new(rng.draw_word(), Self::script(), note_storage); + + Ok(Note::new(assets, metadata, recipient)) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Builds the note storage for a B2AGG note. +/// +/// The storage layout is: +/// - 1 felt: destination_network +/// - 5 felts: destination_address (20 bytes as 5 u32 values) +fn build_note_storage( + destination_network: u32, + destination_address: EthAddressFormat, +) -> Result { + let mut elements = Vec::with_capacity(6); + + elements.push(Felt::new(destination_network as u64)); + elements.extend(destination_address.to_elements()); + + NoteStorage::new(elements) +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 308e40e3db..a1874001d9 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -12,6 +12,8 @@ use miden_protocol::errors::MasmError; /// 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" pub const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("B2AGG script expects exactly 6 note storage items"); /// Error Message: "B2AGG script requires exactly 1 note asset" @@ -47,5 +49,7 @@ 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: "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"); diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 82ee38d116..d1538716e7 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -24,12 +24,14 @@ use miden_standards::account::auth::NoAuth; use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_utils_sync::LazyLock; +pub mod b2agg_note; pub mod claim_note; pub mod errors; pub mod eth_types; pub mod update_ger_note; pub mod utils; +pub use b2agg_note::B2AggNote; pub use claim_note::{ ClaimNoteStorage, ExitRoot, @@ -40,22 +42,11 @@ pub use claim_note::{ create_claim_note, }; pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError}; -pub use update_ger_note::create_update_ger_note; +pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS // ================================================================================================ -// 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")); - Program::read_from_bytes(bytes).expect("Shipped B2AGG script is well-formed") -}); - -/// Returns the B2AGG (Bridge to AggLayer) note script. -pub fn b2agg_script() -> Program { - B2AGG_SCRIPT.clone() -} - // 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")); @@ -68,19 +59,6 @@ pub fn claim_script() -> NoteScript { CLAIM_SCRIPT.clone() } -// Initialize the UPDATE_GER note script only once -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"); - NoteScript::new(program) -}); - -/// Returns the UPDATE_GER note script. -pub fn update_ger_script() -> NoteScript { - UPDATE_GER_SCRIPT.clone() -} - // AGGLAYER ACCOUNT COMPONENTS // ================================================================================================ @@ -268,22 +246,28 @@ 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") .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_storage_slots = vec![ + 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_comp = bridge_in_component(bridge_storage_slots); - let bridge_out_comp = bridge_out_component(vec![]); + 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)]; + let bridge_out_component = bridge_out_component(bridge_out_storage_slots); + + // Combine the components into a single account(builder) Account::builder(seed.into()) - .storage_mode(AccountStorageMode::Public) - .with_component(bridge_in_comp) - .with_component(bridge_out_comp) + .storage_mode(AccountStorageMode::Network) + .with_component(bridge_out_component) + .with_component(bridge_in_component) } /// Creates a new bridge account with the standard configuration. diff --git a/crates/miden-agglayer/src/update_ger_note.rs b/crates/miden-agglayer/src/update_ger_note.rs index d0f0b608b3..f760f0f604 100644 --- a/crates/miden-agglayer/src/update_ger_note.rs +++ b/crates/miden-agglayer/src/update_ger_note.rs @@ -1,38 +1,115 @@ +//! UPDATE_GER note creation utilities. +//! +//! This module provides helpers for creating UPDATE_GER notes, +//! which are used to update the Global Exit Root in the bridge account. + 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, NoteMetadata, NoteRecipient, NoteStorage, NoteType}; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteExecutionHint, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteType, +}; +use miden_standards::note::NetworkAccountTarget; +use miden_utils_sync::LazyLock; + +use crate::ExitRoot; + +// NOTE SCRIPT +// ================================================================================================ -use crate::{ExitRoot, update_ger_script}; +// Initialize the UPDATE_GER note script only once +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"); + NoteScript::new(program) +}); -/// Creates an UPDATE_GER note with the given GER (Global Exit Root) data. +// UPDATE_GER NOTE +// ================================================================================================ + +/// UPDATE_GER note. /// -/// The note storage contains 8 felts: GER[0..7] -pub fn create_update_ger_note( - ger: ExitRoot, - sender_account_id: miden_protocol::account::AccountId, - rng: &mut R, -) -> Result { - let update_ger_script = update_ger_script(); +/// This note is used to update the Global Exit Root (GER) in the bridge account. +/// It carries the new GER data and is always public. +pub struct UpdateGerNote; + +impl UpdateGerNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for an UPDATE_GER note. + pub const NUM_STORAGE_ITEMS: usize = 8; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the UPDATE_GER note script. + pub fn script() -> NoteScript { + UPDATE_GER_SCRIPT.clone() + } + + /// Returns the UPDATE_GER note script root. + pub fn script_root() -> Word { + UPDATE_GER_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- - // Create note storage with 8 felts: GER[0..7] - let storage_values = ger.to_elements().to_vec(); + /// Creates an UPDATE_GER note with the given GER (Global Exit Root) data. + /// + /// The note storage contains 8 felts: GER[0..7] + /// + /// # Parameters + /// - `ger`: The Global Exit Root data + /// - `sender_account_id`: The account ID of the note creator + /// - `target_account_id`: The account ID that will consume this note (bridge account) + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + ger: ExitRoot, + sender_account_id: AccountId, + target_account_id: AccountId, + rng: &mut R, + ) -> Result { + // Create note storage with 8 felts: GER[0..7] + let storage_values = ger.to_elements().to_vec(); - let note_storage = NoteStorage::new(storage_values)?; + let note_storage = NoteStorage::new(storage_values)?; - // Generate a serial number for the note - let serial_num = rng.draw_word(); + // Generate a serial number for the note + let serial_num = rng.draw_word(); - let recipient = NoteRecipient::new(serial_num, update_ger_script, note_storage); + let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage); - // Create note metadata - use a simple public tag - let metadata = NoteMetadata::new(sender_account_id, NoteType::Public); + 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); - // UPDATE_GER notes don't carry assets - let assets = NoteAssets::new(vec![])?; + // UPDATE_GER notes don't carry assets + let assets = NoteAssets::new(vec![])?; - Ok(Note::new(assets, metadata, recipient)) + Ok(Note::new(assets, metadata, recipient)) + } } diff --git a/crates/miden-standards/asm/standards/attachments/network_account_target.masm b/crates/miden-standards/asm/standards/attachments/network_account_target.masm index 9c097162bc..46133f4136 100644 --- a/crates/miden-standards/asm/standards/attachments/network_account_target.masm +++ b/crates/miden-standards/asm/standards/attachments/network_account_target.masm @@ -2,6 +2,8 @@ #! #! Provides a standardized way to work with network account targets. +use miden::protocol::account_id +use miden::protocol::active_account use miden::protocol::active_note use miden::protocol::note @@ -18,32 +20,45 @@ pub const NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND = 1 # ERRORS # ================================================================================================ -const ERR_ATTACHMENT_SCHEME_MISMATCH = "expected network account target attachment scheme" -const ERR_ATTACHMENT_KIND_MISMATCH = "expected attachment kind to be Word for network account target" +const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" + +#! Returns a boolean indicating whether the attachment scheme and kind match the expected +#! values for a NetworkAccountTarget attachment. +#! +#! Inputs: [attachment_scheme, attachment_kind] +#! Outputs: [is_network_account_target] +#! +#! Invocation: exec +pub proc is_network_account_target + eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME + # => [is_scheme_valid, attachment_kind] + + swap eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND + # => [is_kind_valid, is_scheme_valid] + + and + # => [is_network_account_target] +end #! Returns the account ID encoded in the attachment. #! #! The attachment is expected to have the following layout: #! [0, exec_hint_tag, account_id_prefix, account_id_suffix] #! +#! WARNING: This procedure does not validate the attachment scheme or kind. The caller +#! should validate these using `is_network_account_target` before calling this procedure. +#! #! WARNING: This procedure does not validate that the returned account ID is well-formed. #! The caller should validate the account ID if needed using `account_id::validate`. #! -#! Inputs: [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] +#! Inputs: [NOTE_ATTACHMENT] #! Outputs: [account_id_prefix, account_id_suffix] #! #! Where: #! - account_id_{prefix,suffix} are the prefix and suffix felts of an account ID. #! -#! Panics if: -#! - the attachment scheme does not match NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME. -#! #! Invocation: exec pub proc get_id - # verify that the attachment scheme and kind are correct - # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] - eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME assert.err=ERR_ATTACHMENT_SCHEME_MISMATCH - eq.NETWORK_ACCOUNT_TARGET_ATTACHMENT_KIND assert.err=ERR_ATTACHMENT_KIND_MISMATCH # => [NOTE_ATTACHMENT] = [0, exec_hint_tag, account_id_prefix, account_id_suffix] drop drop @@ -70,3 +85,45 @@ pub proc new push.NETWORK_ACCOUNT_TARGET_ATTACHMENT_SCHEME # => [attachment_scheme, attachment_kind, ATTACHMENT] end + +#! Returns a boolean indicating whether the active account matches the target account +#! encoded in the active note's attachment. +#! +#! Inputs: [] +#! Outputs: [is_equal] +#! +#! Where: +#! - is_equal is a boolean indicating whether the active account matches the target account. +#! +#! Panics if: +#! - the attachment is not a valid network account target. +#! +#! Invocation: exec +pub proc active_account_matches_target_account + # ensure note attachment targets the consuming bridge account + exec.active_note::get_metadata + # => [NOTE_ATTACHMENT, METADATA_HEADER] + + swapw + # => [METADATA_HEADER, NOTE_ATTACHMENT] + + exec.note::extract_attachment_info_from_metadata + # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] + + swap + # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + + # ensure the attachment is a network account target + exec.is_network_account_target assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + + # => [NOTE_ATTACHMENT] = [0, exec_hint_tag, account_id_prefix, account_id_suffix] + + exec.get_id + # => [account_id_prefix, account_id_suffix] + + exec.active_account::get_id + # => [account_id_prefix, account_id_suffix, target_id_prefix, target_id_suffix] + + exec.account_id::is_equal + # => [is_equal] +end diff --git a/crates/miden-standards/src/errors/standards.rs b/crates/miden-standards/src/errors/standards.rs index 261f80e062..f16cc23ed4 100644 --- a/crates/miden-standards/src/errors/standards.rs +++ b/crates/miden-standards/src/errors/standards.rs @@ -9,11 +9,6 @@ use miden_protocol::errors::MasmError; // STANDARDS ERRORS // ================================================================================================ -/// Error Message: "expected attachment kind to be Word for network account target" -pub const ERR_ATTACHMENT_KIND_MISMATCH: MasmError = MasmError::from_static_str("expected attachment kind to be Word for network account target"); -/// Error Message: "expected network account target attachment scheme" -pub const ERR_ATTACHMENT_SCHEME_MISMATCH: MasmError = MasmError::from_static_str("expected network account target attachment scheme"); - /// Error Message: "burn requires exactly 1 note asset" pub const ERR_BASIC_FUNGIBLE_BURN_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("burn requires exactly 1 note asset"); @@ -36,6 +31,9 @@ pub const ERR_MINT_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::fr /// Error Message: "note tag length can be at most 32" pub const ERR_NOTE_TAG_MAX_ACCOUNT_TARGET_LENGTH_EXCEEDED: MasmError = MasmError::from_static_str("note tag length can be at most 32"); +/// Error Message: "attachment is not a valid network account target" +pub const ERR_NOT_NETWORK_ACCOUNT_TARGET: MasmError = MasmError::from_static_str("attachment is not a valid network account target"); + /// Error Message: "failed to reclaim P2IDE note because the reclaiming account is not the sender" pub const ERR_P2IDE_RECLAIM_ACCT_IS_NOT_SENDER: MasmError = MasmError::from_static_str("failed to reclaim P2IDE note because the reclaiming account is not the sender"); /// Error Message: "P2IDE reclaim is disabled" diff --git a/crates/miden-testing/src/standards/network_account_target.rs b/crates/miden-testing/src/standards/network_account_target.rs index e3d0b0f798..bd07dfe44a 100644 --- a/crates/miden-testing/src/standards/network_account_target.rs +++ b/crates/miden-testing/src/standards/network_account_target.rs @@ -26,12 +26,21 @@ async fn network_account_target_get_id() -> anyhow::Result<()> { use miden::standards::attachments::network_account_target use miden::protocol::note + const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" + begin push.{attachment_word} push.{metadata_header} exec.note::extract_attachment_info_from_metadata # => [attachment_kind, attachment_scheme, NOTE_ATTACHMENT] + swap + # => [attachment_scheme, attachment_kind, NOTE_ATTACHMENT] + exec.network_account_target::is_network_account_target + # => [is_valid, NOTE_ATTACHMENT] + assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + # => [NOTE_ATTACHMENT] exec.network_account_target::get_id + # => [account_id_prefix, account_id_suffix] # cleanup stack movup.2 drop movup.2 drop end @@ -104,6 +113,8 @@ async fn network_account_target_attachment_round_trip() -> anyhow::Result<()> { r#" use miden::standards::attachments::network_account_target + const ERR_NOT_NETWORK_ACCOUNT_TARGET = "attachment is not a valid network account target" + begin push.{exec_hint} push.{target_id_suffix} @@ -111,8 +122,13 @@ async fn network_account_target_attachment_round_trip() -> anyhow::Result<()> { # => [target_id_prefix, target_id_suffix, exec_hint] exec.network_account_target::new # => [attachment_scheme, attachment_kind, ATTACHMENT] + exec.network_account_target::is_network_account_target + # => [is_valid, ATTACHMENT] + assert.err=ERR_NOT_NETWORK_ACCOUNT_TARGET + # => [ATTACHMENT] exec.network_account_target::get_id # => [target_id_prefix, target_id_suffix] + # cleanup stack movup.2 drop movup.2 drop end "#, diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 72e8ec48b1..2ba7682dc2 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,32 +1,16 @@ extern crate alloc; -use miden_agglayer::{EthAddressFormat, b2agg_script, bridge_out_component}; -use miden_protocol::account::{ - Account, - AccountId, - AccountIdVersion, - AccountStorageMode, - AccountType, - StorageSlot, - StorageSlotName, -}; +use miden_agglayer::errors::ERR_B2AGG_TARGET_ACCOUNT_MISMATCH; +use miden_agglayer::{B2AggNote, EthAddressFormat, create_existing_bridge_account}; +use miden_crypto::rand::FeltRng; +use miden_protocol::Felt; +use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteScript, - NoteStorage, - NoteTag, - NoteType, -}; +use miden_protocol::note::{NoteAssets, NoteScript, NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; -use miden_protocol::{Felt, Word}; use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_standards::note::StandardNote; -use miden_testing::{AccountState, Auth, MockChain}; -use rand::Rng; +use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; /// Tests the B2AGG (Bridge to AggLayer) note script with bridge_out account component. /// @@ -52,50 +36,33 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; - // Create a bridge account with the bridge_out component using network (public) storage - // Add a storage map for the bridge component to store MMR frontier data - let storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap(); - let storage_slots = vec![StorageSlot::with_empty_map(storage_slot_name)]; - let bridge_component = bridge_out_component(storage_slots); - let account_builder = Account::builder(builder.rng_mut().random()) - .storage_mode(AccountStorageMode::Public) - .with_component(bridge_component); - let mut bridge_account = - builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists)?; + // 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(); - let tag = NoteTag::new(0); - let note_type = NoteType::Public; // Use Public note type for network transaction - - // Get the B2AGG note script - let b2agg_script = b2agg_script(); // Create note storage with destination network and address - // destination_network: u32 (AggLayer-assigned network ID) - // destination_address: 20 bytes (Ethereum address) split into 5 u32 values - let destination_network = Felt::new(1); // Example network ID + let destination_network = 1u32; // Example network ID let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); - let address_felts = eth_address.to_elements().to_vec(); - - // Combine network ID and address felts into note storage (6 felts total) - let mut input_felts = vec![destination_network]; - input_felts.extend(address_felts); - let inputs = NoteStorage::new(input_felts.clone())?; + let assets = NoteAssets::new(vec![bridge_asset])?; - // Create the B2AGG note with assets from the faucet - let b2agg_note_metadata = NoteMetadata::new(faucet.id(), note_type).with_tag(tag); - let b2agg_note_assets = NoteAssets::new(vec![bridge_asset])?; - let serial_num = Word::from([1, 2, 3, 4u32]); - let b2agg_note_script = NoteScript::new(b2agg_script); - let b2agg_note_recipient = NoteRecipient::new(serial_num, b2agg_note_script, inputs); - let b2agg_note = Note::new(b2agg_note_assets, b2agg_note_metadata, b2agg_note_recipient); + // 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())); @@ -219,6 +186,10 @@ async fn test_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) + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + // Create a user account that will create and consume the B2AGG note let mut user_account = builder.add_existing_wallet(Auth::BasicAuth)?; @@ -227,33 +198,25 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let amount = Felt::new(50); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - let tag = NoteTag::new(0); - let note_type = NoteType::Public; - - // Get the B2AGG note script - let b2agg_script = b2agg_script(); // Create note storage with destination network and address - let destination_network = Felt::new(1); + let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); - let address_felts = eth_address.to_elements().to_vec(); - - // Combine network ID and address felts into note storage (6 felts total) - let mut input_felts = vec![destination_network]; - input_felts.extend(address_felts); - let inputs = NoteStorage::new(input_felts.clone())?; + 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 - let b2agg_note_metadata = NoteMetadata::new(user_account.id(), note_type).with_tag(tag); - let b2agg_note_assets = NoteAssets::new(vec![bridge_asset])?; - let serial_num = Word::from([1, 2, 3, 4u32]); - let b2agg_note_script = NoteScript::new(b2agg_script); - let b2agg_note_recipient = NoteRecipient::new(serial_num, b2agg_note_script, inputs); - let b2agg_note = Note::new(b2agg_note_assets, b2agg_note_metadata, b2agg_note_recipient); + let b2agg_note = B2AggNote::create( + destination_network, + eth_address, + assets, + bridge_account.id(), + user_account.id(), + builder.rng_mut(), + )?; // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); @@ -297,3 +260,84 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { Ok(()) } + +/// Tests that a non-target account cannot consume a B2AGG note (non-reclaim branch). +/// +/// This test covers the security check in the B2AGG note script that ensures only the +/// designated target account (specified in the note attachment) can consume the note +/// when not in reclaim mode. +/// +/// Test flow: +/// 1. Creates a network faucet to provide assets +/// 2. Creates a bridge account as the designated target for the B2AGG note +/// 3. Creates a user account as the sender (creator) of the B2AGG note +/// 4. Creates a "malicious" account with a bridge interface +/// 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<()> { + let mut builder = MockChain::builder(); + + // Create a network faucet owner account + let faucet_owner_account_id = AccountId::dummy( + [1; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + 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))?; + + // Create a bridge account as the designated TARGET for the B2AGG note + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + 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()); + builder.add_account(malicious_account.clone())?; + + // 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 = + EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + + let assets = NoteAssets::new(vec![bridge_asset])?; + + // Create the B2AGG note + let b2agg_note = B2AggNote::create( + destination_network, + eth_address, + assets, + bridge_account.id(), + sender_account.id(), + 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()?; + + // ATTEMPT TO CONSUME B2AGG NOTE WITH MALICIOUS ACCOUNT (SHOULD FAIL) + // -------------------------------------------------------------------------------------------- + let result = mock_chain + .build_tx_context(malicious_account.id(), &[], &[b2agg_note])? + .build()? + .execute() + .await; + + assert_transaction_executor_error!(result, ERR_B2AGG_TARGET_ACCOUNT_MISMATCH); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 06c35ad6ee..6b2f973ae9 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -1,9 +1,9 @@ -use miden_agglayer::{ExitRoot, create_existing_bridge_account, create_update_ger_note}; +use miden_agglayer::{ExitRoot, UpdateGerNote, create_existing_bridge_account}; use miden_protocol::Word; use miden_protocol::account::StorageSlotName; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; -use miden_testing::MockChain; +use miden_testing::{Auth, MockChain}; #[tokio::test] async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { @@ -15,7 +15,12 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { let bridge_account = create_existing_bridge_account(bridge_seed); builder.add_account(bridge_account.clone())?; - // CREATE UPDATE_GER NOTE WITH 8 STORAGE ITEMS + // 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) // -------------------------------------------------------------------------------------------- let ger_bytes: [u8; 32] = [ @@ -24,7 +29,8 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { 0x77, 0x88, ]; let ger = ExitRoot::from(ger_bytes); - let update_ger_note = create_update_ger_note(ger, bridge_account.id(), builder.rng_mut())?; + let update_ger_note = + UpdateGerNote::create(ger, user_account.id(), bridge_account.id(), builder.rng_mut())?; builder.add_output_note(OutputNote::Full(update_ger_note.clone())); let mock_chain = builder.build()?;