From 46143c8f0875fdd51f6a7c5d659cb23da1c29c02 Mon Sep 17 00:00:00 2001 From: riemann Date: Thu, 8 Jan 2026 19:14:48 -0500 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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 1567d8907e1de2f02cfba6a69c6a02ba288d90d3 Mon Sep 17 00:00:00 2001 From: riemann Date: Tue, 20 Jan 2026 11:09:03 -0500 Subject: [PATCH 29/29] 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 - } -}