From 46143c8f0875fdd51f6a7c5d659cb23da1c29c02 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 8 Jan 2026 19:14:48 -0500 Subject: [PATCH 01/37] feat: add Solidity<>Miden address type conversion functions --- Cargo.lock | 2 + .../asm/bridge/crypto_utils.masm | 2 +- .../asm/bridge/eth_address.masm | 97 +++++ crates/miden-agglayer/src/errors/agglayer.rs | 12 + crates/miden-agglayer/src/eth_address.rs | 133 ++++++ crates/miden-agglayer/src/lib.rs | 94 +--- crates/miden-agglayer/src/utils.rs | 412 +++++++++--------- crates/miden-testing/Cargo.toml | 2 + .../tests/agglayer/bridge_out.rs | 11 +- crates/miden-testing/tests/agglayer/mod.rs | 1 + .../solidity_miden_address_conversion.rs | 217 +++++++++ 11 files changed, 683 insertions(+), 300 deletions(-) create mode 100644 crates/miden-agglayer/asm/bridge/eth_address.masm create mode 100644 crates/miden-agglayer/src/eth_address.rs create mode 100644 crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs diff --git a/Cargo.lock b/Cargo.lock index 0ab6b7af3c..1ec7360d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1753,11 +1753,13 @@ version = "0.13.0" dependencies = [ "anyhow", "assert_matches", + "hex", "itertools 0.14.0", "miden-agglayer", "miden-assembly", "miden-block-prover", "miden-core-lib", + "miden-crypto", "miden-processor", "miden-protocol", "miden-standards", diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index f0020785d1..75b2cb2151 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -1,4 +1,5 @@ use miden::core::crypto::hashes::keccak256 +use miden::core::word #! Given the leaf data returns the leaf value. #! @@ -80,4 +81,3 @@ pub proc verify_claim_proof dropw dropw dropw dropw push.1 end - diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm new file mode 100644 index 0000000000..c1ba108a28 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -0,0 +1,97 @@ +use miden::core::crypto::hashes::keccak256 +use miden::core::word + +# CONSTANTS +# ================================================================================================= + +const U32_MAX=4294967295 +const TWO_POW_32=4294967296 + +const ERR_NOT_U32="address limb is not u32" +const ERR_ADDR0_NONZERO="first 4 bytes (addr0) must be zero" +const ERR_PREFIX_OUT_OF_FIELD="prefix would wrap field modulus" +const ERR_SUFFIX_OUT_OF_FIELD="suffix would wrap field modulus" + + +# ETHEREUM ADDRESS PROCEDURES +# ================================================================================================= + +#! Hashes an Ethereum address (address[5] type) using Keccak256. +#! +#! Inputs: [addr0, addr1, addr2, addr3, addr4] +#! Outputs: [DIGEST_U32[8]] +#! +#! Invocation: exec +pub proc account_id_to_ethereum_hash + mem_store.0 + mem_store.1 + mem_store.2 + mem_store.3 + mem_store.4 + + push.20.0 + exec.keccak256::hash_bytes + # Stack: [DIGEST_U32[8]] +end + +#! Converts an Ethereum address (address[5] type) back into an AccountId [prefix, suffix] type. +#! +#! The Ethereum address is represented as 5 u32 felts (20 bytes total) in big-endian format. +#! The first 4 bytes must be zero for a valid AccountId conversion. +#! The remaining 16 bytes are converted into two u64 values (prefix and suffix). +#! +#! Inputs: [addr0, addr1, addr2, addr3, addr4] +#! Outputs: [prefix, suffix] +#! +#! Where: +#! - addr0..addr4 are u32 felts (big-endian) representing the 20-byte Ethereum address +#! - Each addr[i] represents 4 bytes: addr0=bytes[0..3], addr1=bytes[4..7], etc. +#! - prefix is the first u64 from bytes[4..11] = (addr1 << 32) | addr2 +#! - suffix is the second u64 from bytes[12..19] = (addr3 << 32) | addr4 +#! +#! Note: This procedure ensures the packed u64 values don't overflow the field modulus +#! p = 2^64 - 2^32 + 1 by checking that if the high 32 bits are 0xFFFFFFFF, +#! then the low 32 bits must be 0. +#! +#! Invocation: exec +pub proc ethereum_address_to_account_id + # --- addr0 must be 0 --- + u32assert.err=ERR_NOT_U32 + dup eq.0 assert.err=ERR_ADDR0_NONZERO + drop + # => [addr1, addr2, addr3, addr4] + + # --- validate u32 limbs (optional but nice) --- + u32assert.err=ERR_NOT_U32 # addr1 + dup.1 u32assert.err=ERR_NOT_U32 drop # addr2 + dup.2 u32assert.err=ERR_NOT_U32 drop # addr3 + dup.3 u32assert.err=ERR_NOT_U32 drop # addr4 + # => [addr1, addr2, addr3, addr4] + + # --- prefix: (addr1 << 32) | addr2 --- + dup push.U32_MAX eq + if.true + dup.1 eq.0 assert.err=ERR_PREFIX_OUT_OF_FIELD + end + + push.TWO_POW_32 mul + add + # => [prefix, addr3, addr4] + + # --- suffix: (addr3 << 32) | addr4 --- + swap + # => [addr3, prefix, addr4] + + dup push.U32_MAX eq + if.true + dup.2 eq.0 assert.err=ERR_SUFFIX_OUT_OF_FIELD + end + + push.TWO_POW_32 mul + movup.2 + add + # => [suffix, prefix] + + swap + # => [prefix, suffix] +end diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 8f70572329..947fa6f771 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -9,6 +9,9 @@ use miden_protocol::errors::MasmError; // AGGLAYER ERRORS // ================================================================================================ +/// Error Message: "first 4 bytes (addr0) must be zero" +pub const ERR_ADDR0_NONZERO: MasmError = MasmError::from_static_str("first 4 bytes (addr0) must be zero"); + /// 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: "B2AGG script expects exactly 6 note inputs" @@ -20,5 +23,14 @@ pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); +/// Error Message: "address limb is not u32" +pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); + +/// Error Message: "prefix would wrap field modulus" +pub const ERR_PREFIX_OUT_OF_FIELD: MasmError = MasmError::from_static_str("prefix would wrap field modulus"); + /// 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: "suffix would wrap field modulus" +pub const ERR_SUFFIX_OUT_OF_FIELD: MasmError = MasmError::from_static_str("suffix would wrap field modulus"); diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs new file mode 100644 index 0000000000..256ce4218d --- /dev/null +++ b/crates/miden-agglayer/src/eth_address.rs @@ -0,0 +1,133 @@ +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt; + +use miden_core::FieldElement; +use miden_protocol::Felt; +use miden_protocol::account::AccountId; + +use crate::utils::{ + AddrConvError, + account_id_to_ethereum_address, + bytes20_to_evm_hex, + ethereum_address_to_account_id, + evm_hex_to_bytes20, +}; + +// ================================================================================================ +// ETHEREUM ADDRESS +// ================================================================================================ + +/// Represents an Ethereum address (20 bytes). +/// +/// This type provides conversions between Ethereum addresses and Miden types such as +/// [`AccountId`] and field elements ([`Felt`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EthAddress([u8; 20]); + +impl EthAddress { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`EthAddress`] from a 20-byte array. + pub const fn new(bytes: [u8; 20]) -> Self { + Self(bytes) + } + + /// Creates an [`EthAddress`] from a hex string (with or without "0x" prefix). + /// + /// # Errors + /// + /// Returns an error if the hex string is invalid or not 40 characters (20 bytes). + pub fn from_hex(hex_str: &str) -> Result { + evm_hex_to_bytes20(hex_str).map(Self) + } + + /// Creates an [`EthAddress`] from an [`AccountId`]. + /// + /// The AccountId is converted to an Ethereum address using the embedded format where + /// the first 4 bytes are zero padding, followed by the prefix and suffix as u64 values + /// in big-endian format. + /// + /// # Errors + /// + /// Returns an error if the conversion fails (e.g., if the AccountId cannot be represented + /// as a valid Ethereum address). + pub fn from_account_id(account_id: AccountId) -> Result { + account_id_to_ethereum_address(account_id).map(Self) + } + + // CONVERSIONS + // -------------------------------------------------------------------------------------------- + + /// Returns the raw 20-byte array. + pub const fn as_bytes(&self) -> &[u8; 20] { + &self.0 + } + + /// Converts the address into a 20-byte array. + pub const fn into_bytes(self) -> [u8; 20] { + self.0 + } + + /// Converts the Ethereum address into a vector of 5 [`Felt`] values. + /// + /// Each felt represents 4 bytes of the address in big-endian format. + pub fn to_felts(&self) -> Vec { + let mut result = Vec::with_capacity(5); + for i in 0..5 { + let start = i * 4; + let chunk = &self.0[start..start + 4]; + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + result.push(Felt::new(value as u64)); + } + result + } + + /// Converts the Ethereum address into an array of 5 [`Felt`] values. + /// + /// Each felt represents 4 bytes of the address in big-endian format. + pub fn to_felt_array(&self) -> [Felt; 5] { + let mut result = [Felt::ZERO; 5]; + for (i, felt) in result.iter_mut().enumerate() { + let start = i * 4; + let chunk = &self.0[start..start + 4]; + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + *felt = Felt::new(value as u64); + } + result + } + + /// Converts the Ethereum address to an [`AccountId`]. + /// + /// # Errors + /// + /// Returns an error if the first 4 bytes are not zero or if the resulting + /// AccountId is invalid. + pub fn to_account_id(&self) -> Result { + ethereum_address_to_account_id(&self.0) + } + + /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). + pub fn to_hex(&self) -> String { + bytes20_to_evm_hex(self.0) + } +} + +impl fmt::Display for EthAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl From<[u8; 20]> for EthAddress { + fn from(bytes: [u8; 20]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 20] { + fn from(addr: EthAddress) -> Self { + addr.0 + } +} diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 65765d1482..ece094be12 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -37,9 +37,11 @@ use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_utils_sync::LazyLock; pub mod errors; +pub mod eth_address; pub mod utils; -use utils::{bytes32_to_felts, ethereum_address_to_felts}; +pub use eth_address::EthAddress; +use utils::bytes32_to_felts; // AGGLAYER NOTE SCRIPTS // ================================================================================================ @@ -423,7 +425,7 @@ pub fn create_claim_note(params: ClaimNoteParams<'_, R>) -> Result(params: ClaimNoteParams<'_, R>) -> Result [u8; 20] { - let mut address = [0u8; 20]; - - // Convert prefix and suffix to u64, then to bytes (big-endian) - let prefix_u64 = account_id.prefix().as_felt().as_int(); - let suffix_u64 = account_id.suffix().as_int(); - - let prefix_bytes = prefix_u64.to_be_bytes(); - let suffix_bytes = suffix_u64.to_be_bytes(); - - // Copy last 4 bytes from prefix (u32 portion) - address[0..4].copy_from_slice(&prefix_bytes[4..8]); - // Copy last 4 bytes from suffix (u32 portion) - address[4..8].copy_from_slice(&suffix_bytes[4..8]); - // Remaining 12 bytes stay as zeros - - address -} - -/// Converts a bytes20 EVM address into an AccountId. -/// -/// The conversion extracts the first 8 bytes as prefix (u32) and suffix (u32), -/// with the remaining bytes ignored (treated as zeros). -pub fn evm_address_to_account_id(address: &[u8; 20]) -> AccountId { - // Extract first 8 bytes and convert to u32 values - let mut prefix_bytes = [0u8; 8]; - let mut suffix_bytes = [0u8; 8]; - - // Copy first 4 bytes to prefix (pad with zeros) - prefix_bytes[4..8].copy_from_slice(&address[0..4]); - // Copy next 4 bytes to suffix (pad with zeros) - suffix_bytes[4..8].copy_from_slice(&address[4..8]); - - let prefix = u64::from_be_bytes(prefix_bytes); - let suffix = u64::from_be_bytes(suffix_bytes); - - // Create AccountId from the extracted values - // Note: This creates a basic account ID - in practice you might want to use - // a specific account type and storage mode - AccountId::new_unchecked([Felt::new(prefix), Felt::new(suffix)]) -} - -/// Converts an AccountId to a bytes20 address that will produce [prefix, suffix, 0, 0, 0] -/// when processed by ethereum_address_to_felts(). -/// -/// This function creates a 20-byte address where: -/// - Bytes 0-3: AccountId prefix as u32 (big-endian) -/// - Bytes 4-7: AccountId suffix as u32 (big-endian) -/// - Bytes 8-19: Zero padding -/// -/// When ethereum_address_to_felts() processes this address, it will extract: -/// - u32\[0\] from bytes 0-3: prefix -/// - u32\[1\] from bytes 4-7: suffix -/// - u32\[2\] from bytes 8-11: zeros -/// - u32\[3\] from bytes 12-15: zeros -/// - u32\[4\] from bytes 16-19: zeros -/// -/// This results in [prefix, suffix, 0, 0, 0] as desired. -pub fn account_id_to_destination_bytes(account_id: AccountId) -> [u8; 20] { - let mut address = [0u8; 20]; - - // Get prefix and suffix as u64 values, then convert to u32 - let prefix = account_id.prefix().as_felt().as_int() as u32; - let suffix = account_id.suffix().as_int() as u32; - - // Pack prefix into first 4 bytes (u32, big-endian) - address[0..4].copy_from_slice(&prefix.to_be_bytes()); - - // Pack suffix into next 4 bytes (u32, big-endian) - address[4..8].copy_from_slice(&suffix.to_be_bytes()); - - // Remaining 12 bytes stay as zeros - // This will result in [prefix, suffix, 0, 0, 0] when processed by ethereum_address_to_felts() - - address -} - // TESTING HELPERS // ================================================================================================ @@ -667,7 +585,9 @@ pub fn claim_note_test_inputs( let destination_network = Felt::new(2); // Convert AccountId to destination address bytes - let destination_address = account_id_to_destination_bytes(destination_account_id); + let destination_address = EthAddress::from_account_id(destination_account_id) + .expect("Valid AccountId should convert to EthAddress") + .into_bytes(); // Convert amount Felt to u256 array for agglayer let amount_u256 = [ diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs index 3b8cd683c2..515271642d 100644 --- a/crates/miden-agglayer/src/utils.rs +++ b/crates/miden-agglayer/src/utils.rs @@ -1,259 +1,259 @@ use alloc::string::String; use alloc::vec::Vec; +use core::fmt; +use miden_core::FieldElement; use miden_protocol::Felt; +use miden_protocol::account::AccountId; -/// Convert 8 Felt values (u32 limbs in little-endian order) to U256 bytes in little-endian format. +// ================================================================================================ +// ETHEREUM ADDRESS +// ================================================================================================ + +/// Represents an Ethereum address (20 bytes). /// -/// The input limbs are expected to be in little-endian order (least significant limb first). -/// This function converts them to a 32-byte array in little-endian format for compatibility -/// with Ethereum/EVM which expects U256 values as 32 bytes in little-endian format. -/// This ensures compatibility when bridging assets between Miden and Ethereum-based chains. -pub fn felts_to_u256_bytes(limbs: [Felt; 8]) -> [u8; 32] { - let mut bytes = [0u8; 32]; +/// This type provides conversions between Ethereum addresses and Miden types such as +/// [`AccountId`] and field elements ([`Felt`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct EthAddress([u8; 20]); + +impl EthAddress { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`EthAddress`] from a 20-byte array. + pub const fn new(bytes: [u8; 20]) -> Self { + Self(bytes) + } - for (i, limb) in limbs.iter().enumerate() { - let u32_value = limb.as_int() as u32; - let limb_bytes = u32_value.to_le_bytes(); - bytes[i * 4..(i + 1) * 4].copy_from_slice(&limb_bytes); + /// Creates an [`EthAddress`] from a hex string (with or without "0x" prefix). + /// + /// # Errors + /// + /// Returns an error if the hex string is invalid or not 40 characters (20 bytes). + pub fn from_hex(hex_str: &str) -> Result { + evm_hex_to_bytes20(hex_str).map(Self) } - bytes -} + /// Creates an [`EthAddress`] from an [`AccountId`]. + /// + /// The AccountId is converted to an Ethereum address using the embedded format where + /// the first 4 bytes are zero padding, followed by the prefix and suffix as u64 values + /// in big-endian format. + /// + /// # Errors + /// + /// Returns an error if the conversion fails (e.g., if the AccountId cannot be represented + /// as a valid Ethereum address). + pub fn from_account_id(account_id: AccountId) -> Result { + account_id_to_ethereum_address(account_id).map(Self) + } -/// Converts an Ethereum address (20 bytes) into a vector of 5 Felt values. -/// -/// An Ethereum address is 20 bytes, which we split into 5 u32 values (4 bytes each). -/// The address bytes are distributed as follows: -/// - u32\[0\]: bytes 0-3 -/// - u32\[1\]: bytes 4-7 -/// - u32\[2\]: bytes 8-11 -/// - u32\[3\]: bytes 12-15 -/// - u32\[4\]: bytes 16-19 -/// -/// # Arguments -/// * `address` - A 20-byte Ethereum address -/// -/// # Returns -/// A vector of 5 Felt values representing the address -/// -/// # Panics -/// Panics if the address is not exactly 20 bytes -pub fn ethereum_address_to_felts(address: &[u8; 20]) -> Vec { - let mut result = Vec::with_capacity(5); - - // Convert each 4-byte chunk to a u32 (big-endian) - for i in 0..5 { - let start = i * 4; - let chunk = &address[start..start + 4]; - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result.push(Felt::new(value as u64)); + // CONVERSIONS + // -------------------------------------------------------------------------------------------- + + /// Returns the raw 20-byte array. + pub const fn as_bytes(&self) -> &[u8; 20] { + &self.0 } - result -} + /// Converts the address into a 20-byte array. + pub const fn into_bytes(self) -> [u8; 20] { + self.0 + } -/// Converts a vector of 5 Felt values back into a 20-byte Ethereum address. -/// -/// # Arguments -/// * `felts` - A vector of 5 Felt values representing an Ethereum address -/// -/// # Returns -/// A Result containing a 20-byte Ethereum address array, or an error string -/// -/// # Errors -/// Returns an error if the vector doesn't contain exactly 5 felts -pub fn felts_to_ethereum_address(felts: &[Felt]) -> Result<[u8; 20], String> { - if felts.len() != 5 { - return Err(alloc::format!("Expected 5 felts for Ethereum address, got {}", felts.len())); + /// Converts the Ethereum address into a vector of 5 [`Felt`] values. + /// + /// Each felt represents 4 bytes of the address in big-endian format. + pub fn to_felts(&self) -> Vec { + self.0 + .chunks(4) + .map(|chunk| { + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + Felt::new(value as u64) + }) + .collect() } - let mut address = [0u8; 20]; + /// Converts the Ethereum address into an array of 5 [`Felt`] values. + /// + /// Each felt represents 4 bytes of the address in big-endian format. + pub fn to_felt_array(&self) -> [Felt; 5] { + let mut result = [Felt::ZERO; 5]; + for (i, chunk) in self.0.chunks(4).enumerate() { + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + result[i] = Felt::new(value as u64); + } + result + } - for (i, felt) in felts.iter().enumerate() { - let value = felt.as_int() as u32; - let bytes = value.to_be_bytes(); - let start = i * 4; - address[start..start + 4].copy_from_slice(&bytes); + /// Converts the Ethereum address to an [`AccountId`]. + /// + /// # Errors + /// + /// Returns an error if the first 4 bytes are not zero or if the resulting + /// AccountId is invalid. + pub fn to_account_id(&self) -> Result { + ethereum_address_to_account_id(&self.0) } - Ok(address) + /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). + pub fn to_hex(&self) -> String { + bytes20_to_evm_hex(self.0) + } } -/// Converts an Ethereum address string (with or without "0x" prefix) into a vector of 5 Felt -/// values. -/// -/// # Arguments -/// * `address_str` - A hex string representing an Ethereum address (40 hex chars, optionally -/// prefixed with "0x") -/// -/// # Returns -/// A Result containing a vector of 5 Felt values representing the address, or an error string -/// -/// # Errors -/// Returns an error if: -/// - The string is not a valid hex string -/// - The string does not represent exactly 20 bytes (40 hex characters) -pub fn ethereum_address_string_to_felts(address_str: &str) -> Result, String> { - // Remove "0x" prefix if present - let hex_str = address_str.strip_prefix("0x").unwrap_or(address_str); - - // Check length (should be 40 hex characters for 20 bytes) - if hex_str.len() != 40 { - return Err(alloc::format!( - "Invalid Ethereum address length: expected 40 hex characters, got {}", - hex_str.len() - )); +impl fmt::Display for EthAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) } +} - // Parse hex string to bytes - let mut address_bytes = [0u8; 20]; - for (i, chunk) in hex_str.as_bytes().chunks(2).enumerate() { - let hex_byte = core::str::from_utf8(chunk) - .map_err(|_| String::from("Invalid UTF-8 in address string"))?; - address_bytes[i] = u8::from_str_radix(hex_byte, 16) - .map_err(|_| alloc::format!("Invalid hex character in address: {}", hex_byte))?; +impl From<[u8; 20]> for EthAddress { + fn from(bytes: [u8; 20]) -> Self { + Self(bytes) } +} - Ok(ethereum_address_to_felts(&address_bytes)) +impl From for [u8; 20] { + fn from(addr: EthAddress) -> Self { + addr.0 + } } +// ================================================================================================ +// UTILITY FUNCTIONS +// ================================================================================================ + /// Converts a bytes32 value (32 bytes) into a vector of 8 Felt values. -/// -/// A bytes32 value is 32 bytes, which we split into 8 u32 values (4 bytes each). -/// The bytes are distributed as follows: -/// - u32\[0\]: bytes 0-3 -/// - u32\[1\]: bytes 4-7 -/// - u32\[2\]: bytes 8-11 -/// - u32\[3\]: bytes 12-15 -/// - u32\[4\]: bytes 16-19 -/// - u32\[5\]: bytes 20-23 -/// - u32\[6\]: bytes 24-27 -/// - u32\[7\]: bytes 28-31 -/// -/// # Arguments -/// * `bytes32` - A 32-byte value (e.g., hash, root) -/// -/// # Returns -/// A vector of 8 Felt values representing the bytes32 value -/// -/// # Panics -/// Panics if the input is not exactly 32 bytes pub fn bytes32_to_felts(bytes32: &[u8; 32]) -> Vec { - let mut result = Vec::with_capacity(8); - - // Convert each 4-byte chunk to a u32 (big-endian) - for i in 0..8 { - let start = i * 4; - let chunk = &bytes32[start..start + 4]; - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result.push(Felt::new(value as u64)); - } - - result + bytes32 + .chunks(4) + .map(|chunk| { + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + Felt::new(value as u64) + }) + .collect() } -/// Converts a vector of 8 Felt values back into a 32-byte array. -/// -/// # Arguments -/// * `felts` - A vector of 8 Felt values representing a bytes32 value -/// -/// # Returns -/// A Result containing a 32-byte array, or an error string -/// -/// # Errors -/// Returns an error if the vector doesn't contain exactly 8 felts -pub fn felts_to_bytes32(felts: &[Felt]) -> Result<[u8; 32], String> { - if felts.len() != 8 { - return Err(alloc::format!("Expected 8 felts for bytes32, got {}", felts.len())); +/// Convert 8 Felt values (u32 limbs in little-endian order) to U256 bytes in little-endian format. +pub fn felts_to_u256_bytes(limbs: [Felt; 8]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (i, limb) in limbs.iter().enumerate() { + let u32_value = limb.as_int() as u32; + let limb_bytes = u32_value.to_le_bytes(); + bytes[i * 4..(i + 1) * 4].copy_from_slice(&limb_bytes); } + bytes +} - let mut bytes32 = [0u8; 32]; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AddrConvError { + NonZeroWordPadding, + NonZeroBytePrefix, + InvalidHexLength, + InvalidHexChar(char), +} - for (i, felt) in felts.iter().enumerate() { - let value = felt.as_int() as u32; - let bytes = value.to_be_bytes(); - let start = i * 4; - bytes32[start..start + 4].copy_from_slice(&bytes); +impl fmt::Display for AddrConvError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) } - - Ok(bytes32) } -#[cfg(test)] -mod tests { - use alloc::vec; - - use super::*; +/// Convert `[u64; 5]` -> `[u8; 20]` (EVM address bytes). +/// Layout: 4 zero bytes prefix + word0(be) + word1(be) +pub fn u64x5_to_bytes20(words: [u64; 5]) -> Result<[u8; 20], AddrConvError> { + if words[2] != 0 || words[3] != 0 || words[4] != 0 { + return Err(AddrConvError::NonZeroWordPadding); + } - #[test] - fn test_ethereum_address_round_trip() { - // Test that converting from string to felts and back gives the same result - let original_address = "0x1234567890abcdef1122334455667788990011aa"; + let mut out = [0u8; 20]; + let w0 = words[0].to_be_bytes(); + let w1 = words[1].to_be_bytes(); - // Convert string to felts - let felts = ethereum_address_string_to_felts(original_address).unwrap(); + out[0..4].copy_from_slice(&[0, 0, 0, 0]); + out[4..12].copy_from_slice(&w0); + out[12..20].copy_from_slice(&w1); - // Convert felts back to bytes - let recovered_bytes = felts_to_ethereum_address(&felts).unwrap(); + Ok(out) +} - // Convert original string to bytes for comparison - let original_hex = original_address.strip_prefix("0x").unwrap(); - let mut expected_bytes = [0u8; 20]; - for (i, chunk) in original_hex.as_bytes().chunks(2).enumerate() { - let hex_byte = core::str::from_utf8(chunk).unwrap(); - expected_bytes[i] = u8::from_str_radix(hex_byte, 16).unwrap(); - } +/// Convert `[u8; 20]` -> EVM address hex string (lowercase, 0x-prefixed). +pub(crate) fn bytes20_to_evm_hex(bytes: [u8; 20]) -> String { + let mut s = String::with_capacity(42); + s.push_str("0x"); + for b in bytes { + s.push(nibble_to_hex(b >> 4)); + s.push(nibble_to_hex(b & 0x0f)); + } + s +} - // Assert they match - assert_eq!(recovered_bytes, expected_bytes); +fn nibble_to_hex(n: u8) -> char { + match n { + 0..=9 => (b'0' + n) as char, + 10..=15 => (b'a' + (n - 10)) as char, + _ => unreachable!(), } +} - #[test] - fn test_ethereum_address_to_felts_basic() { - let address: [u8; 20] = [ - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, - 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, - ]; - - let result = ethereum_address_to_felts(&address); - assert_eq!(result.len(), 5); - assert_eq!(result[0], Felt::new(0x12345678)); - assert_eq!(result[1], Felt::new(0x9abcdef0)); +/// Parse a `0x` hex address string into `[u8;20]`. +pub(crate) fn evm_hex_to_bytes20(s: &str) -> Result<[u8; 20], AddrConvError> { + let s = s.strip_prefix("0x").unwrap_or(s); + if s.len() != 40 { + return Err(AddrConvError::InvalidHexLength); } - #[test] - fn test_felts_to_ethereum_address_invalid_length() { - let felts = vec![Felt::new(1), Felt::new(2)]; // Only 2 felts - let result = felts_to_ethereum_address(&felts); - assert!(result.is_err()); + let mut out = [0u8; 20]; + let chars: alloc::vec::Vec = s.chars().collect(); + for i in 0..20 { + let hi = hex_val(chars[2 * i])?; + let lo = hex_val(chars[2 * i + 1])?; + out[i] = (hi << 4) | lo; } + Ok(out) +} - #[test] - fn test_ethereum_address_string_invalid_length() { - let address_str = "0x123456"; // Too short - let result = ethereum_address_string_to_felts(address_str); - assert!(result.is_err()); +fn hex_val(c: char) -> Result { + match c { + '0'..='9' => Ok((c as u8) - b'0'), + 'a'..='f' => Ok((c as u8) - b'a' + 10), + 'A'..='F' => Ok((c as u8) - b'A' + 10), + _ => Err(AddrConvError::InvalidHexChar(c)), } +} - #[test] - fn test_bytes32_to_felts_basic() { - let bytes32: [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 result = bytes32_to_felts(&bytes32); - assert_eq!(result.len(), 8); - assert_eq!(result[0], Felt::new(0x12345678)); - assert_eq!(result[1], Felt::new(0x9abcdef0)); +/// Convert `[u8; 20]` -> `[u64; 5]` by extracting the last 16 bytes. +/// Requires the first 4 bytes be zero. +pub fn bytes20_to_u64x5(bytes: [u8; 20]) -> Result<[u64; 5], AddrConvError> { + if bytes[0..4] != [0, 0, 0, 0] { + return Err(AddrConvError::NonZeroBytePrefix); } - #[test] - fn test_felts_to_bytes32_invalid_length() { - let felts = vec![Felt::new(1), Felt::new(2)]; // Only 2 felts - let result = felts_to_bytes32(&felts); - assert!(result.is_err()); + let w0 = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); + let w1 = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); + + Ok([w0, w1, 0, 0, 0]) +} + +// Helper functions used by EthAddress +pub(crate) fn ethereum_address_to_account_id( + address: &[u8; 20], +) -> Result { + let u64x5 = bytes20_to_u64x5(*address)?; + let felts = [Felt::new(u64x5[0]), Felt::new(u64x5[1])]; + + match AccountId::try_from(felts) { + Ok(account_id) => Ok(account_id), + Err(_) => Err(AddrConvError::NonZeroBytePrefix), } } + +pub(crate) fn account_id_to_ethereum_address( + account_id: AccountId, +) -> Result<[u8; 20], AddrConvError> { + let felts: [Felt; 2] = account_id.into(); + let u64x5 = [felts[0].as_int(), felts[1].as_int(), 0, 0, 0]; + u64x5_to_bytes20(u64x5) +} diff --git a/crates/miden-testing/Cargo.toml b/crates/miden-testing/Cargo.toml index bbcbb06991..af565ab386 100644 --- a/crates/miden-testing/Cargo.toml +++ b/crates/miden-testing/Cargo.toml @@ -39,6 +39,8 @@ winterfell = { version = "0.13" } [dev-dependencies] anyhow = { features = ["backtrace", "std"], workspace = true } assert_matches = { workspace = true } +hex = { version = "0.4" } +miden-crypto = { workspace = true } miden-protocol = { features = ["std"], workspace = true } primitive-types = { workspace = true } rstest = { workspace = true } diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index e5be8f3814..d7e9b29fc5 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,7 +1,6 @@ extern crate alloc; -use miden_agglayer::utils::ethereum_address_string_to_felts; -use miden_agglayer::{b2agg_script, bridge_out_component}; +use miden_agglayer::{EthAddress, b2agg_script, bridge_out_component}; use miden_protocol::account::{ Account, AccountId, @@ -83,8 +82,8 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { // destination_address: 20 bytes (Ethereum address) split into 5 u32 values let destination_network = Felt::new(1); // Example network ID let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let address_felts = - ethereum_address_string_to_felts(destination_address).expect("Valid Ethereum address"); + let eth_address = EthAddress::from_hex(destination_address).expect("Valid Ethereum address"); + let address_felts = eth_address.to_felts(); // Combine network ID and address felts into note inputs (6 felts total) let mut input_felts = vec![destination_network]; @@ -241,8 +240,8 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // Create note inputs with destination network and address let destination_network = Felt::new(1); let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let address_felts = - ethereum_address_string_to_felts(destination_address).expect("Valid Ethereum address"); + let eth_address = EthAddress::from_hex(destination_address).expect("Valid Ethereum address"); + let address_felts = eth_address.to_felts(); // Combine network ID and address felts into note inputs (6 felts total) let mut input_felts = vec![destination_network]; diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index b238560b86..2a6d344c67 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,3 +1,4 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; +mod solidity_miden_address_conversion; diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs new file mode 100644 index 0000000000..a137e158a9 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -0,0 +1,217 @@ +extern crate alloc; + +use alloc::sync::Arc; + +use miden_agglayer::{EthAddress, agglayer_library}; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_core_lib::handlers::keccak256::KeccakPreimage; +use miden_processor::fast::{ExecutionOutput, FastProcessor}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; +use miden_protocol::Felt; +use miden_protocol::account::AccountId; +use miden_protocol::address::NetworkId; +use miden_protocol::testing::account_id::{ + ACCOUNT_ID_PRIVATE_SENDER, + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + AccountIdBuilder, +}; +use miden_protocol::transaction::TransactionKernel; + +/// 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(); + + for (event_name, handler) in std_lib.handlers() { + host.register_handler(event_name, handler)?; + } + + 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 +} + +#[test] +fn test_account_id_to_ethereum_roundtrip() { + let original_account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let eth_address = EthAddress::from_account_id(original_account_id).unwrap(); + let recovered_account_id = eth_address.to_account_id().unwrap(); + assert_eq!(original_account_id, recovered_account_id); +} + +#[test] +fn test_bech32_to_ethereum_roundtrip() { + let test_addresses = [ + "mtst1azcw08rget79fqp8ymr0zqkv5v5lj466", + "mtst1arxmxavamh7lqyp79mexktt4vgxv40mp", + "mtst1ar2phe0pa0ln75plsczxr8ryws4s8zyp", + ]; + + for bech32_address in test_addresses { + let (network_id, account_id) = AccountId::from_bech32(bech32_address).unwrap(); + let eth_address = EthAddress::from_account_id(account_id).unwrap(); + let recovered_account_id = eth_address.to_account_id().unwrap(); + let recovered_bech32 = recovered_account_id.to_bech32(network_id); + + assert_eq!(account_id, recovered_account_id); + assert_eq!(bech32_address, recovered_bech32); + } +} + +#[test] +fn test_random_bech32_to_ethereum_roundtrip() { + let mut rng = rand::rng(); + let network_id = NetworkId::Testnet; + + for _ in 0..3 { + let account_id = AccountIdBuilder::new().build_with_rng(&mut rng); + let bech32_address = account_id.to_bech32(network_id.clone()); + let eth_address = EthAddress::from_account_id(account_id).unwrap(); + let recovered_account_id = eth_address.to_account_id().unwrap(); + let recovered_bech32 = recovered_account_id.to_bech32(network_id.clone()); + + assert_eq!(account_id, recovered_account_id); + assert_eq!(bech32_address, recovered_bech32); + } +} + +#[tokio::test] +async fn test_address_bytes20_hash_in_masm() -> anyhow::Result<()> { + // Create account ID and convert to Ethereum address + let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?; + let eth_address = EthAddress::from_account_id(account_id) + .map_err(|e| anyhow::anyhow!("Failed to convert AccountId to Ethereum address: {:?}", e))?; + + // Convert to field elements for MASM + let address_felts = eth_address.to_felts(); + let addr_u32s: Vec = address_felts.iter().map(|f| f.as_int() as u32).collect(); + + // Compute expected Keccak256 hash using the same byte representation as MASM + let mut address_bytes = Vec::new(); + for &addr_u32 in &addr_u32s { + address_bytes.extend_from_slice(&addr_u32.to_le_bytes()); + } + address_bytes.truncate(20); + + let preimage = KeccakPreimage::new(address_bytes); + let expected_digest: Vec = preimage.digest().as_ref().iter().map(Felt::as_int).collect(); + + // Execute MASM procedure to compute the hash + let script_code = format!( + " + use miden::core::sys + use miden::agglayer::eth_address + + begin + push.{}.{}.{}.{}.{} + exec.eth_address::account_id_to_ethereum_hash + exec.sys::truncate_stack + end + ", + addr_u32s[4], addr_u32s[3], addr_u32s[2], addr_u32s[1], addr_u32s[0] + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_library()) + .unwrap() + .assemble_program(&script_code) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + let actual_digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); + + assert_eq!(actual_digest, expected_digest); + + Ok(()) +} + +#[tokio::test] +async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { + // Test with multiple account IDs to ensure correctness + let test_account_ids = [ + AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?, + AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?, + AccountIdBuilder::new().build_with_rng(&mut rand::rng()), + AccountIdBuilder::new().build_with_rng(&mut rand::rng()), + AccountIdBuilder::new().build_with_rng(&mut rand::rng()), + ]; + + for (idx, original_account_id) in test_account_ids.iter().enumerate() { + // 1) Convert AccountId to Ethereum address + let eth_address = EthAddress::from_account_id(*original_account_id).map_err(|e| { + anyhow::anyhow!( + "Test {}: Failed to convert AccountId to Ethereum address: {:?}", + idx, + e + ) + })?; + + // 2) Convert to address[5] field elements for MASM (big-endian u32 chunks) + let address_felts = eth_address.to_felts(); + let addr_u32s: Vec = address_felts.iter().map(|f| f.as_int() as u32).collect(); + + // 4) Get expected AccountId as [prefix, suffix] + let account_id_felts: [Felt; 2] = (*original_account_id).into(); + let expected_prefix = account_id_felts[0].as_int(); + let expected_suffix = account_id_felts[1].as_int(); + + // 5) Execute MASM procedure to convert address[5] back to AccountId + let script_code = format!( + " + use miden::core::sys + use miden::agglayer::eth_address + + begin + push.{}.{}.{}.{}.{} + exec.eth_address::ethereum_address_to_account_id + exec.sys::truncate_stack + end + ", + addr_u32s[4], addr_u32s[3], addr_u32s[2], addr_u32s[1], addr_u32s[0] + ); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_library()) + .unwrap() + .assemble_program(&script_code) + .unwrap(); + + let exec_output = execute_program_with_default_host(program).await?; + + // Stack should contain [prefix, suffix, ...] + let actual_prefix = exec_output.stack[0].as_int(); + let actual_suffix = exec_output.stack[1].as_int(); + + assert_eq!(actual_prefix, expected_prefix, "Test {}: Prefix mismatch", idx); + assert_eq!(actual_suffix, expected_suffix, "Test {}: Suffix mismatch", idx); + + // Verify we can reconstruct the AccountId + let reconstructed_account_id = + AccountId::try_from([Felt::new(actual_prefix), Felt::new(actual_suffix)])?; + + assert_eq!( + reconstructed_account_id, *original_account_id, + "Test {}: AccountId roundtrip failed", + idx + ); + } + + Ok(()) +} From 3c84da66c968b89d61fefdb3d4f2d45ece4aae36 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 8 Jan 2026 19:17:49 -0500 Subject: [PATCH 02/37] fix: formatting --- crates/miden-agglayer/asm/bridge/crypto_utils.masm | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 75b2cb2151..7796c1f94f 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -1,5 +1,4 @@ use miden::core::crypto::hashes::keccak256 -use miden::core::word #! Given the leaf data returns the leaf value. #! From c71d9df93c1c71a8a9f355e0b93881343176632d Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 8 Jan 2026 19:58:20 -0500 Subject: [PATCH 03/37] refactor: rm unnecessary indirection --- crates/miden-agglayer/src/eth_address.rs | 106 +++++++++-- crates/miden-agglayer/src/utils.rs | 230 ----------------------- 2 files changed, 95 insertions(+), 241 deletions(-) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index 256ce4218d..74b99a6282 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -6,13 +6,19 @@ use miden_core::FieldElement; use miden_protocol::Felt; use miden_protocol::account::AccountId; -use crate::utils::{ - AddrConvError, - account_id_to_ethereum_address, - bytes20_to_evm_hex, - ethereum_address_to_account_id, - evm_hex_to_bytes20, -}; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AddrConvError { + NonZeroWordPadding, + NonZeroBytePrefix, + InvalidHexLength, + InvalidHexChar(char), +} + +impl fmt::Display for AddrConvError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} // ================================================================================================ // ETHEREUM ADDRESS @@ -40,7 +46,19 @@ impl EthAddress { /// /// Returns an error if the hex string is invalid or not 40 characters (20 bytes). pub fn from_hex(hex_str: &str) -> Result { - evm_hex_to_bytes20(hex_str).map(Self) + let s = hex_str.strip_prefix("0x").unwrap_or(hex_str); + if s.len() != 40 { + return Err(AddrConvError::InvalidHexLength); + } + + let mut out = [0u8; 20]; + let chars: alloc::vec::Vec = s.chars().collect(); + for i in 0..20 { + let hi = Self::hex_val(chars[2 * i])?; + let lo = Self::hex_val(chars[2 * i + 1])?; + out[i] = (hi << 4) | lo; + } + Ok(Self(out)) } /// Creates an [`EthAddress`] from an [`AccountId`]. @@ -54,7 +72,10 @@ impl EthAddress { /// Returns an error if the conversion fails (e.g., if the AccountId cannot be represented /// as a valid Ethereum address). pub fn from_account_id(account_id: AccountId) -> Result { - account_id_to_ethereum_address(account_id).map(Self) + let felts: [Felt; 2] = account_id.into(); + let u64x5 = [felts[0].as_int(), felts[1].as_int(), 0, 0, 0]; + let bytes = Self::u64x5_to_bytes20(u64x5)?; + Ok(Self(bytes)) } // CONVERSIONS @@ -105,12 +126,75 @@ impl EthAddress { /// Returns an error if the first 4 bytes are not zero or if the resulting /// AccountId is invalid. pub fn to_account_id(&self) -> Result { - ethereum_address_to_account_id(&self.0) + let u64x5 = Self::bytes20_to_u64x5(self.0)?; + let felts = [Felt::new(u64x5[0]), Felt::new(u64x5[1])]; + + match AccountId::try_from(felts) { + Ok(account_id) => Ok(account_id), + Err(_) => Err(AddrConvError::NonZeroBytePrefix), + } } /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). pub fn to_hex(&self) -> String { - bytes20_to_evm_hex(self.0) + let mut s = String::with_capacity(42); + s.push_str("0x"); + for b in self.0 { + s.push(Self::nibble_to_hex(b >> 4)); + s.push(Self::nibble_to_hex(b & 0x0f)); + } + s + } + + // HELPER FUNCTIONS + // -------------------------------------------------------------------------------------------- + + fn hex_val(c: char) -> Result { + match c { + '0'..='9' => Ok((c as u8) - b'0'), + 'a'..='f' => Ok((c as u8) - b'a' + 10), + 'A'..='F' => Ok((c as u8) - b'A' + 10), + _ => Err(AddrConvError::InvalidHexChar(c)), + } + } + + fn nibble_to_hex(n: u8) -> char { + match n { + 0..=9 => (b'0' + n) as char, + 10..=15 => (b'a' + (n - 10)) as char, + _ => unreachable!(), + } + } + + /// Convert `[u64; 5]` -> `[u8; 20]` (EVM address bytes). + /// Layout: 4 zero bytes prefix + word0(be) + word1(be) + fn u64x5_to_bytes20(words: [u64; 5]) -> Result<[u8; 20], AddrConvError> { + if words[2] != 0 || words[3] != 0 || words[4] != 0 { + return Err(AddrConvError::NonZeroWordPadding); + } + + let mut out = [0u8; 20]; + let w0 = words[0].to_be_bytes(); + let w1 = words[1].to_be_bytes(); + + out[0..4].copy_from_slice(&[0, 0, 0, 0]); + out[4..12].copy_from_slice(&w0); + out[12..20].copy_from_slice(&w1); + + Ok(out) + } + + /// Convert `[u8; 20]` -> `[u64; 5]` by extracting the last 16 bytes. + /// Requires the first 4 bytes be zero. + fn bytes20_to_u64x5(bytes: [u8; 20]) -> Result<[u64; 5], AddrConvError> { + if bytes[0..4] != [0, 0, 0, 0] { + return Err(AddrConvError::NonZeroBytePrefix); + } + + let w0 = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); + let w1 = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); + + Ok([w0, w1, 0, 0, 0]) } } diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs index 515271642d..2e025da0ff 100644 --- a/crates/miden-agglayer/src/utils.rs +++ b/crates/miden-agglayer/src/utils.rs @@ -1,125 +1,6 @@ -use alloc::string::String; use alloc::vec::Vec; -use core::fmt; -use miden_core::FieldElement; use miden_protocol::Felt; -use miden_protocol::account::AccountId; - -// ================================================================================================ -// ETHEREUM ADDRESS -// ================================================================================================ - -/// Represents an Ethereum address (20 bytes). -/// -/// This type provides conversions between Ethereum addresses and Miden types such as -/// [`AccountId`] and field elements ([`Felt`]). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct EthAddress([u8; 20]); - -impl EthAddress { - // CONSTRUCTORS - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`EthAddress`] from a 20-byte array. - pub const fn new(bytes: [u8; 20]) -> Self { - Self(bytes) - } - - /// Creates an [`EthAddress`] from a hex string (with or without "0x" prefix). - /// - /// # Errors - /// - /// Returns an error if the hex string is invalid or not 40 characters (20 bytes). - pub fn from_hex(hex_str: &str) -> Result { - evm_hex_to_bytes20(hex_str).map(Self) - } - - /// Creates an [`EthAddress`] from an [`AccountId`]. - /// - /// The AccountId is converted to an Ethereum address using the embedded format where - /// the first 4 bytes are zero padding, followed by the prefix and suffix as u64 values - /// in big-endian format. - /// - /// # Errors - /// - /// Returns an error if the conversion fails (e.g., if the AccountId cannot be represented - /// as a valid Ethereum address). - pub fn from_account_id(account_id: AccountId) -> Result { - account_id_to_ethereum_address(account_id).map(Self) - } - - // CONVERSIONS - // -------------------------------------------------------------------------------------------- - - /// Returns the raw 20-byte array. - pub const fn as_bytes(&self) -> &[u8; 20] { - &self.0 - } - - /// Converts the address into a 20-byte array. - pub const fn into_bytes(self) -> [u8; 20] { - self.0 - } - - /// Converts the Ethereum address into a vector of 5 [`Felt`] values. - /// - /// Each felt represents 4 bytes of the address in big-endian format. - pub fn to_felts(&self) -> Vec { - self.0 - .chunks(4) - .map(|chunk| { - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - Felt::new(value as u64) - }) - .collect() - } - - /// Converts the Ethereum address into an array of 5 [`Felt`] values. - /// - /// Each felt represents 4 bytes of the address in big-endian format. - pub fn to_felt_array(&self) -> [Felt; 5] { - let mut result = [Felt::ZERO; 5]; - for (i, chunk) in self.0.chunks(4).enumerate() { - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result[i] = Felt::new(value as u64); - } - result - } - - /// Converts the Ethereum address to an [`AccountId`]. - /// - /// # Errors - /// - /// Returns an error if the first 4 bytes are not zero or if the resulting - /// AccountId is invalid. - pub fn to_account_id(&self) -> Result { - ethereum_address_to_account_id(&self.0) - } - - /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). - pub fn to_hex(&self) -> String { - bytes20_to_evm_hex(self.0) - } -} - -impl fmt::Display for EthAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -impl From<[u8; 20]> for EthAddress { - fn from(bytes: [u8; 20]) -> Self { - Self(bytes) - } -} - -impl From for [u8; 20] { - fn from(addr: EthAddress) -> Self { - addr.0 - } -} // ================================================================================================ // UTILITY FUNCTIONS @@ -146,114 +27,3 @@ pub fn felts_to_u256_bytes(limbs: [Felt; 8]) -> [u8; 32] { } bytes } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AddrConvError { - NonZeroWordPadding, - NonZeroBytePrefix, - InvalidHexLength, - InvalidHexChar(char), -} - -impl fmt::Display for AddrConvError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -/// Convert `[u64; 5]` -> `[u8; 20]` (EVM address bytes). -/// Layout: 4 zero bytes prefix + word0(be) + word1(be) -pub fn u64x5_to_bytes20(words: [u64; 5]) -> Result<[u8; 20], AddrConvError> { - if words[2] != 0 || words[3] != 0 || words[4] != 0 { - return Err(AddrConvError::NonZeroWordPadding); - } - - let mut out = [0u8; 20]; - let w0 = words[0].to_be_bytes(); - let w1 = words[1].to_be_bytes(); - - out[0..4].copy_from_slice(&[0, 0, 0, 0]); - out[4..12].copy_from_slice(&w0); - out[12..20].copy_from_slice(&w1); - - Ok(out) -} - -/// Convert `[u8; 20]` -> EVM address hex string (lowercase, 0x-prefixed). -pub(crate) fn bytes20_to_evm_hex(bytes: [u8; 20]) -> String { - let mut s = String::with_capacity(42); - s.push_str("0x"); - for b in bytes { - s.push(nibble_to_hex(b >> 4)); - s.push(nibble_to_hex(b & 0x0f)); - } - s -} - -fn nibble_to_hex(n: u8) -> char { - match n { - 0..=9 => (b'0' + n) as char, - 10..=15 => (b'a' + (n - 10)) as char, - _ => unreachable!(), - } -} - -/// Parse a `0x` hex address string into `[u8;20]`. -pub(crate) fn evm_hex_to_bytes20(s: &str) -> Result<[u8; 20], AddrConvError> { - let s = s.strip_prefix("0x").unwrap_or(s); - if s.len() != 40 { - return Err(AddrConvError::InvalidHexLength); - } - - let mut out = [0u8; 20]; - let chars: alloc::vec::Vec = s.chars().collect(); - for i in 0..20 { - let hi = hex_val(chars[2 * i])?; - let lo = hex_val(chars[2 * i + 1])?; - out[i] = (hi << 4) | lo; - } - Ok(out) -} - -fn hex_val(c: char) -> Result { - match c { - '0'..='9' => Ok((c as u8) - b'0'), - 'a'..='f' => Ok((c as u8) - b'a' + 10), - 'A'..='F' => Ok((c as u8) - b'A' + 10), - _ => Err(AddrConvError::InvalidHexChar(c)), - } -} - -/// Convert `[u8; 20]` -> `[u64; 5]` by extracting the last 16 bytes. -/// Requires the first 4 bytes be zero. -pub fn bytes20_to_u64x5(bytes: [u8; 20]) -> Result<[u64; 5], AddrConvError> { - if bytes[0..4] != [0, 0, 0, 0] { - return Err(AddrConvError::NonZeroBytePrefix); - } - - let w0 = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); - let w1 = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); - - Ok([w0, w1, 0, 0, 0]) -} - -// Helper functions used by EthAddress -pub(crate) fn ethereum_address_to_account_id( - address: &[u8; 20], -) -> Result { - let u64x5 = bytes20_to_u64x5(*address)?; - let felts = [Felt::new(u64x5[0]), Felt::new(u64x5[1])]; - - match AccountId::try_from(felts) { - Ok(account_id) => Ok(account_id), - Err(_) => Err(AddrConvError::NonZeroBytePrefix), - } -} - -pub(crate) fn account_id_to_ethereum_address( - account_id: AccountId, -) -> Result<[u8; 20], AddrConvError> { - let felts: [Felt; 2] = account_id.into(); - let u64x5 = [felts[0].as_int(), felts[1].as_int(), 0, 0, 0]; - u64x5_to_bytes20(u64x5) -} From 779ab246cf7a61f79edf6bf2c0dba151bc6096b0 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 8 Jan 2026 21:27:11 -0500 Subject: [PATCH 04/37] refactor: use crypto util functions --- crates/miden-agglayer/src/eth_address.rs | 45 ++++++++---------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index 74b99a6282..c84c1cb91d 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -5,6 +5,7 @@ use core::fmt; use miden_core::FieldElement; use miden_protocol::Felt; use miden_protocol::account::AccountId; +use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AddrConvError { @@ -12,11 +13,21 @@ pub enum AddrConvError { NonZeroBytePrefix, InvalidHexLength, InvalidHexChar(char), + HexParseError, } impl fmt::Display for AddrConvError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) + match self { + AddrConvError::HexParseError => write!(f, "Hex parse error"), + _ => write!(f, "{:?}", self), + } + } +} + +impl From for AddrConvError { + fn from(_err: HexParseError) -> Self { + AddrConvError::HexParseError } } @@ -51,14 +62,8 @@ impl EthAddress { return Err(AddrConvError::InvalidHexLength); } - let mut out = [0u8; 20]; - let chars: alloc::vec::Vec = s.chars().collect(); - for i in 0..20 { - let hi = Self::hex_val(chars[2 * i])?; - let lo = Self::hex_val(chars[2 * i + 1])?; - out[i] = (hi << 4) | lo; - } - Ok(Self(out)) + let bytes: [u8; 20] = hex_to_bytes(s)?; + Ok(Self(bytes)) } /// Creates an [`EthAddress`] from an [`AccountId`]. @@ -139,33 +144,13 @@ impl EthAddress { pub fn to_hex(&self) -> String { let mut s = String::with_capacity(42); s.push_str("0x"); - for b in self.0 { - s.push(Self::nibble_to_hex(b >> 4)); - s.push(Self::nibble_to_hex(b & 0x0f)); - } + s.push_str(&bytes_to_hex_string(self.0)); s } // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- - fn hex_val(c: char) -> Result { - match c { - '0'..='9' => Ok((c as u8) - b'0'), - 'a'..='f' => Ok((c as u8) - b'a' + 10), - 'A'..='F' => Ok((c as u8) - b'A' + 10), - _ => Err(AddrConvError::InvalidHexChar(c)), - } - } - - fn nibble_to_hex(n: u8) -> char { - match n { - 0..=9 => (b'0' + n) as char, - 10..=15 => (b'a' + (n - 10)) as char, - _ => unreachable!(), - } - } - /// Convert `[u64; 5]` -> `[u8; 20]` (EVM address bytes). /// Layout: 4 zero bytes prefix + word0(be) + word1(be) fn u64x5_to_bytes20(words: [u64; 5]) -> Result<[u8; 20], AddrConvError> { From a8238b6d9d645df1aadab62da2e514a7bf069b85 Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 12:03:10 -0500 Subject: [PATCH 05/37] refactor: implement suggestions & refactor --- crates/miden-agglayer/src/eth_address.rs | 71 +++++++------------ crates/miden-agglayer/src/lib.rs | 3 +- .../tests/agglayer/bridge_out.rs | 4 +- .../solidity_miden_address_conversion.rs | 4 +- 4 files changed, 31 insertions(+), 51 deletions(-) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index c84c1cb91d..b2674f5f8c 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -1,5 +1,5 @@ -use alloc::string::String; -use alloc::vec::Vec; +use alloc::format; +use alloc::string::{String, ToString}; use core::fmt; use miden_core::FieldElement; @@ -55,14 +55,20 @@ impl EthAddress { /// /// # Errors /// - /// Returns an error if the hex string is invalid or not 40 characters (20 bytes). + /// Returns an error if the hex string is invalid or the hex part is not exactly 40 characters. pub fn from_hex(hex_str: &str) -> Result { - let s = hex_str.strip_prefix("0x").unwrap_or(hex_str); - if s.len() != 40 { + let hex_part = hex_str.strip_prefix("0x").unwrap_or(hex_str); + if hex_part.len() != 40 { return Err(AddrConvError::InvalidHexLength); } - let bytes: [u8; 20] = hex_to_bytes(s)?; + let prefixed_hex = if hex_str.starts_with("0x") { + hex_str.to_string() + } else { + format!("0x{}", hex_str) + }; + + let bytes: [u8; 20] = hex_to_bytes(&prefixed_hex)?; Ok(Self(bytes)) } @@ -78,9 +84,17 @@ impl EthAddress { /// as a valid Ethereum address). pub fn from_account_id(account_id: AccountId) -> Result { let felts: [Felt; 2] = account_id.into(); - let u64x5 = [felts[0].as_int(), felts[1].as_int(), 0, 0, 0]; - let bytes = Self::u64x5_to_bytes20(u64x5)?; - Ok(Self(bytes)) + let words = [felts[0].as_int(), felts[1].as_int()]; + + let mut out = [0u8; 20]; + let w0 = words[0].to_be_bytes(); + let w1 = words[1].to_be_bytes(); + + out[0..4].copy_from_slice(&[0, 0, 0, 0]); + out[4..12].copy_from_slice(&w0); + out[12..20].copy_from_slice(&w1); + + Ok(Self(out)) } // CONVERSIONS @@ -96,24 +110,10 @@ impl EthAddress { self.0 } - /// Converts the Ethereum address into a vector of 5 [`Felt`] values. - /// - /// Each felt represents 4 bytes of the address in big-endian format. - pub fn to_felts(&self) -> Vec { - let mut result = Vec::with_capacity(5); - for i in 0..5 { - let start = i * 4; - let chunk = &self.0[start..start + 4]; - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result.push(Felt::new(value as u64)); - } - result - } - /// Converts the Ethereum address into an array of 5 [`Felt`] values. /// /// Each felt represents 4 bytes of the address in big-endian format. - pub fn to_felt_array(&self) -> [Felt; 5] { + pub fn to_elements(&self) -> [Felt; 5] { let mut result = [Felt::ZERO; 5]; for (i, felt) in result.iter_mut().enumerate() { let start = i * 4; @@ -142,33 +142,12 @@ impl EthAddress { /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). pub fn to_hex(&self) -> String { - let mut s = String::with_capacity(42); - s.push_str("0x"); - s.push_str(&bytes_to_hex_string(self.0)); - s + bytes_to_hex_string(self.0) } // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- - /// Convert `[u64; 5]` -> `[u8; 20]` (EVM address bytes). - /// Layout: 4 zero bytes prefix + word0(be) + word1(be) - fn u64x5_to_bytes20(words: [u64; 5]) -> Result<[u8; 20], AddrConvError> { - if words[2] != 0 || words[3] != 0 || words[4] != 0 { - return Err(AddrConvError::NonZeroWordPadding); - } - - let mut out = [0u8; 20]; - let w0 = words[0].to_be_bytes(); - let w1 = words[1].to_be_bytes(); - - out[0..4].copy_from_slice(&[0, 0, 0, 0]); - out[4..12].copy_from_slice(&w0); - out[12..20].copy_from_slice(&w1); - - Ok(out) - } - /// Convert `[u8; 20]` -> `[u64; 5]` by extracting the last 16 bytes. /// Requires the first 4 bytes be zero. fn bytes20_to_u64x5(bytes: [u8; 20]) -> Result<[u64; 5], AddrConvError> { diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index ece094be12..f637959404 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -425,7 +425,8 @@ pub fn create_claim_note(params: ClaimNoteParams<'_, R>) -> Result anyhow::Result<()> { let destination_network = Felt::new(1); // Example network ID let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = EthAddress::from_hex(destination_address).expect("Valid Ethereum address"); - let address_felts = eth_address.to_felts(); + let address_felts = eth_address.to_elements().to_vec(); // Combine network ID and address felts into note inputs (6 felts total) let mut input_felts = vec![destination_network]; @@ -241,7 +241,7 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let destination_network = Felt::new(1); let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = EthAddress::from_hex(destination_address).expect("Valid Ethereum address"); - let address_felts = eth_address.to_felts(); + let address_felts = eth_address.to_elements().to_vec(); // Combine network ID and address felts into note inputs (6 felts total) let mut input_felts = vec![destination_network]; diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index a137e158a9..f158a442ba 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -96,7 +96,7 @@ async fn test_address_bytes20_hash_in_masm() -> anyhow::Result<()> { .map_err(|e| anyhow::anyhow!("Failed to convert AccountId to Ethereum address: {:?}", e))?; // Convert to field elements for MASM - let address_felts = eth_address.to_felts(); + let address_felts = eth_address.to_elements().to_vec(); let addr_u32s: Vec = address_felts.iter().map(|f| f.as_int() as u32).collect(); // Compute expected Keccak256 hash using the same byte representation as MASM @@ -162,7 +162,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { })?; // 2) Convert to address[5] field elements for MASM (big-endian u32 chunks) - let address_felts = eth_address.to_felts(); + let address_felts = eth_address.to_elements().to_vec(); let addr_u32s: Vec = address_felts.iter().map(|f| f.as_int() as u32).collect(); // 4) Get expected AccountId as [prefix, suffix] From 5dd9c85681a41d66cb93b5f5857ebdd59419bcc7 Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 14:36:20 -0500 Subject: [PATCH 06/37] refactor: update logic & comments to little endian --- .../asm/bridge/eth_address.masm | 114 +++++++++++------- crates/miden-agglayer/src/errors/agglayer.rs | 13 +- crates/miden-agglayer/src/eth_address.rs | 23 ++-- .../solidity_miden_address_conversion.rs | 38 +++--- 4 files changed, 112 insertions(+), 76 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index c1ba108a28..79c402ae1c 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -8,9 +8,8 @@ const U32_MAX=4294967295 const TWO_POW_32=4294967296 const ERR_NOT_U32="address limb is not u32" -const ERR_ADDR0_NONZERO="first 4 bytes (addr0) must be zero" -const ERR_PREFIX_OUT_OF_FIELD="prefix would wrap field modulus" -const ERR_SUFFIX_OUT_OF_FIELD="suffix would wrap field modulus" +const ERR_ADDR4_NONZERO="most-significant 4 bytes (addr4) must be zero" +const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" # ETHEREUM ADDRESS PROCEDURES @@ -18,6 +17,8 @@ const ERR_SUFFIX_OUT_OF_FIELD="suffix would wrap field modulus" #! Hashes an Ethereum address (address[5] type) using Keccak256. #! +#! Address limb order: little-endian (addr0 is least-significant, addr4 is most-significant). +#! #! Inputs: [addr0, addr1, addr2, addr3, addr4] #! Outputs: [DIGEST_U32[8]] #! @@ -34,62 +35,85 @@ pub proc account_id_to_ethereum_hash # Stack: [DIGEST_U32[8]] end +#! Builds a single felt from two u32 limbs (little-endian limb order). +#! i.e. felt = lo + (hi << 32) +#! +#! Inputs: [lo, hi] +#! Outputs: [felt] +proc build_felt + # --- validate u32 limbs --- + u32assert.err=ERR_NOT_U32 # lo + dup.1 u32assert.err=ERR_NOT_U32 drop # hi + # => [lo, hi] + + # keep copies for the overflow check + dup.1 dup.1 + # => [lo, hi, lo, hi] + + # felt = (hi * 2^32) + lo + swap + push.TWO_POW_32 mul + add + # => [felt, lo, hi] + + # ensure no reduction mod p happened: + # split felt back into (hi, lo) and compare to inputs + dup u32split + # => [hi2, lo2, felt, lo, hi] + + movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD + # => [lo2, felt, lo] + + movup.2 assert_eq.err=ERR_FELT_OUT_OF_FIELD + # => [felt] +end + #! Converts an Ethereum address (address[5] type) back into an AccountId [prefix, suffix] type. #! -#! The Ethereum address is represented as 5 u32 felts (20 bytes total) in big-endian format. -#! The first 4 bytes must be zero for a valid AccountId conversion. -#! The remaining 16 bytes are converted into two u64 values (prefix and suffix). +#! The Ethereum address is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*: +#! addr0 = bytes[16..19] (least-significant 4 bytes) +#! addr1 = bytes[12..15] +#! addr2 = bytes[ 8..11] +#! addr3 = bytes[ 4.. 7] +#! addr4 = bytes[ 0.. 3] (most-significant 4 bytes) +#! +#! The most-significant 4 bytes must be zero for a valid AccountId conversion (addr4 == 0). +#! The remaining 16 bytes are converted into two u64 values: +#! prefix = (addr3 << 32) | addr2 # bytes[4..11] +#! suffix = (addr1 << 32) | addr0 # bytes[12..19] #! #! Inputs: [addr0, addr1, addr2, addr3, addr4] #! Outputs: [prefix, suffix] #! -#! Where: -#! - addr0..addr4 are u32 felts (big-endian) representing the 20-byte Ethereum address -#! - Each addr[i] represents 4 bytes: addr0=bytes[0..3], addr1=bytes[4..7], etc. -#! - prefix is the first u64 from bytes[4..11] = (addr1 << 32) | addr2 -#! - suffix is the second u64 from bytes[12..19] = (addr3 << 32) | addr4 -#! -#! Note: This procedure ensures the packed u64 values don't overflow the field modulus -#! p = 2^64 - 2^32 + 1 by checking that if the high 32 bits are 0xFFFFFFFF, -#! then the low 32 bits must be 0. -#! #! Invocation: exec pub proc ethereum_address_to_account_id - # --- addr0 must be 0 --- + # --- addr4 must be 0 (most-significant limb) --- + movup.4 u32assert.err=ERR_NOT_U32 - dup eq.0 assert.err=ERR_ADDR0_NONZERO + dup eq.0 assert.err=ERR_ADDR4_NONZERO drop - # => [addr1, addr2, addr3, addr4] - - # --- validate u32 limbs (optional but nice) --- - u32assert.err=ERR_NOT_U32 # addr1 - dup.1 u32assert.err=ERR_NOT_U32 drop # addr2 - dup.2 u32assert.err=ERR_NOT_U32 drop # addr3 - dup.3 u32assert.err=ERR_NOT_U32 drop # addr4 - # => [addr1, addr2, addr3, addr4] - - # --- prefix: (addr1 << 32) | addr2 --- - dup push.U32_MAX eq - if.true - dup.1 eq.0 assert.err=ERR_PREFIX_OUT_OF_FIELD - end - - push.TWO_POW_32 mul - add - # => [prefix, addr3, addr4] + # => [addr0, addr1, addr2, addr3] - # --- suffix: (addr3 << 32) | addr4 --- + # --- prefix: (addr3 << 32) | addr2 --- + # need build_felt([lo, hi]) = [addr2, addr3] + movup.2 + # => [addr2, addr0, addr1, addr3] + movup.3 + # => [addr3, addr2, addr0, addr1] swap - # => [addr3, prefix, addr4] - - dup push.U32_MAX eq - if.true - dup.2 eq.0 assert.err=ERR_SUFFIX_OUT_OF_FIELD - end + # => [addr2, addr3, addr0, addr1] + exec.build_felt + # => [prefix, addr0, addr1] - push.TWO_POW_32 mul + # --- suffix: (addr1 << 32) | addr0 --- + # need build_felt([lo, hi]) = [addr0, addr1] + swap + # => [addr0, prefix, addr1] movup.2 - add + # => [addr1, addr0, prefix] + swap + # => [addr0, addr1, prefix] + exec.build_felt # => [suffix, prefix] swap diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 947fa6f771..efa9275dee 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -9,8 +9,8 @@ use miden_protocol::errors::MasmError; // AGGLAYER ERRORS // ================================================================================================ -/// Error Message: "first 4 bytes (addr0) must be zero" -pub const ERR_ADDR0_NONZERO: MasmError = MasmError::from_static_str("first 4 bytes (addr0) must be zero"); +/// 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 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"); @@ -20,17 +20,14 @@ pub const ERR_B2AGG_WRONG_NUMBER_OF_INPUTS: MasmError = MasmError::from_static_s /// Error Message: "CLAIM's target account address and transaction address do not match" pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM's target account address and transaction address do not match"); +/// Error Message: "combined u64 doesn't fit in field" +pub const ERR_FELT_OUT_OF_FIELD: MasmError = MasmError::from_static_str("combined u64 doesn't fit in field"); + /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); /// Error Message: "address limb is not u32" pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); -/// Error Message: "prefix would wrap field modulus" -pub const ERR_PREFIX_OUT_OF_FIELD: MasmError = MasmError::from_static_str("prefix would wrap field modulus"); - /// 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: "suffix would wrap field modulus" -pub const ERR_SUFFIX_OUT_OF_FIELD: MasmError = MasmError::from_static_str("suffix would wrap field modulus"); diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index b2674f5f8c..a3740fb586 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -112,7 +112,12 @@ impl EthAddress { /// Converts the Ethereum address into an array of 5 [`Felt`] values. /// - /// Each felt represents 4 bytes of the address in big-endian format. + /// Each felt represents 4 bytes of the address in big-endian format: + /// - addr0 = bytes[0..3] (most-significant 4 bytes) + /// - addr1 = bytes[4..7] + /// - addr2 = bytes[8..11] + /// - addr3 = bytes[12..15] + /// - addr4 = bytes[16..19] (least-significant 4 bytes) pub fn to_elements(&self) -> [Felt; 5] { let mut result = [Felt::ZERO; 5]; for (i, felt) in result.iter_mut().enumerate() { @@ -131,8 +136,8 @@ impl EthAddress { /// Returns an error if the first 4 bytes are not zero or if the resulting /// AccountId is invalid. pub fn to_account_id(&self) -> Result { - let u64x5 = Self::bytes20_to_u64x5(self.0)?; - let felts = [Felt::new(u64x5[0]), Felt::new(u64x5[1])]; + let (prefix, suffix) = Self::bytes20_to_prefix_suffix(self.0)?; + let felts = [Felt::new(prefix), Felt::new(suffix)]; match AccountId::try_from(felts) { Ok(account_id) => Ok(account_id), @@ -148,17 +153,19 @@ impl EthAddress { // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- - /// Convert `[u8; 20]` -> `[u64; 5]` by extracting the last 16 bytes. + /// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. /// Requires the first 4 bytes be zero. - fn bytes20_to_u64x5(bytes: [u8; 20]) -> Result<[u64; 5], AddrConvError> { + /// Returns prefix and suffix values that match the MASM little-endian implementation. + fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddrConvError> { if bytes[0..4] != [0, 0, 0, 0] { return Err(AddrConvError::NonZeroBytePrefix); } - let w0 = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); - let w1 = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); + // Extract prefix from bytes[4..12] and suffix from bytes[12..20] + let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); // (addr3 << 32) | addr2 + let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); // (addr1 << 32) | addr0 - Ok([w0, w1, 0, 0, 0]) + Ok((prefix, suffix)) } } diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index f158a442ba..63be69e80a 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -142,7 +142,6 @@ async fn test_address_bytes20_hash_in_masm() -> anyhow::Result<()> { #[tokio::test] async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { - // Test with multiple account IDs to ensure correctness let test_account_ids = [ AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?, AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?, @@ -152,27 +151,38 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { ]; for (idx, original_account_id) in test_account_ids.iter().enumerate() { - // 1) Convert AccountId to Ethereum address let eth_address = EthAddress::from_account_id(*original_account_id).map_err(|e| { anyhow::anyhow!( - "Test {}: Failed to convert AccountId to Ethereum address: {:?}", + "test {}: failed to convert AccountId to ethereum address: {:?}", idx, e ) })?; - // 2) Convert to address[5] field elements for MASM (big-endian u32 chunks) let address_felts = eth_address.to_elements().to_vec(); - let addr_u32s: Vec = address_felts.iter().map(|f| f.as_int() as u32).collect(); + let be: Vec = address_felts + .iter() + .map(|f| { + let val = f.as_int(); + assert!(val <= u32::MAX as u64, "felt value {} exceeds u32::MAX", val); + val as u32 + }) + .collect(); + + assert_eq!(be[0], 0, "test {}: expected msw limb (be[0]) to be zero", idx); + + let addr0 = be[4]; + let addr1 = be[3]; + let addr2 = be[2]; + let addr3 = be[1]; + let addr4 = be[0]; - // 4) Get expected AccountId as [prefix, suffix] let account_id_felts: [Felt; 2] = (*original_account_id).into(); let expected_prefix = account_id_felts[0].as_int(); let expected_suffix = account_id_felts[1].as_int(); - // 5) Execute MASM procedure to convert address[5] back to AccountId let script_code = format!( - " + r#" use miden::core::sys use miden::agglayer::eth_address @@ -181,8 +191,8 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { exec.eth_address::ethereum_address_to_account_id exec.sys::truncate_stack end - ", - addr_u32s[4], addr_u32s[3], addr_u32s[2], addr_u32s[1], addr_u32s[0] + "#, + addr4, addr3, addr2, addr1, addr0 ); let program = Assembler::new(Arc::new(DefaultSourceManager::default())) @@ -195,20 +205,18 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { let exec_output = execute_program_with_default_host(program).await?; - // Stack should contain [prefix, suffix, ...] let actual_prefix = exec_output.stack[0].as_int(); let actual_suffix = exec_output.stack[1].as_int(); - assert_eq!(actual_prefix, expected_prefix, "Test {}: Prefix mismatch", idx); - assert_eq!(actual_suffix, expected_suffix, "Test {}: Suffix mismatch", idx); + assert_eq!(actual_prefix, expected_prefix, "test {}: prefix mismatch", idx); + assert_eq!(actual_suffix, expected_suffix, "test {}: suffix mismatch", idx); - // Verify we can reconstruct the AccountId let reconstructed_account_id = AccountId::try_from([Felt::new(actual_prefix), Felt::new(actual_suffix)])?; assert_eq!( reconstructed_account_id, *original_account_id, - "Test {}: AccountId roundtrip failed", + "test {}: accountId roundtrip failed", idx ); } From 99bcee7a1924135cf215e8943384a501d69a33a7 Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:12:06 -0500 Subject: [PATCH 07/37] Update crates/miden-agglayer/src/utils.rs Co-authored-by: igamigo --- crates/miden-agglayer/src/utils.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs index 2e025da0ff..26ab5b0e48 100644 --- a/crates/miden-agglayer/src/utils.rs +++ b/crates/miden-agglayer/src/utils.rs @@ -2,7 +2,6 @@ use alloc::vec::Vec; use miden_protocol::Felt; -// ================================================================================================ // UTILITY FUNCTIONS // ================================================================================================ From 2d0a89a93df7c013e98271791cf1831674cd54f2 Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 16:31:33 -0500 Subject: [PATCH 08/37] refactor: improve EthAddress representation clarity and MASM alignment --- .../asm/bridge/eth_address.masm | 10 +- crates/miden-agglayer/src/eth_address.rs | 107 ++++++++++++------ crates/miden-agglayer/src/lib.rs | 4 +- .../solidity_miden_address_conversion.rs | 31 ++--- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index 79c402ae1c..fc52f7ca54 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -36,7 +36,9 @@ pub proc account_id_to_ethereum_hash end #! Builds a single felt from two u32 limbs (little-endian limb order). -#! i.e. felt = lo + (hi << 32) +#! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. +#! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping +#! through u32split and comparing the limbs. #! #! Inputs: [lo, hi] #! Outputs: [felt] @@ -78,10 +80,14 @@ end #! addr4 = bytes[ 0.. 3] (most-significant 4 bytes) #! #! The most-significant 4 bytes must be zero for a valid AccountId conversion (addr4 == 0). -#! The remaining 16 bytes are converted into two u64 values: +#! The remaining 16 bytes are treated as two 8-byte words (conceptual u64 values): #! prefix = (addr3 << 32) | addr2 # bytes[4..11] #! suffix = (addr1 << 32) | addr0 # bytes[12..19] #! +#! These 8-byte words are represented as field elements by packing two u32 limbs into a felt. +#! The packing is done via build_felt, which validates limbs are u32 and checks the packed value +#! did not reduce mod p (i.e. the word fits in the field). +#! #! Inputs: [addr0, addr1, addr2, addr3, addr4] #! Outputs: [prefix, suffix] #! diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index a3740fb586..3f57daa8ba 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -14,13 +14,24 @@ pub enum AddrConvError { InvalidHexLength, InvalidHexChar(char), HexParseError, + FeltOutOfField, + InvalidAccountId, } impl fmt::Display for AddrConvError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - AddrConvError::HexParseError => write!(f, "Hex parse error"), - _ => write!(f, "{:?}", self), + AddrConvError::NonZeroWordPadding => write!(f, "non-zero word padding"), + AddrConvError::NonZeroBytePrefix => write!(f, "address has non-zero 4-byte prefix"), + AddrConvError::InvalidHexLength => { + write!(f, "invalid hex length (expected 40 hex chars)") + }, + AddrConvError::InvalidHexChar(c) => write!(f, "invalid hex character: {}", c), + AddrConvError::HexParseError => write!(f, "hex parse error"), + AddrConvError::FeltOutOfField => { + write!(f, "packed 64-bit word does not fit in the field") + }, + AddrConvError::InvalidAccountId => write!(f, "invalid AccountId"), } } } @@ -37,8 +48,22 @@ impl From for AddrConvError { /// Represents an Ethereum address (20 bytes). /// -/// This type provides conversions between Ethereum addresses and Miden types such as -/// [`AccountId`] and field elements ([`Felt`]). +/// # Representations used in this module +/// +/// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the +/// most-significant byte). +/// - MASM "address[5]" limbs: 5 x u32 limbs in *little-endian limb order*: +/// - addr0 = bytes[16..19] (least-significant 4 bytes) +/// - addr1 = bytes[12..15] +/// - addr2 = bytes[ 8..11] +/// - addr3 = bytes[ 4.. 7] +/// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) +/// - Embedded AccountId format: `0x00000000 || prefix(8) || suffix(8)`, where: +/// - prefix = (addr3 << 32) | addr2 = bytes[4..11] as a big-endian u64 +/// - suffix = (addr1 << 32) | addr0 = bytes[12..19] as a big-endian u64 +/// +/// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure +/// `Felt::new(u64)` does not reduce mod p (checked explicitly in `to_account_id`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EthAddress([u8; 20]); @@ -74,15 +99,9 @@ impl EthAddress { /// Creates an [`EthAddress`] from an [`AccountId`]. /// - /// The AccountId is converted to an Ethereum address using the embedded format where - /// the first 4 bytes are zero padding, followed by the prefix and suffix as u64 values - /// in big-endian format. - /// - /// # Errors - /// - /// Returns an error if the conversion fails (e.g., if the AccountId cannot be represented - /// as a valid Ethereum address). - pub fn from_account_id(account_id: AccountId) -> Result { + /// This conversion is infallible: an [`AccountId`] is two felts, and `as_int()` yields `u64` + /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). + pub fn from_account_id(account_id: AccountId) -> Self { let felts: [Felt; 2] = account_id.into(); let words = [felts[0].as_int(), felts[1].as_int()]; @@ -94,7 +113,7 @@ impl EthAddress { out[4..12].copy_from_slice(&w0); out[12..20].copy_from_slice(&w1); - Ok(Self(out)) + Self(out) } // CONVERSIONS @@ -112,20 +131,25 @@ impl EthAddress { /// Converts the Ethereum address into an array of 5 [`Felt`] values. /// - /// Each felt represents 4 bytes of the address in big-endian format: - /// - addr0 = bytes[0..3] (most-significant 4 bytes) - /// - addr1 = bytes[4..7] - /// - addr2 = bytes[8..11] - /// - addr3 = bytes[12..15] - /// - addr4 = bytes[16..19] (least-significant 4 bytes) + /// The returned order matches the MASM `address[5]` convention (*little-endian limb order*): + /// - addr0 = bytes[16..19] (least-significant 4 bytes) + /// - addr1 = bytes[12..15] + /// - addr2 = bytes[ 8..11] + /// - addr3 = bytes[ 4.. 7] + /// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) + /// + /// Each limb is interpreted as a big-endian `u32` and stored in a [`Felt`]. pub fn to_elements(&self) -> [Felt; 5] { let mut result = [Felt::ZERO; 5]; - for (i, felt) in result.iter_mut().enumerate() { - let start = i * 4; + + // i=0 -> bytes[16..20], i=4 -> bytes[0..4] + for i in 0..5 { + let start = (4 - i) * 4; let chunk = &self.0[start..start + 4]; let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - *felt = Felt::new(value as u64); + result[i] = Felt::new(value as u64); } + result } @@ -133,16 +157,26 @@ impl EthAddress { /// /// # Errors /// - /// Returns an error if the first 4 bytes are not zero or if the resulting - /// AccountId is invalid. + /// Returns an error if: + /// - the first 4 bytes are not zero (not in the embedded AccountId format), + /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p, + /// - or the resulting felts do not form a valid [`AccountId`]. pub fn to_account_id(&self) -> Result { let (prefix, suffix) = Self::bytes20_to_prefix_suffix(self.0)?; - let felts = [Felt::new(prefix), Felt::new(suffix)]; - match AccountId::try_from(felts) { - Ok(account_id) => Ok(account_id), - Err(_) => Err(AddrConvError::NonZeroBytePrefix), + // `Felt::new(u64)` may reduce mod p for some u64 values. Mirror the MASM `build_felt` + // safety: construct the felt, then require round-trip equality. + let prefix_felt = Felt::new(prefix); + if prefix_felt.as_int() != prefix { + return Err(AddrConvError::FeltOutOfField); + } + + let suffix_felt = Felt::new(suffix); + if suffix_felt.as_int() != suffix { + return Err(AddrConvError::FeltOutOfField); } + + AccountId::try_from([prefix_felt, suffix_felt]).map_err(|_| AddrConvError::InvalidAccountId) } /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). @@ -155,15 +189,16 @@ impl EthAddress { /// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. /// Requires the first 4 bytes be zero. - /// Returns prefix and suffix values that match the MASM little-endian implementation. + /// Returns prefix and suffix values that match the MASM little-endian limb implementation: + /// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2 + /// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0 fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddrConvError> { if bytes[0..4] != [0, 0, 0, 0] { return Err(AddrConvError::NonZeroBytePrefix); } - // Extract prefix from bytes[4..12] and suffix from bytes[12..20] - let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); // (addr3 << 32) | addr2 - let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); // (addr1 << 32) | addr0 + let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); + let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); Ok((prefix, suffix)) } @@ -181,6 +216,12 @@ impl From<[u8; 20]> for EthAddress { } } +impl From for EthAddress { + fn from(account_id: AccountId) -> Self { + EthAddress::from_account_id(account_id) + } +} + impl From for [u8; 20] { fn from(addr: EthAddress) -> Self { addr.0 diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index f637959404..b9b0b6f879 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -586,9 +586,7 @@ pub fn claim_note_test_inputs( let destination_network = Felt::new(2); // Convert AccountId to destination address bytes - let destination_address = EthAddress::from_account_id(destination_account_id) - .expect("Valid AccountId should convert to EthAddress") - .into_bytes(); + let destination_address = EthAddress::from_account_id(destination_account_id).into_bytes(); // Convert amount Felt to u256 array for agglayer let amount_u256 = [ diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 63be69e80a..0832c360db 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -47,7 +47,7 @@ async fn execute_program_with_default_host( #[test] fn test_account_id_to_ethereum_roundtrip() { let original_account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let eth_address = EthAddress::from_account_id(original_account_id).unwrap(); + let eth_address = EthAddress::from_account_id(original_account_id); let recovered_account_id = eth_address.to_account_id().unwrap(); assert_eq!(original_account_id, recovered_account_id); } @@ -62,7 +62,7 @@ fn test_bech32_to_ethereum_roundtrip() { for bech32_address in test_addresses { let (network_id, account_id) = AccountId::from_bech32(bech32_address).unwrap(); - let eth_address = EthAddress::from_account_id(account_id).unwrap(); + let eth_address = EthAddress::from_account_id(account_id); let recovered_account_id = eth_address.to_account_id().unwrap(); let recovered_bech32 = recovered_account_id.to_bech32(network_id); @@ -79,7 +79,7 @@ fn test_random_bech32_to_ethereum_roundtrip() { for _ in 0..3 { let account_id = AccountIdBuilder::new().build_with_rng(&mut rng); let bech32_address = account_id.to_bech32(network_id.clone()); - let eth_address = EthAddress::from_account_id(account_id).unwrap(); + let eth_address = EthAddress::from_account_id(account_id); let recovered_account_id = eth_address.to_account_id().unwrap(); let recovered_bech32 = recovered_account_id.to_bech32(network_id.clone()); @@ -92,8 +92,7 @@ fn test_random_bech32_to_ethereum_roundtrip() { async fn test_address_bytes20_hash_in_masm() -> anyhow::Result<()> { // Create account ID and convert to Ethereum address let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?; - let eth_address = EthAddress::from_account_id(account_id) - .map_err(|e| anyhow::anyhow!("Failed to convert AccountId to Ethereum address: {:?}", e))?; + let eth_address = EthAddress::from_account_id(account_id); // Convert to field elements for MASM let address_felts = eth_address.to_elements().to_vec(); @@ -151,16 +150,10 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { ]; for (idx, original_account_id) in test_account_ids.iter().enumerate() { - let eth_address = EthAddress::from_account_id(*original_account_id).map_err(|e| { - anyhow::anyhow!( - "test {}: failed to convert AccountId to ethereum address: {:?}", - idx, - e - ) - })?; + let eth_address = EthAddress::from_account_id(*original_account_id); let address_felts = eth_address.to_elements().to_vec(); - let be: Vec = address_felts + let le: Vec = address_felts .iter() .map(|f| { let val = f.as_int(); @@ -169,13 +162,13 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { }) .collect(); - assert_eq!(be[0], 0, "test {}: expected msw limb (be[0]) to be zero", idx); + assert_eq!(le[4], 0, "test {}: expected msw limb (le[4]) to be zero", idx); - let addr0 = be[4]; - let addr1 = be[3]; - let addr2 = be[2]; - let addr3 = be[1]; - let addr4 = be[0]; + let addr0 = le[0]; + let addr1 = le[1]; + let addr2 = le[2]; + let addr3 = le[3]; + let addr4 = le[4]; let account_id_felts: [Felt; 2] = (*original_account_id).into(); let expected_prefix = account_id_felts[0].as_int(); From 3d45e7f8b43ca822792442056988be722a0e2ebb Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 16:50:34 -0500 Subject: [PATCH 09/37] refactor: simplify ethereum_address_to_account_id proc --- .../asm/bridge/eth_address.masm | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index fc52f7ca54..b7e4bc1168 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -93,35 +93,19 @@ end #! #! Invocation: exec pub proc ethereum_address_to_account_id - # --- addr4 must be 0 (most-significant limb) --- + # addr4 must be 0 (most-significant limb) movup.4 u32assert.err=ERR_NOT_U32 dup eq.0 assert.err=ERR_ADDR4_NONZERO drop # => [addr0, addr1, addr2, addr3] - # --- prefix: (addr3 << 32) | addr2 --- - # need build_felt([lo, hi]) = [addr2, addr3] - movup.2 - # => [addr2, addr0, addr1, addr3] - movup.3 - # => [addr3, addr2, addr0, addr1] - swap - # => [addr2, addr3, addr0, addr1] exec.build_felt - # => [prefix, addr0, addr1] + # => [suffix, addr2, addr3] - # --- suffix: (addr1 << 32) | addr0 --- - # need build_felt([lo, hi]) = [addr0, addr1] - swap - # => [addr0, prefix, addr1] - movup.2 - # => [addr1, addr0, prefix] - swap - # => [addr0, addr1, prefix] - exec.build_felt - # => [suffix, prefix] + movdn.2 + # => [addr2, addr3, suffix] - swap + exec.build_felt # => [prefix, suffix] end From 43cbcf3162e2006bf08c1bf1f8a2eaf204cf7776 Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 16:58:57 -0500 Subject: [PATCH 10/37] fix: clippy --- .../asm/bridge/eth_address.masm | 20 ------- crates/miden-agglayer/src/eth_address.rs | 4 +- .../solidity_miden_address_conversion.rs | 52 ------------------- 3 files changed, 2 insertions(+), 74 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index b7e4bc1168..20ddd70670 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -15,26 +15,6 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" # ETHEREUM ADDRESS PROCEDURES # ================================================================================================= -#! Hashes an Ethereum address (address[5] type) using Keccak256. -#! -#! Address limb order: little-endian (addr0 is least-significant, addr4 is most-significant). -#! -#! Inputs: [addr0, addr1, addr2, addr3, addr4] -#! Outputs: [DIGEST_U32[8]] -#! -#! Invocation: exec -pub proc account_id_to_ethereum_hash - mem_store.0 - mem_store.1 - mem_store.2 - mem_store.3 - mem_store.4 - - push.20.0 - exec.keccak256::hash_bytes - # Stack: [DIGEST_U32[8]] -end - #! Builds a single felt from two u32 limbs (little-endian limb order). #! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. #! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index 3f57daa8ba..ed135256a8 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -143,11 +143,11 @@ impl EthAddress { let mut result = [Felt::ZERO; 5]; // i=0 -> bytes[16..20], i=4 -> bytes[0..4] - for i in 0..5 { + for (i, felt) in result.iter_mut().enumerate() { let start = (4 - i) * 4; let chunk = &self.0[start..start + 4]; let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - result[i] = Felt::new(value as u64); + *felt = Felt::new(value as u64); } result diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 0832c360db..33e4f29e63 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -5,7 +5,6 @@ use alloc::sync::Arc; use miden_agglayer::{EthAddress, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; -use miden_core_lib::handlers::keccak256::KeccakPreimage; use miden_processor::fast::{ExecutionOutput, FastProcessor}; use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; use miden_protocol::Felt; @@ -88,57 +87,6 @@ fn test_random_bech32_to_ethereum_roundtrip() { } } -#[tokio::test] -async fn test_address_bytes20_hash_in_masm() -> anyhow::Result<()> { - // Create account ID and convert to Ethereum address - let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?; - let eth_address = EthAddress::from_account_id(account_id); - - // Convert to field elements for MASM - let address_felts = eth_address.to_elements().to_vec(); - let addr_u32s: Vec = address_felts.iter().map(|f| f.as_int() as u32).collect(); - - // Compute expected Keccak256 hash using the same byte representation as MASM - let mut address_bytes = Vec::new(); - for &addr_u32 in &addr_u32s { - address_bytes.extend_from_slice(&addr_u32.to_le_bytes()); - } - address_bytes.truncate(20); - - let preimage = KeccakPreimage::new(address_bytes); - let expected_digest: Vec = preimage.digest().as_ref().iter().map(Felt::as_int).collect(); - - // Execute MASM procedure to compute the hash - let script_code = format!( - " - use miden::core::sys - use miden::agglayer::eth_address - - begin - push.{}.{}.{}.{}.{} - exec.eth_address::account_id_to_ethereum_hash - exec.sys::truncate_stack - end - ", - addr_u32s[4], addr_u32s[3], addr_u32s[2], addr_u32s[1], addr_u32s[0] - ); - - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(agglayer_library()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); - - let exec_output = execute_program_with_default_host(program).await?; - let actual_digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); - - assert_eq!(actual_digest, expected_digest); - - Ok(()) -} - #[tokio::test] async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { let test_account_ids = [ From 049e8bebec73aeafd2bf6b4b0e738b3064bebdca Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 17:17:56 -0500 Subject: [PATCH 11/37] fix: lint doc check --- crates/miden-agglayer/src/eth_address.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index ed135256a8..a6f7a1a0d5 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -52,7 +52,7 @@ impl From for AddrConvError { /// /// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the /// most-significant byte). -/// - MASM "address[5]" limbs: 5 x u32 limbs in *little-endian limb order*: +/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *little-endian limb order*: /// - addr0 = bytes[16..19] (least-significant 4 bytes) /// - addr1 = bytes[12..15] /// - addr2 = bytes[ 8..11] @@ -131,7 +131,7 @@ impl EthAddress { /// Converts the Ethereum address into an array of 5 [`Felt`] values. /// - /// The returned order matches the MASM `address[5]` convention (*little-endian limb order*): + /// The returned order matches the MASM `address\[5\]` convention (*little-endian limb order*): /// - addr0 = bytes[16..19] (least-significant 4 bytes) /// - addr1 = bytes[12..15] /// - addr2 = bytes[ 8..11] From 99161e3946910d21812c94777e87353f65c9a5fc Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 17:21:11 -0500 Subject: [PATCH 12/37] refactor: use u32assert2 --- crates/miden-agglayer/asm/bridge/eth_address.masm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index 20ddd70670..b973bfdf5a 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -24,8 +24,7 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" #! Outputs: [felt] proc build_felt # --- validate u32 limbs --- - u32assert.err=ERR_NOT_U32 # lo - dup.1 u32assert.err=ERR_NOT_U32 drop # hi + u32assert2.err=ERR_NOT_U32 # => [lo, hi] # keep copies for the overflow check From 3dc29f62c4e9dbba4189e3510a92954d987ca021 Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 17:46:28 -0500 Subject: [PATCH 13/37] refactor: simplify from_account_id() & u32 check --- crates/miden-agglayer/asm/bridge/eth_address.masm | 5 +---- crates/miden-agglayer/src/eth_address.rs | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index b973bfdf5a..4c372f887a 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -73,10 +73,7 @@ end #! Invocation: exec pub proc ethereum_address_to_account_id # addr4 must be 0 (most-significant limb) - movup.4 - u32assert.err=ERR_NOT_U32 - dup eq.0 assert.err=ERR_ADDR4_NONZERO - drop + movup.4 drop # => [addr0, addr1, addr2, addr3] exec.build_felt diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index a6f7a1a0d5..57aed612fd 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -103,15 +103,11 @@ impl EthAddress { /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). pub fn from_account_id(account_id: AccountId) -> Self { let felts: [Felt; 2] = account_id.into(); - let words = [felts[0].as_int(), felts[1].as_int()]; let mut out = [0u8; 20]; - let w0 = words[0].to_be_bytes(); - let w1 = words[1].to_be_bytes(); - out[0..4].copy_from_slice(&[0, 0, 0, 0]); - out[4..12].copy_from_slice(&w0); - out[12..20].copy_from_slice(&w1); + out[4..12].copy_from_slice(&felts[0].as_int().to_be_bytes()); + out[12..20].copy_from_slice(&felts[1].as_int().to_be_bytes()); Self(out) } From 4c6289da62d815b1a676fe948bcc6d878a62b65c Mon Sep 17 00:00:00 2001 From: riemann Date: Fri, 9 Jan 2026 17:48:11 -0500 Subject: [PATCH 14/37] revert: undo drop addr4 in ethereum_address_to_account_id --- crates/miden-agglayer/asm/bridge/eth_address.masm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index 4c372f887a..3dd2bac8a3 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -73,7 +73,8 @@ end #! Invocation: exec pub proc ethereum_address_to_account_id # addr4 must be 0 (most-significant limb) - movup.4 drop + movup.4 + eq.0 assert.err=ERR_ADDR4_NONZERO # => [addr0, addr1, addr2, addr3] exec.build_felt From 576f907e9ecbfc7397b3dbefae09125cafca2d85 Mon Sep 17 00:00:00 2001 From: riemann Date: Mon, 12 Jan 2026 11:43:31 -0500 Subject: [PATCH 15/37] feat: init getLeafValue() test --- .../asm/bridge/crypto_utils.masm | 21 ++ .../tests/agglayer/crypto_utils.rs | 328 ++++++++++++++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + 3 files changed, 350 insertions(+) create mode 100644 crates/miden-testing/tests/agglayer/crypto_utils.rs diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 7796c1f94f..de15835dea 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -80,3 +80,24 @@ pub proc verify_claim_proof dropw dropw dropw dropw push.1 end + +#! Hash bytes using Keccak256 +#! +#! This procedure takes a pointer to memory containing packed u32 values and the length in bytes, +#! then computes the Keccak256 hash of those bytes. +#! +#! Inputs: [ptr, len_bytes] +#! Outputs: [DIGEST_U32[8]] +#! +#! Where: +#! - ptr is the memory address where the packed u32 values are stored +#! - len_bytes is the total length of the data in bytes +#! - DIGEST_U32[8] is the Keccak256 hash as 8 u32 values (32 bytes total) +#! +#! The memory at ptr should contain u32 values in little-endian format, +#! packed sequentially to represent the byte data to be hashed. +#! +#! Invocation: exec +pub proc hash_bytes + exec.keccak256::hash_bytes +end 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..f4a8d9896f --- /dev/null +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -0,0 +1,328 @@ +extern crate alloc; + +use alloc::sync::Arc; +use alloc::string::String; +use alloc::vec::Vec; + +use miden_agglayer::agglayer_library; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_processor::fast::{ExecutionOutput, FastProcessor}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; +use miden_protocol::Felt; +use miden_protocol::transaction::TransactionKernel; + +const INPUT_MEMORY_ADDR: u32 = 0x1000; + +/// 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(); + + 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 = AdviceInputs::default(); + + let processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); + processor.execute(&program, &mut host).await +} + +/// Generate MASM code to store field elements in memory +fn masm_store_felts(felts: &[Felt], base_addr: u32) -> String { + let mut code = String::new(); + + for (i, felt) in felts.iter().enumerate() { + let addr = base_addr + (i as u32); + code.push_str(&format!("push.{}.{} mem_store\n", felt.as_int(), addr)); + } + + code +} + +/// 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() % 4 != 0 { + padded_data.push(0); + } + + // Convert to u32 words in little-endian format + for chunk in padded_data.chunks(4) { + let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + felts.push(Felt::new(word as u64)); + } + + 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 +} + +#[tokio::test] +async fn test_keccak_hash_bytes_test() -> anyhow::Result<()> { + let mut input_u8: Vec = vec![0u8; 24]; + input_u8.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); + + let len_bytes = input_u8.len(); + let input_felts = bytes_to_felts(&input_u8); + let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR); + + let agglayer_lib = agglayer_library(); + + let source = format!( + r#" + use miden::core::sys + use miden::core::crypto::hashes::keccak256 + + begin + # Store packed u32 values in memory + {memory_stores_source} + + # Push wrapper inputs + push.{len_bytes}.{INPUT_MEMORY_ADDR} + # => [ptr, len_bytes] + + exec.keccak256::hash_bytes + # => [DIGEST_U32[8]] + + 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).await?; + + // Extract the digest from the stack (8 u32 values) + let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); + let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest); + + + println!("solidity-style digest: {solidity_hex}"); + println!("digest: {:?}", digest); + + // Expected digest for the test case: 24 zero bytes + [1,2,3,4,5,6,7,8] + let expected_digest = vec![3225960785, 4007474008, 2169124512, 2724332080, 2839075162, 3406483620, 4039244674, 3474684833]; + let expected_hex = "0x514148c05833ddeea0364a81300262a25ad938a9a4d00acb82fbc1f0a17b1bcf"; + + assert_eq!(digest, expected_digest); + assert_eq!(solidity_hex, expected_hex); + + Ok(()) +} + + +#[tokio::test] +async fn test_keccak_hash_get_leaf_value_encode_packed() -> anyhow::Result<()> { + // Solidity equivalent: + // keccak256(abi.encodePacked( + // leafType(uint8), + // originNetwork(uint32), + // originAddress(address), + // destinationNetwork(uint32), + // destinationAddress(address), + // amount(uint256), + // metadataHash(bytes32) + // )) + + // ---- Fixed test vector (easy to mirror in Solidity) ---- + let leaf_type: u8 = 0x01; + let origin_network: u32 = 0x1122_3344; + let origin_address: [u8; 20] = [0x11; 20]; + + let destination_network: u32 = 0x5566_7788; + let destination_address: [u8; 20] = [0x22; 20]; + + // uint256 amount = 0x0102030405060708 (packed to 32 bytes big-endian) + let mut amount: [u8; 32] = [0u8; 32]; + amount[24..32].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); + + // bytes32 metadataHash = 0xaaaa....aaaa + let metadata_hash: [u8; 32] = [0xaa; 32]; + + // ---- abi.encodePacked layout ---- + // uint8 -> 1 byte + // uint32 -> 4 bytes big-endian + // address-> 20 bytes + // uint32 -> 4 bytes big-endian + // address-> 20 bytes + // uint256-> 32 bytes big-endian + // bytes32-> 32 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(&origin_address); + input_u8.extend_from_slice(&destination_network.to_be_bytes()); + input_u8.extend_from_slice(&destination_address); + input_u8.extend_from_slice(&amount); + input_u8.extend_from_slice(&metadata_hash); + + let len_bytes = input_u8.len(); + assert_eq!(len_bytes, 113); + + let input_felts = bytes_to_felts(&input_u8); + let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR); + + let agglayer_lib = agglayer_library(); + + let source = format!( + r#" + use miden::core::sys + use miden::core::crypto::hashes::keccak256 + + begin + # Store packed u32 values in memory + {memory_stores_source} + + # Push wrapper inputs + push.{len_bytes}.{INPUT_MEMORY_ADDR} + # => [ptr, len_bytes] + + exec.keccak256::hash_bytes + # => [DIGEST_U32[8]] + + 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).await?; + + // Extract the digest from the stack (8 u32 values) + let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); + let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest); + + println!("solidity-style digest: {solidity_hex}"); + println!("digest: {:?}", digest); + + Ok(()) +} + + +#[tokio::test] +async fn test_keccak_hash_get_leaf_value_hardhat_vector() -> anyhow::Result<()> { + // 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 + } + + // === 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", + ); + + // 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 input_felts = bytes_to_felts(&input_u8); + let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR); + + let agglayer_lib = agglayer_library(); + + let source = format!( + r#" + use miden::core::sys + use miden::core::crypto::hashes::keccak256 + + begin + {memory_stores_source} + + push.{len_bytes}.{INPUT_MEMORY_ADDR} + exec.keccak256::hash_bytes + 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).await?; + + // Extract digest as 8 u32 words + let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); + let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest); + + println!("solidity-style digest: {solidity_hex}"); + println!("digest: {:?}", digest); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 2a6d344c67..1e365d6d92 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,4 +1,5 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; +mod crypto_utils; mod solidity_miden_address_conversion; From a8e35d3fe42533f60f069df9d2195759bf170b37 Mon Sep 17 00:00:00 2001 From: riemann Date: Mon, 12 Jan 2026 18:24:46 -0500 Subject: [PATCH 16/37] feat: implement AdviceMap key based getLeafValue procedure --- .../miden-agglayer/asm/bridge/bridge_in.masm | 2 +- .../asm/bridge/crypto_utils.masm | 98 ++++---- .../tests/agglayer/crypto_utils.rs | 238 +++--------------- 3 files changed, 93 insertions(+), 245 deletions(-) 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/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index de15835dea..07285400c3 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -1,32 +1,66 @@ use miden::core::crypto::hashes::keccak256 -#! Given the leaf data returns the leaf value. +const LEAF_DATA_BYTES = 113 + +#! 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) +#! EMPTY_WORD // padding +#! ], +#! } #! 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 - # stubbed out: - push.1.1.1.1 - push.1.1.1.1 - - # exec.keccak256::hash_bytes + adv.push_mapval + # => [len, LEAF_DATA_KEY] + + dropw + # => [] + + # @dev what should the starting mem ptr be? + # writing AdviceStack into memory starting at mem address 0 + push.0 + repeat.7 + # => [loc_ptr] + + padw + # => [EMPTY_WORD, loc_ptr] + + adv_loadw + # => [VALS, loc_ptr] + + movup.4 dup movdn.5 + # => [loc_ptr, VALS, loc_ptr] + + mem_storew_be dropw + # => [loc_ptr] + + add.4 + # => [loc_ptr+4] + end + # => [loc_ptr] + + adv_push.1 swap + # => [loc_ptr, data] + + mem_store + # => [] + + push.LEAF_DATA_BYTES.0 + # => [mem_ptr, len_bytes] + + exec.keccak256::hash_bytes # => [LEAF_VALUE[8]] end @@ -36,7 +70,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]) @@ -81,23 +115,3 @@ pub proc verify_claim_proof push.1 end -#! Hash bytes using Keccak256 -#! -#! This procedure takes a pointer to memory containing packed u32 values and the length in bytes, -#! then computes the Keccak256 hash of those bytes. -#! -#! Inputs: [ptr, len_bytes] -#! Outputs: [DIGEST_U32[8]] -#! -#! Where: -#! - ptr is the memory address where the packed u32 values are stored -#! - len_bytes is the total length of the data in bytes -#! - DIGEST_U32[8] is the Keccak256 hash as 8 u32 values (32 bytes total) -#! -#! The memory at ptr should contain u32 values in little-endian format, -#! packed sequentially to represent the byte data to be hashed. -#! -#! Invocation: exec -pub proc hash_bytes - exec.keccak256::hash_bytes -end diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index f4a8d9896f..c3da8c617f 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -1,22 +1,22 @@ extern crate alloc; -use alloc::sync::Arc; 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_processor::fast::{ExecutionOutput, FastProcessor}; use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; -use miden_protocol::Felt; use miden_protocol::transaction::TransactionKernel; - -const INPUT_MEMORY_ADDR: u32 = 0x1000; +use miden_protocol::{Felt, Word}; /// Execute a program with default host async fn execute_program_with_default_host( program: Program, + advice_inputs: AdviceInputs, ) -> Result { let mut host = DefaultHost::default(); @@ -34,40 +34,27 @@ async fn execute_program_with_default_host( host.load_library(agglayer_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 } -/// Generate MASM code to store field elements in memory -fn masm_store_felts(felts: &[Felt], base_addr: u32) -> String { - let mut code = String::new(); - - for (i, felt) in felts.iter().enumerate() { - let addr = base_addr + (i as u32); - code.push_str(&format!("push.{}.{} mem_store\n", felt.as_int(), addr)); - } - - code -} - /// 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() % 4 != 0 { + 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 word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); felts.push(Felt::new(word as u64)); } - + felts } @@ -87,187 +74,30 @@ fn u32_words_to_solidity_bytes32_hex(words: &[u64]) -> String { s } -#[tokio::test] -async fn test_keccak_hash_bytes_test() -> anyhow::Result<()> { - let mut input_u8: Vec = vec![0u8; 24]; - input_u8.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); - - let len_bytes = input_u8.len(); - let input_felts = bytes_to_felts(&input_u8); - let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR); - - let agglayer_lib = agglayer_library(); - - let source = format!( - r#" - use miden::core::sys - use miden::core::crypto::hashes::keccak256 - - begin - # Store packed u32 values in memory - {memory_stores_source} - - # Push wrapper inputs - push.{len_bytes}.{INPUT_MEMORY_ADDR} - # => [ptr, len_bytes] - - exec.keccak256::hash_bytes - # => [DIGEST_U32[8]] - - 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).await?; - - // Extract the digest from the stack (8 u32 values) - let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); - let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest); - - - println!("solidity-style digest: {solidity_hex}"); - println!("digest: {:?}", digest); - - // Expected digest for the test case: 24 zero bytes + [1,2,3,4,5,6,7,8] - let expected_digest = vec![3225960785, 4007474008, 2169124512, 2724332080, 2839075162, 3406483620, 4039244674, 3474684833]; - let expected_hex = "0x514148c05833ddeea0364a81300262a25ad938a9a4d00acb82fbc1f0a17b1bcf"; - - assert_eq!(digest, expected_digest); - assert_eq!(solidity_hex, expected_hex); - - Ok(()) -} - - -#[tokio::test] -async fn test_keccak_hash_get_leaf_value_encode_packed() -> anyhow::Result<()> { - // Solidity equivalent: - // keccak256(abi.encodePacked( - // leafType(uint8), - // originNetwork(uint32), - // originAddress(address), - // destinationNetwork(uint32), - // destinationAddress(address), - // amount(uint256), - // metadataHash(bytes32) - // )) - - // ---- Fixed test vector (easy to mirror in Solidity) ---- - let leaf_type: u8 = 0x01; - let origin_network: u32 = 0x1122_3344; - let origin_address: [u8; 20] = [0x11; 20]; - - let destination_network: u32 = 0x5566_7788; - let destination_address: [u8; 20] = [0x22; 20]; - - // uint256 amount = 0x0102030405060708 (packed to 32 bytes big-endian) - let mut amount: [u8; 32] = [0u8; 32]; - amount[24..32].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); - - // bytes32 metadataHash = 0xaaaa....aaaa - let metadata_hash: [u8; 32] = [0xaa; 32]; - - // ---- abi.encodePacked layout ---- - // uint8 -> 1 byte - // uint32 -> 4 bytes big-endian - // address-> 20 bytes - // uint32 -> 4 bytes big-endian - // address-> 20 bytes - // uint256-> 32 bytes big-endian - // bytes32-> 32 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(&origin_address); - input_u8.extend_from_slice(&destination_network.to_be_bytes()); - input_u8.extend_from_slice(&destination_address); - input_u8.extend_from_slice(&amount); - input_u8.extend_from_slice(&metadata_hash); - - let len_bytes = input_u8.len(); - assert_eq!(len_bytes, 113); - - let input_felts = bytes_to_felts(&input_u8); - let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR); - - let agglayer_lib = agglayer_library(); - - let source = format!( - r#" - use miden::core::sys - use miden::core::crypto::hashes::keccak256 - - begin - # Store packed u32 values in memory - {memory_stores_source} - - # Push wrapper inputs - push.{len_bytes}.{INPUT_MEMORY_ADDR} - # => [ptr, len_bytes] - - exec.keccak256::hash_bytes - # => [DIGEST_U32[8]] - - 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).await?; - - // Extract the digest from the stack (8 u32 values) - let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); - let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest); - - println!("solidity-style digest: {solidity_hex}"); - println!("digest: {:?}", digest); - - Ok(()) +// 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_hardhat_vector() -> anyhow::Result<()> { - // 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 - } + 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 token_address: [u8; 20] = hex_to_fixed("0x1234567890123456789012345678901234567890"); let destination_network: u32 = 1; - let destination_address: [u8; 20] = - hex_to_fixed("0x0987654321098765432109876543210987654321"); + let destination_address: [u8; 20] = hex_to_fixed("0x0987654321098765432109876543210987654321"); let amount_u64: u64 = 1; // 1e19 - let metadata_hash: [u8; 32] = hex_to_fixed( - "0x2cdc14cacf6fec86a549f0e4d01e83027d3b10f29fa527c1535192c1ca1aac81", - ); + let metadata_hash: [u8; 32] = + hex_to_fixed("0x2cdc14cacf6fec86a549f0e4d01e83027d3b10f29fa527c1535192c1ca1aac81"); // abi.encodePacked( // uint8, uint32, address, uint32, address, uint256, bytes32 @@ -287,21 +117,23 @@ async fn test_keccak_hash_get_leaf_value_hardhat_vector() -> anyhow::Result<()> 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); - let memory_stores_source = masm_store_felts(&input_felts, INPUT_MEMORY_ADDR); - let agglayer_lib = agglayer_library(); + // Arbitrary key to store input in advice map (in prod this is RPO(input_felts)) + let key: Word = [Felt::new(1), Felt::new(1), Felt::new(1), Felt::new(1)].into(); + 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 - {memory_stores_source} + push.{key} - push.{len_bytes}.{INPUT_MEMORY_ADDR} - exec.keccak256::hash_bytes + exec.crypto_utils::get_leaf_value exec.sys::truncate_stack end "# @@ -315,14 +147,16 @@ async fn test_keccak_hash_get_leaf_value_hardhat_vector() -> anyhow::Result<()> .assemble_program(&source) .unwrap(); - let exec_output = execute_program_with_default_host(program).await?; + let exec_output = execute_program_with_default_host(program, advice_inputs).await?; - // Extract digest as 8 u32 words let digest: Vec = exec_output.stack[0..8].iter().map(|f| f.as_int()).collect(); - let solidity_hex = u32_words_to_solidity_bytes32_hex(&digest); + 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); - println!("solidity-style digest: {solidity_hex}"); - println!("digest: {:?}", digest); + assert_eq!(digest, keccak256_digest); + assert_eq!(hex_digest, keccak256_hex_digest,); Ok(()) } From a1a1c3dc33afde29a306de9594f743fec441a825 Mon Sep 17 00:00:00 2001 From: Alexander John Lee <77119221+partylikeits1983@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:01:44 -0500 Subject: [PATCH 17/37] Update crates/miden-agglayer/src/eth_address.rs Co-authored-by: Marti --- crates/miden-agglayer/src/eth_address.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index 57aed612fd..0270d4f011 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -46,7 +46,7 @@ impl From for AddrConvError { // ETHEREUM ADDRESS // ================================================================================================ -/// Represents an Ethereum address (20 bytes). +/// Represents an Ethereum address format (20 bytes). /// /// # Representations used in this module /// From 359b3ef7b20aee703eb9dd1c428d888c9cba0c3f Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 13 Jan 2026 14:17:43 -0500 Subject: [PATCH 18/37] refactor: update test name --- crates/miden-testing/tests/agglayer/crypto_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index c3da8c617f..0c3e19b98f 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -86,7 +86,7 @@ fn hex_to_fixed(s: &str) -> [u8; N] { } #[tokio::test] -async fn test_keccak_hash_get_leaf_value_hardhat_vector() -> anyhow::Result<()> { +async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { let agglayer_lib = agglayer_library(); // === Values from hardhat test === From 1264d24425b227213e0c25f2d7b0732d66b47f0e Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 13 Jan 2026 14:42:40 -0500 Subject: [PATCH 19/37] refactor: rename to EthAddressFormat --- .../asm/bridge/eth_address.masm | 6 ++--- crates/miden-agglayer/src/eth_address.rs | 23 +++++++++---------- crates/miden-agglayer/src/lib.rs | 7 +++--- crates/miden-agglayer/src/utils.rs | 20 ++++++++-------- .../tests/agglayer/bridge_out.rs | 8 ++++--- .../solidity_miden_address_conversion.rs | 12 +++++----- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index 3dd2bac8a3..c6e851164b 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -49,9 +49,9 @@ proc build_felt # => [felt] end -#! Converts an Ethereum address (address[5] type) back into an AccountId [prefix, suffix] type. +#! Converts an Ethereum address format (address[5] type) back into an AccountId [prefix, suffix] type. #! -#! The Ethereum address is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*: +#! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*: #! addr0 = bytes[16..19] (least-significant 4 bytes) #! addr1 = bytes[12..15] #! addr2 = bytes[ 8..11] @@ -71,7 +71,7 @@ end #! Outputs: [prefix, suffix] #! #! Invocation: exec -pub proc ethereum_address_to_account_id +pub proc ethereum_address_format_to_account_id # addr4 must be 0 (most-significant limb) movup.4 eq.0 assert.err=ERR_ADDR4_NONZERO diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index 0270d4f011..0b516816cb 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -65,18 +65,18 @@ impl From for AddrConvError { /// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure /// `Felt::new(u64)` does not reduce mod p (checked explicitly in `to_account_id`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct EthAddress([u8; 20]); +pub struct EthAddressFormat([u8; 20]); -impl EthAddress { +impl EthAddressFormat { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`EthAddress`] from a 20-byte array. + /// Creates a new [`EthAddressFormat`] from a 20-byte array. pub const fn new(bytes: [u8; 20]) -> Self { Self(bytes) } - /// Creates an [`EthAddress`] from a hex string (with or without "0x" prefix). + /// Creates an [`EthAddressFormat`] from a hex string (with or without "0x" prefix). /// /// # Errors /// @@ -97,7 +97,7 @@ impl EthAddress { Ok(Self(bytes)) } - /// Creates an [`EthAddress`] from an [`AccountId`]. + /// Creates an [`EthAddressFormat`] from an [`AccountId`]. /// /// This conversion is infallible: an [`AccountId`] is two felts, and `as_int()` yields `u64` /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). @@ -105,7 +105,6 @@ impl EthAddress { let felts: [Felt; 2] = account_id.into(); let mut out = [0u8; 20]; - out[0..4].copy_from_slice(&[0, 0, 0, 0]); out[4..12].copy_from_slice(&felts[0].as_int().to_be_bytes()); out[12..20].copy_from_slice(&felts[1].as_int().to_be_bytes()); @@ -200,26 +199,26 @@ impl EthAddress { } } -impl fmt::Display for EthAddress { +impl fmt::Display for EthAddressFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_hex()) } } -impl From<[u8; 20]> for EthAddress { +impl From<[u8; 20]> for EthAddressFormat { fn from(bytes: [u8; 20]) -> Self { Self(bytes) } } -impl From for EthAddress { +impl From for EthAddressFormat { fn from(account_id: AccountId) -> Self { - EthAddress::from_account_id(account_id) + EthAddressFormat::from_account_id(account_id) } } -impl From for [u8; 20] { - fn from(addr: EthAddress) -> Self { +impl From for [u8; 20] { + fn from(addr: EthAddressFormat) -> Self { addr.0 } } diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index b9b0b6f879..7dc461bff2 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -40,7 +40,7 @@ pub mod errors; pub mod eth_address; pub mod utils; -pub use eth_address::EthAddress; +pub use eth_address::EthAddressFormat; use utils::bytes32_to_felts; // AGGLAYER NOTE SCRIPTS @@ -426,7 +426,7 @@ pub fn create_claim_note(params: ClaimNoteParams<'_, R>) -> Result Vec { - bytes32 - .chunks(4) - .map(|chunk| { - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - Felt::new(value as u64) - }) - .collect() +/// Converts a bytes32 value (32 bytes) into an array of 8 Felt values. +pub fn bytes32_to_felts(bytes32: &[u8; 32]) -> [Felt; 8] { + let mut result = [Felt::ZERO; 8]; + for (i, chunk) in bytes32.chunks(4).enumerate() { + let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + result[i] = Felt::new(value as u64); + } + result } /// Convert 8 Felt values (u32 limbs in little-endian order) to U256 bytes in little-endian format. diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index c87b0c3153..2e9fcde5ea 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,6 +1,6 @@ extern crate alloc; -use miden_agglayer::{EthAddress, b2agg_script, bridge_out_component}; +use miden_agglayer::{EthAddressFormat, b2agg_script, bridge_out_component}; use miden_protocol::account::{ Account, AccountId, @@ -82,7 +82,8 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { // destination_address: 20 bytes (Ethereum address) split into 5 u32 values let destination_network = Felt::new(1); // Example network ID let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = EthAddress::from_hex(destination_address).expect("Valid Ethereum address"); + 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 inputs (6 felts total) @@ -240,7 +241,8 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // Create note inputs with destination network and address let destination_network = Felt::new(1); let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = EthAddress::from_hex(destination_address).expect("Valid Ethereum address"); + 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 inputs (6 felts total) diff --git a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 33e4f29e63..642c788fdc 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -2,7 +2,7 @@ extern crate alloc; use alloc::sync::Arc; -use miden_agglayer::{EthAddress, agglayer_library}; +use miden_agglayer::{EthAddressFormat, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_processor::fast::{ExecutionOutput, FastProcessor}; @@ -46,7 +46,7 @@ async fn execute_program_with_default_host( #[test] fn test_account_id_to_ethereum_roundtrip() { let original_account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let eth_address = EthAddress::from_account_id(original_account_id); + let eth_address = EthAddressFormat::from_account_id(original_account_id); let recovered_account_id = eth_address.to_account_id().unwrap(); assert_eq!(original_account_id, recovered_account_id); } @@ -61,7 +61,7 @@ fn test_bech32_to_ethereum_roundtrip() { for bech32_address in test_addresses { let (network_id, account_id) = AccountId::from_bech32(bech32_address).unwrap(); - let eth_address = EthAddress::from_account_id(account_id); + let eth_address = EthAddressFormat::from_account_id(account_id); let recovered_account_id = eth_address.to_account_id().unwrap(); let recovered_bech32 = recovered_account_id.to_bech32(network_id); @@ -78,7 +78,7 @@ fn test_random_bech32_to_ethereum_roundtrip() { for _ in 0..3 { let account_id = AccountIdBuilder::new().build_with_rng(&mut rng); let bech32_address = account_id.to_bech32(network_id.clone()); - let eth_address = EthAddress::from_account_id(account_id); + let eth_address = EthAddressFormat::from_account_id(account_id); let recovered_account_id = eth_address.to_account_id().unwrap(); let recovered_bech32 = recovered_account_id.to_bech32(network_id.clone()); @@ -98,7 +98,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { ]; for (idx, original_account_id) in test_account_ids.iter().enumerate() { - let eth_address = EthAddress::from_account_id(*original_account_id); + let eth_address = EthAddressFormat::from_account_id(*original_account_id); let address_felts = eth_address.to_elements().to_vec(); let le: Vec = address_felts @@ -129,7 +129,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { begin push.{}.{}.{}.{}.{} - exec.eth_address::ethereum_address_to_account_id + exec.eth_address::ethereum_address_format_to_account_id exec.sys::truncate_stack end "#, From 2288c0d524780d69879c0336c7fc4a70f2601435 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 13 Jan 2026 14:51:32 -0500 Subject: [PATCH 20/37] refactor: rearrange EthAddressFormat --- crates/miden-agglayer/src/eth_address.rs | 40 +++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address.rs index 0b516816cb..74cda88c2f 100644 --- a/crates/miden-agglayer/src/eth_address.rs +++ b/crates/miden-agglayer/src/eth_address.rs @@ -68,7 +68,7 @@ impl From for AddrConvError { pub struct EthAddressFormat([u8; 20]); impl EthAddressFormat { - // CONSTRUCTORS + // EXTERNAL API - For integrators (Gateway, claim managers, etc.) // -------------------------------------------------------------------------------------------- /// Creates a new [`EthAddressFormat`] from a 20-byte array. @@ -99,8 +99,18 @@ impl EthAddressFormat { /// Creates an [`EthAddressFormat`] from an [`AccountId`]. /// + /// **External API**: This function is used by integrators (Gateway, claim managers) to convert + /// Miden AccountIds into the Ethereum address format for constructing CLAIM notes or + /// interfacing when calling the Agglayer Bridge function bridgeAsset(). + /// /// This conversion is infallible: an [`AccountId`] is two felts, and `as_int()` yields `u64` /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). + /// + /// # Example + /// ```ignore + /// let destination_address = EthAddressFormat::from_account_id(destination_account_id).into_bytes(); + /// // then construct the CLAIM note with destination_address... + /// ``` pub fn from_account_id(account_id: AccountId) -> Self { let felts: [Felt; 2] = account_id.into(); @@ -111,9 +121,6 @@ impl EthAddressFormat { Self(out) } - // CONVERSIONS - // -------------------------------------------------------------------------------------------- - /// Returns the raw 20-byte array. pub const fn as_bytes(&self) -> &[u8; 20] { &self.0 @@ -124,7 +131,19 @@ impl EthAddressFormat { self.0 } - /// Converts the Ethereum address into an array of 5 [`Felt`] values. + /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). + pub fn to_hex(&self) -> String { + bytes_to_hex_string(self.0) + } + + // INTERNAL API - For CLAIM note processing + // -------------------------------------------------------------------------------------------- + + /// Converts the Ethereum address format into an array of 5 [`Felt`] values for MASM processing. + /// + /// **Internal API**: This function is used internally during CLAIM note processing to convert + /// the address format into the MASM `address[5]` representation expected by the + /// `ethereum_address_format_to_account_id` procedure. /// /// The returned order matches the MASM `address\[5\]` convention (*little-endian limb order*): /// - addr0 = bytes[16..19] (least-significant 4 bytes) @@ -148,7 +167,11 @@ impl EthAddressFormat { result } - /// Converts the Ethereum address to an [`AccountId`]. + /// Converts the Ethereum address format back to an [`AccountId`]. + /// + /// **Internal API**: This function is used internally during CLAIM note processing to extract + /// the original AccountId from the Ethereum address format. It mirrors the functionality of + /// the MASM `ethereum_address_format_to_account_id` procedure. /// /// # Errors /// @@ -174,11 +197,6 @@ impl EthAddressFormat { AccountId::try_from([prefix_felt, suffix_felt]).map_err(|_| AddrConvError::InvalidAccountId) } - /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). - pub fn to_hex(&self) -> String { - bytes_to_hex_string(self.0) - } - // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- From d9c309a8880a7e7097d55e4edf9907f7476e4d51 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 13 Jan 2026 14:54:47 -0500 Subject: [PATCH 21/37] refactor: rename file to eth_address_format --- .../src/{eth_address.rs => eth_address_format.rs} | 0 crates/miden-agglayer/src/lib.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/miden-agglayer/src/{eth_address.rs => eth_address_format.rs} (100%) diff --git a/crates/miden-agglayer/src/eth_address.rs b/crates/miden-agglayer/src/eth_address_format.rs similarity index 100% rename from crates/miden-agglayer/src/eth_address.rs rename to crates/miden-agglayer/src/eth_address_format.rs diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 7dc461bff2..ba7a6d8774 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -37,10 +37,10 @@ use miden_standards::account::faucets::NetworkFungibleFaucet; use miden_utils_sync::LazyLock; pub mod errors; -pub mod eth_address; +pub mod eth_address_format; pub mod utils; -pub use eth_address::EthAddressFormat; +pub use eth_address_format::EthAddressFormat; use utils::bytes32_to_felts; // AGGLAYER NOTE SCRIPTS From 3c3c29e646d031e4e32d1929debacd0ceabeeb69 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 13 Jan 2026 20:25:30 -0500 Subject: [PATCH 22/37] fix: update script roots --- crates/miden-agglayer/asm/bridge/agglayer_faucet.masm | 2 +- crates/miden-agglayer/asm/bridge/bridge_out.masm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index db22b900ad..c8169b512d 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -35,7 +35,7 @@ const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 548 const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 552 # P2ID output note constants -const P2ID_SCRIPT_ROOT = [7588674509004260508, 4058706621878288170, 5607159951796201570, 5541281552524512743] +const P2ID_SCRIPT_ROOT = [13362761878458161062, 15090726097241769395, 444910447169617901, 3558201871398422326] const P2ID_NOTE_NUM_INPUTS = 2 const OUTPUT_NOTE_TYPE_PUBLIC = 1 const EXECUTION_HINT_ALWAYS = 1 diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index 3b8043e7c1..e5c79a300b 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -10,7 +10,7 @@ use miden::agglayer::local_exit_tree const MMR_PTR=42 const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") -const BURN_NOTE_ROOT = [6407337173854817345, 5626358912819151014, 703918618794810515, 17401169215223723177] +const BURN_NOTE_ROOT = [15615638671708113717, 1774623749760042586, 2028263167268363492, 12931944505143778072] const EXECUTION_HINT_ALWAYS=1 const PUBLIC_NOTE=1 const AUX=0 From af29827924ed6467b9ca35c5ab3d76f84ed54a9d Mon Sep 17 00:00:00 2001 From: Marti Date: Wed, 14 Jan 2026 11:36:41 +0000 Subject: [PATCH 23/37] chore: pipe words to memory --- .../asm/bridge/crypto_utils.masm | 45 ++++--------------- .../tests/agglayer/crypto_utils.rs | 7 +++ 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 07285400c3..3d1ccccb31 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -1,6 +1,9 @@ use miden::core::crypto::hashes::keccak256 +use miden::core::mem 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. #! @@ -14,7 +17,6 @@ const LEAF_DATA_BYTES = 113 #! 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: [LEAF_VALUE] @@ -22,46 +24,17 @@ const LEAF_DATA_BYTES = 113 #! Invocation: exec pub proc get_leaf_value - adv.push_mapval - # => [len, LEAF_DATA_KEY] - - dropw - # => [] - - # @dev what should the starting mem ptr be? - # writing AdviceStack into memory starting at mem address 0 - push.0 - repeat.7 - # => [loc_ptr] - - padw - # => [EMPTY_WORD, loc_ptr] - - adv_loadw - # => [VALS, loc_ptr] - - movup.4 dup movdn.5 - # => [loc_ptr, VALS, loc_ptr] - - mem_storew_be dropw - # => [loc_ptr] - - add.4 - # => [loc_ptr+4] - end - # => [loc_ptr] - - adv_push.1 swap - # => [loc_ptr, data] + adv.push_mapval dropw + # => [LEAF_DATA_KEY] - mem_store - # => [] + push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS + exec.mem::pipe_words_to_memory dropw dropw dropw drop - push.LEAF_DATA_BYTES.0 - # => [mem_ptr, len_bytes] + push.LEAF_DATA_BYTES push.LEAF_DATA_START_PTR exec.keccak256::hash_bytes # => [LEAF_VALUE[8]] + swapdw dropw dropw end #! Verify leaf and checks that it has not been claimed. diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 0c3e19b98f..f7a1f1ea6c 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -8,6 +8,7 @@ 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::fast::{ExecutionOutput, FastProcessor}; use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; use miden_protocol::transaction::TransactionKernel; @@ -55,6 +56,11 @@ fn bytes_to_felts(data: &[u8]) -> Vec { felts.push(Felt::new(word as u64)); } + // pad to next multiple of 4 felts + while felts.len() % 4 != 0 { + felts.push(Felt::ZERO); + } + felts } @@ -119,6 +125,7 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { 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 = [Felt::new(1), Felt::new(1), Felt::new(1), Felt::new(1)].into(); From d6b9954d66ff23574700b7d6960acc6019f811e0 Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 14 Jan 2026 12:52:09 -0500 Subject: [PATCH 24/37] refactor: add stack comments --- crates/miden-agglayer/asm/bridge/crypto_utils.masm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 3d1ccccb31..cf6a59f279 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -23,18 +23,22 @@ const LEAF_DATA_START_PTR = 0 #! #! Invocation: exec pub proc get_leaf_value - adv.push_mapval dropw # => [LEAF_DATA_KEY] push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS exec.mem::pipe_words_to_memory dropw dropw dropw 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 #! Verify leaf and checks that it has not been claimed. From d61f836e5b5f812016de91b25a907679b9813bd7 Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 14 Jan 2026 14:38:11 -0500 Subject: [PATCH 25/37] refactor: deduplicate execute_program_with_default_host --- .../tests/agglayer/asset_conversion.rs | 34 ++++-------------- .../tests/agglayer/crypto_utils.rs | 36 +++---------------- crates/miden-testing/tests/agglayer/mod.rs | 1 + .../solidity_miden_address_conversion.rs | 31 ++-------------- .../tests/agglayer/test_utils.rs | 35 ++++++++++++++++++ 5 files changed, 49 insertions(+), 88 deletions(-) create mode 100644 crates/miden-testing/tests/agglayer/test_utils.rs 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 index f7a1f1ea6c..9d21069e6c 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -9,36 +9,10 @@ use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_core_lib::handlers::keccak256::KeccakPreimage; use miden_crypto::FieldElement; -use miden_processor::fast::{ExecutionOutput, FastProcessor}; -use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; -use miden_protocol::transaction::TransactionKernel; +use miden_processor::AdviceInputs; use miden_protocol::{Felt, Word}; -/// Execute a program with default host -async fn execute_program_with_default_host( - program: Program, - advice_inputs: AdviceInputs, -) -> 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(); - - 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 processor = FastProcessor::new_debug(stack_inputs.as_slice(), advice_inputs); - processor.execute(&program, &mut host).await -} +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 { @@ -52,8 +26,8 @@ fn bytes_to_felts(data: &[u8]) -> Vec { // Convert to u32 words in little-endian format for chunk in padded_data.chunks(4) { - let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - felts.push(Felt::new(word as u64)); + 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 @@ -154,7 +128,7 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { .assemble_program(&source) .unwrap(); - let exec_output = execute_program_with_default_host(program, advice_inputs).await?; + 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); diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 1e365d6d92..65269c8c42 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -3,3 +3,4 @@ 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/solidity_miden_address_conversion.rs b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs index 642c788fdc..8df9105247 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -5,8 +5,6 @@ use alloc::sync::Arc; use miden_agglayer::{EthAddressFormat, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; -use miden_processor::fast::{ExecutionOutput, FastProcessor}; -use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; use miden_protocol::Felt; use miden_protocol::account::AccountId; use miden_protocol::address::NetworkId; @@ -15,33 +13,8 @@ use miden_protocol::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, AccountIdBuilder, }; -use miden_protocol::transaction::TransactionKernel; -/// 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(); - - for (event_name, handler) in std_lib.handlers() { - host.register_handler(event_name, handler)?; - } - - 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 -} +use super::test_utils::execute_program_with_default_host; #[test] fn test_account_id_to_ethereum_roundtrip() { @@ -144,7 +117,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> 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 actual_prefix = exec_output.stack[0].as_int(); let actual_suffix = exec_output.stack[1].as_int(); 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 d51bed16539035f943fedb459d55517a4627bc54 Mon Sep 17 00:00:00 2001 From: riemann Date: Wed, 14 Jan 2026 14:44:10 -0500 Subject: [PATCH 26/37] feat: add hardcoded expected hash to test --- crates/miden-testing/tests/agglayer/crypto_utils.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 9d21069e6c..d843fc1635 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -79,6 +79,9 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { 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 // ) @@ -137,7 +140,7 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { 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, keccak256_hex_digest); + assert_eq!(hex_digest, expected_hash); Ok(()) } From 1388770dd941c5f0b476398bbe60a6c6eaf082e5 Mon Sep 17 00:00:00 2001 From: Marti Date: Thu, 15 Jan 2026 11:26:40 +0000 Subject: [PATCH 27/37] fix: verify hash matches commitment --- crates/miden-agglayer/asm/bridge/crypto_utils.masm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index cf6a59f279..7346850620 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -23,11 +23,11 @@ const LEAF_DATA_START_PTR = 0 #! #! Invocation: exec pub proc get_leaf_value - adv.push_mapval dropw + adv.push_mapval # => [LEAF_DATA_KEY] push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS - exec.mem::pipe_words_to_memory dropw dropw dropw drop + exec.mem::pipe_preimage_to_memory drop # => [] push.LEAF_DATA_BYTES push.LEAF_DATA_START_PTR From f200752c5ce33255279efcd6ff362ae58321c758 Mon Sep 17 00:00:00 2001 From: Marti Date: Thu, 15 Jan 2026 11:27:09 +0000 Subject: [PATCH 28/37] fix: put data under correct key in advice map --- crates/miden-testing/tests/agglayer/crypto_utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index d843fc1635..4899b76490 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -10,7 +10,7 @@ use miden_core_lib::CoreLibrary; use miden_core_lib::handlers::keccak256::KeccakPreimage; use miden_crypto::FieldElement; use miden_processor::AdviceInputs; -use miden_protocol::{Felt, Word}; +use miden_protocol::{Felt, Hasher, Word}; use super::test_utils::execute_program_with_default_host; @@ -105,7 +105,7 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { assert_eq!(input_felts.len(), 32); // Arbitrary key to store input in advice map (in prod this is RPO(input_felts)) - let key: Word = [Felt::new(1), Felt::new(1), Felt::new(1), Felt::new(1)].into(); + let key: Word = Hasher::hash_elements(&input_felts); let advice_inputs = AdviceInputs::default().with_map(vec![(key, input_felts)]); let source = format!( From fc9ff91f897340307fd5054b936fcc3731b3c703 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Fri, 16 Jan 2026 23:09:38 -0800 Subject: [PATCH 29/37] chore: refresh Cargo.lock file --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 724453228e..1b4b769435 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1496,9 +1496,9 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7981c1d907bb9864e24f2bd6304c4fca03a41fc4606c09edd6a7f5a8fc80fc" +checksum = "f0b49de9b0d8370c992ee04791f68a4509078198b6f42e5f72a262e7d4456487" dependencies = [ "blake3", "cc", @@ -1530,9 +1530,9 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83479e7af490784c6f2d2e02cec5210fd6e5bc6ce3d4427734e36a773bca72d2" +checksum = "16a27c656284ef1985c1af92b9e68d2171c7996d6c8c557a2c57d1ef32f107ed" dependencies = [ "quote", "syn 2.0.114", From 0aef40d1f234f85b7c3d2dae51c713f39b6e5db1 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Sat, 17 Jan 2026 11:48:45 -0800 Subject: [PATCH 30/37] chore: increment crate versions to v0.14.0 --- CHANGELOG.md | 2 ++ Cargo.lock | 16 ++++++++-------- Cargo.toml | 18 +++++++++--------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5aa4d6d49..20aca620ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 0.14.0 (TBD) + ## 0.13.0 (2026-01-16) ### Features diff --git a/Cargo.lock b/Cargo.lock index 1b4b769435..7f5cdad936 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1381,7 +1381,7 @@ dependencies = [ [[package]] name = "miden-agglayer" -version = "0.13.0" +version = "0.14.0" dependencies = [ "fs-err", "miden-agglayer", @@ -1449,7 +1449,7 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.13.0" +version = "0.14.0" dependencies = [ "miden-protocol", "thiserror", @@ -1641,7 +1641,7 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.13.0" +version = "0.14.0" dependencies = [ "anyhow", "assert_matches", @@ -1680,7 +1680,7 @@ dependencies = [ [[package]] name = "miden-protocol-macros" -version = "0.13.0" +version = "0.14.0" dependencies = [ "miden-protocol", "proc-macro2", @@ -1704,7 +1704,7 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.13.0" +version = "0.14.0" dependencies = [ "anyhow", "assert_matches", @@ -1723,7 +1723,7 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.13.0" +version = "0.14.0" dependencies = [ "anyhow", "assert_matches", @@ -1751,7 +1751,7 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.13.0" +version = "0.14.0" dependencies = [ "anyhow", "assert_matches", @@ -1768,7 +1768,7 @@ dependencies = [ [[package]] name = "miden-tx-batch-prover" -version = "0.13.0" +version = "0.14.0" dependencies = [ "miden-protocol", "miden-tx", diff --git a/Cargo.toml b/Cargo.toml index 7b3b758c32..6e68706dd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ homepage = "https://miden.xyz" license = "MIT" repository = "https://github.com/0xMiden/miden-base" rust-version = "1.90" -version = "0.13.0" +version = "0.14.0" [profile.release] codegen-units = 1 @@ -42,14 +42,14 @@ lto = true [workspace.dependencies] # Workspace crates -miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.13" } -miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.13" } -miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.13" } -miden-protocol-macros = { default-features = false, path = "crates/miden-protocol-macros", version = "0.13" } -miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.13" } -miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.13" } -miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.13" } -miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.13" } +miden-agglayer = { default-features = false, path = "crates/miden-agglayer", version = "0.14" } +miden-block-prover = { default-features = false, path = "crates/miden-block-prover", version = "0.14" } +miden-protocol = { default-features = false, path = "crates/miden-protocol", version = "0.14" } +miden-protocol-macros = { default-features = false, path = "crates/miden-protocol-macros", version = "0.14" } +miden-standards = { default-features = false, path = "crates/miden-standards", version = "0.14" } +miden-testing = { default-features = false, path = "crates/miden-testing", version = "0.14" } +miden-tx = { default-features = false, path = "crates/miden-tx", version = "0.14" } +miden-tx-batch-prover = { default-features = false, path = "crates/miden-tx-batch-prover", version = "0.14" } # Miden dependencies miden-air = { default-features = false, version = "0.20" } From 1a3b8a37dad39e676972b0baa2a96e67d169438c Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Sat, 17 Jan 2026 11:56:08 -0800 Subject: [PATCH 31/37] chore: fix documentation links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df78fec0f4..2f84bd027d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Miden is a zero-knowledge rollup for high-throughput and private applications. M If you want to join the technical discussion or learn more about the project, please check out -- the [Documentation](https://0xMiden.github.io/miden-docs). +- the [Documentation](https://docs.miden.xyz/miden-base/). - the [Telegram](https://t.me/BuildOnMiden) - the [Repo](https://github.com/0xMiden) - the [Roadmap](https://miden.xyz/roadmap) @@ -71,7 +71,7 @@ Some of the functions in this project are computationally intensive and may take ## Documentation -The documentation in the `docs/` folder is built using Docusaurus and is automatically absorbed into the main [miden-docs](https://github.com/0xMiden/miden-docs) repository for the main documentation website. Changes to the `next` branch trigger an automated deployment workflow. The docs folder requires npm packages to be installed before building. +The documentation in the `docs/` folder is built using Docusaurus and is automatically absorbed into the main [miden-docs](https://docs.miden.xyz/miden-base/) repository for the main documentation website. Changes to the `next` branch trigger an automated deployment workflow. The docs folder requires npm packages to be installed before building. ## License From 1567d8907e1de2f02cfba6a69c6a02ba288d90d3 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 20 Jan 2026 11:09:03 -0500 Subject: [PATCH 32/37] fix: rm redundant file --- .../miden-agglayer/src/eth_address_format.rs | 242 ------------------ 1 file changed, 242 deletions(-) delete mode 100644 crates/miden-agglayer/src/eth_address_format.rs diff --git a/crates/miden-agglayer/src/eth_address_format.rs b/crates/miden-agglayer/src/eth_address_format.rs deleted file mode 100644 index 74cda88c2f..0000000000 --- a/crates/miden-agglayer/src/eth_address_format.rs +++ /dev/null @@ -1,242 +0,0 @@ -use alloc::format; -use alloc::string::{String, ToString}; -use core::fmt; - -use miden_core::FieldElement; -use miden_protocol::Felt; -use miden_protocol::account::AccountId; -use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AddrConvError { - NonZeroWordPadding, - NonZeroBytePrefix, - InvalidHexLength, - InvalidHexChar(char), - HexParseError, - FeltOutOfField, - InvalidAccountId, -} - -impl fmt::Display for AddrConvError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AddrConvError::NonZeroWordPadding => write!(f, "non-zero word padding"), - AddrConvError::NonZeroBytePrefix => write!(f, "address has non-zero 4-byte prefix"), - AddrConvError::InvalidHexLength => { - write!(f, "invalid hex length (expected 40 hex chars)") - }, - AddrConvError::InvalidHexChar(c) => write!(f, "invalid hex character: {}", c), - AddrConvError::HexParseError => write!(f, "hex parse error"), - AddrConvError::FeltOutOfField => { - write!(f, "packed 64-bit word does not fit in the field") - }, - AddrConvError::InvalidAccountId => write!(f, "invalid AccountId"), - } - } -} - -impl From for AddrConvError { - fn from(_err: HexParseError) -> Self { - AddrConvError::HexParseError - } -} - -// ================================================================================================ -// ETHEREUM ADDRESS -// ================================================================================================ - -/// Represents an Ethereum address format (20 bytes). -/// -/// # Representations used in this module -/// -/// - Raw bytes: `[u8; 20]` in the conventional Ethereum big-endian byte order (`bytes[0]` is the -/// most-significant byte). -/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *little-endian limb order*: -/// - addr0 = bytes[16..19] (least-significant 4 bytes) -/// - addr1 = bytes[12..15] -/// - addr2 = bytes[ 8..11] -/// - addr3 = bytes[ 4.. 7] -/// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) -/// - Embedded AccountId format: `0x00000000 || prefix(8) || suffix(8)`, where: -/// - prefix = (addr3 << 32) | addr2 = bytes[4..11] as a big-endian u64 -/// - suffix = (addr1 << 32) | addr0 = bytes[12..19] as a big-endian u64 -/// -/// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure -/// `Felt::new(u64)` does not reduce mod p (checked explicitly in `to_account_id`). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct EthAddressFormat([u8; 20]); - -impl EthAddressFormat { - // EXTERNAL API - For integrators (Gateway, claim managers, etc.) - // -------------------------------------------------------------------------------------------- - - /// Creates a new [`EthAddressFormat`] from a 20-byte array. - pub const fn new(bytes: [u8; 20]) -> Self { - Self(bytes) - } - - /// Creates an [`EthAddressFormat`] from a hex string (with or without "0x" prefix). - /// - /// # Errors - /// - /// Returns an error if the hex string is invalid or the hex part is not exactly 40 characters. - pub fn from_hex(hex_str: &str) -> Result { - let hex_part = hex_str.strip_prefix("0x").unwrap_or(hex_str); - if hex_part.len() != 40 { - return Err(AddrConvError::InvalidHexLength); - } - - let prefixed_hex = if hex_str.starts_with("0x") { - hex_str.to_string() - } else { - format!("0x{}", hex_str) - }; - - let bytes: [u8; 20] = hex_to_bytes(&prefixed_hex)?; - Ok(Self(bytes)) - } - - /// Creates an [`EthAddressFormat`] from an [`AccountId`]. - /// - /// **External API**: This function is used by integrators (Gateway, claim managers) to convert - /// Miden AccountIds into the Ethereum address format for constructing CLAIM notes or - /// interfacing when calling the Agglayer Bridge function bridgeAsset(). - /// - /// This conversion is infallible: an [`AccountId`] is two felts, and `as_int()` yields `u64` - /// words which we embed as `0x00000000 || prefix(8) || suffix(8)` (big-endian words). - /// - /// # Example - /// ```ignore - /// let destination_address = EthAddressFormat::from_account_id(destination_account_id).into_bytes(); - /// // then construct the CLAIM note with destination_address... - /// ``` - pub fn from_account_id(account_id: AccountId) -> Self { - let felts: [Felt; 2] = account_id.into(); - - let mut out = [0u8; 20]; - out[4..12].copy_from_slice(&felts[0].as_int().to_be_bytes()); - out[12..20].copy_from_slice(&felts[1].as_int().to_be_bytes()); - - Self(out) - } - - /// Returns the raw 20-byte array. - pub const fn as_bytes(&self) -> &[u8; 20] { - &self.0 - } - - /// Converts the address into a 20-byte array. - pub const fn into_bytes(self) -> [u8; 20] { - self.0 - } - - /// Converts the Ethereum address to a hex string (lowercase, 0x-prefixed). - pub fn to_hex(&self) -> String { - bytes_to_hex_string(self.0) - } - - // INTERNAL API - For CLAIM note processing - // -------------------------------------------------------------------------------------------- - - /// Converts the Ethereum address format into an array of 5 [`Felt`] values for MASM processing. - /// - /// **Internal API**: This function is used internally during CLAIM note processing to convert - /// the address format into the MASM `address[5]` representation expected by the - /// `ethereum_address_format_to_account_id` procedure. - /// - /// The returned order matches the MASM `address\[5\]` convention (*little-endian limb order*): - /// - addr0 = bytes[16..19] (least-significant 4 bytes) - /// - addr1 = bytes[12..15] - /// - addr2 = bytes[ 8..11] - /// - addr3 = bytes[ 4.. 7] - /// - addr4 = bytes[ 0.. 3] (most-significant 4 bytes) - /// - /// Each limb is interpreted as a big-endian `u32` and stored in a [`Felt`]. - pub fn to_elements(&self) -> [Felt; 5] { - let mut result = [Felt::ZERO; 5]; - - // i=0 -> bytes[16..20], i=4 -> bytes[0..4] - for (i, felt) in result.iter_mut().enumerate() { - let start = (4 - i) * 4; - let chunk = &self.0[start..start + 4]; - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - *felt = Felt::new(value as u64); - } - - result - } - - /// Converts the Ethereum address format back to an [`AccountId`]. - /// - /// **Internal API**: This function is used internally during CLAIM note processing to extract - /// the original AccountId from the Ethereum address format. It mirrors the functionality of - /// the MASM `ethereum_address_format_to_account_id` procedure. - /// - /// # Errors - /// - /// Returns an error if: - /// - the first 4 bytes are not zero (not in the embedded AccountId format), - /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p, - /// - or the resulting felts do not form a valid [`AccountId`]. - pub fn to_account_id(&self) -> Result { - let (prefix, suffix) = Self::bytes20_to_prefix_suffix(self.0)?; - - // `Felt::new(u64)` may reduce mod p for some u64 values. Mirror the MASM `build_felt` - // safety: construct the felt, then require round-trip equality. - let prefix_felt = Felt::new(prefix); - if prefix_felt.as_int() != prefix { - return Err(AddrConvError::FeltOutOfField); - } - - let suffix_felt = Felt::new(suffix); - if suffix_felt.as_int() != suffix { - return Err(AddrConvError::FeltOutOfField); - } - - AccountId::try_from([prefix_felt, suffix_felt]).map_err(|_| AddrConvError::InvalidAccountId) - } - - // HELPER FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes. - /// Requires the first 4 bytes be zero. - /// Returns prefix and suffix values that match the MASM little-endian limb implementation: - /// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2 - /// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0 - fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddrConvError> { - if bytes[0..4] != [0, 0, 0, 0] { - return Err(AddrConvError::NonZeroBytePrefix); - } - - let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap()); - let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap()); - - Ok((prefix, suffix)) - } -} - -impl fmt::Display for EthAddressFormat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -impl From<[u8; 20]> for EthAddressFormat { - fn from(bytes: [u8; 20]) -> Self { - Self(bytes) - } -} - -impl From for EthAddressFormat { - fn from(account_id: AccountId) -> Self { - EthAddressFormat::from_account_id(account_id) - } -} - -impl From for [u8; 20] { - fn from(addr: EthAddressFormat) -> Self { - addr.0 - } -} From a36bc67f2c65963a9df7174167b0d4d637fc21f0 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 33/37] `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 | 304 +----------------- .../miden-testing/tests/agglayer/bridge_in.rs | 81 +++-- .../tests/agglayer/test_utils.rs | 79 +++++ 8 files changed, 604 insertions(+), 321 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 4c12783065..53fa77e4b4 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_INPUTS = 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_INPUTS # => [num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] - exec.get_destination_account_id - # => [account_id_prefix, account_id_suffix, num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] - - mem_store.0 mem_store.1 - # => [num_output_note_inputs, SERIAL_NUM, SCRIPT_ROOT] - push.OUTPUT_NOTE_INPUTS_MEM_ADDR # => [inputs_ptr = 0, num_output_note_inputs, 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 7020a384a4..197933c711 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, - NoteInputs, - NoteMetadata, - NoteRecipient, - NoteScript, - 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,279 +329,3 @@ pub fn create_existing_agglayer_faucet( .build_existing() .expect("Agglayer faucet account should be valid") } - -// 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_inputs = vec![]; - - // 1) PROOF DATA - // smtProofLocalExitRoot (256 felts) - first SMT proof parameter - claim_inputs.extend(params.smt_proof_local_exit_root); - // smtProofRollupExitRoot (256 felts) - second SMT proof parameter - claim_inputs.extend(params.smt_proof_rollup_exit_root); - - // globalIndex (uint256 as 8 u32 felts) - claim_inputs.extend(params.global_index); - - // mainnetExitRoot (bytes32 as 8 u32 felts) - let mainnet_exit_root_felts = bytes32_to_felts(params.mainnet_exit_root); - claim_inputs.extend(mainnet_exit_root_felts); - - // rollupExitRoot (bytes32 as 8 u32 felts) - let rollup_exit_root_felts = bytes32_to_felts(params.rollup_exit_root); - claim_inputs.extend(rollup_exit_root_felts); - - // 2) LEAF DATA - // originNetwork (uint32 as Felt) - claim_inputs.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_inputs.extend(origin_token_address_felts); - - // destinationNetwork (uint32 as Felt) - claim_inputs.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_inputs.extend(destination_address_felts); - - // amount (uint256 as 8 u32 felts) - claim_inputs.extend(params.amount); - - // metadata (fixed size of 8 felts) - claim_inputs.extend(params.metadata); - - let padding = vec![Felt::ZERO; 4]; - claim_inputs.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_inputs.extend(params.p2id_serial_number); - - // agglayer_faucet_account_id (2 felts: prefix and suffix) - claim_inputs.push(params.agglayer_faucet_account_id.prefix().as_felt()); - claim_inputs.push(params.agglayer_faucet_account_id.suffix()); - - // output note tag - claim_inputs.push(params.output_note_tag.as_u32().into()); - - let inputs = NoteInputs::new(claim_inputs)?; - - 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, - ) -} diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 81392b584d..b1e9d60f07 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::WellKnownNote; 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 = WellKnownNote::P2ID.script(); let p2id_inputs = vec![user_account.id().suffix(), user_account.id().prefix().as_felt()]; let note_inputs = NoteInputs::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 4c5ccae4dcd2c60145cf523f10c4bcba5f979c9c Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Fri, 23 Jan 2026 12:46:47 +0300 Subject: [PATCH 34/37] 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 c2a6099e0e..edaf41e42d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,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 66bf679fe4501e1ae77aff643c0607ca60c5dd51 Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 23 Jan 2026 11:15:02 +0100 Subject: [PATCH 35/37] 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 edaf41e42d..6c6085fcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,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 60ab0aed905672e52d15d06b1b0dc510de0c115c Mon Sep 17 00:00:00 2001 From: Marti Date: Fri, 23 Jan 2026 18:50:34 +0100 Subject: [PATCH 36/37] 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 6c6085fcad..de170a9aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,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 cfdf12333c083aa8d78be8f815300acf85d1eeb6 Mon Sep 17 00:00:00 2001 From: Marti Date: Sat, 24 Jan 2026 11:18:18 +0100 Subject: [PATCH 37/37] 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(()) +}