Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
[submodule "crates/miden-agglayer/solidity-compat/lib/agglayer-contracts"]
path = crates/miden-agglayer/solidity-compat/lib/agglayer-contracts
url = https://github.com/agglayer/agglayer-contracts
[submodule "crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable"]
path = crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git
branch = release-v4.9
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Fo
cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVectors
cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateCanonicalZeros
cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateVerificationProofData
cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateLeafValueVectors

# --- benchmarking --------------------------------------------------------------------------------

Expand Down
148 changes: 145 additions & 3 deletions crates/miden-agglayer/asm/bridge/crypto_utils.masm
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,21 @@ type MemoryAddress = u32
# CONSTANTS
# =================================================================================================

# the number of bytes in the leaf data to hash (matches Solidity's abi.encodePacked output)
const LEAF_DATA_BYTES = 113

# the number of words (4 felts each) in the advice map data
const LEAF_DATA_NUM_WORDS = 8

# the memory address where leaf data is stored
const LEAF_DATA_START_PTR = 0

# the local memory offset where we store the leaf data start pointer
const PACKING_START_PTR_LOCAL= 0

# the number of elements to pack (113 bytes = 29 elements, rounding up from 28.25)
const PACKED_DATA_NUM_ELEMENTS = 29

# The offset of the first half of the current Keccak256 hash value in the local memory of the
# `calculate_root` procedure.
const CUR_HASH_LO_LOCAL = 0
Expand All @@ -28,7 +39,8 @@ const CUR_HASH_HI_LOCAL = 4
# PUBLIC INTERFACE
# =================================================================================================

#! Given the leaf data key returns the leaf value.
#! Given the leaf data key, loads the leaf data from advice map to memory, packs the data in-place,
#! and computes the leaf value by hashing the packed bytes.
#!
#! Inputs:
#! Operand stack: [LEAF_DATA_KEY]
Expand All @@ -40,7 +52,7 @@ const CUR_HASH_HI_LOCAL = 4
#! destinationNetwork[1], // Destination network identifier (1 felt, uint32)
#! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts)
#! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts)
#! metadata[8], // ABI encoded metadata (8 felts, fixed size)
#! metadata_hash[8], // Metadata hash (8 felts, bytes32 as 8 u32 felts)
#! padding[3], // padding (3 felts) - not used in the hash
#! ],
#! }
Expand All @@ -55,7 +67,26 @@ pub proc get_leaf_value(leaf_data_key: BeWord) -> DoubleWord
exec.mem::pipe_preimage_to_memory drop
# => []

push.LEAF_DATA_BYTES push.LEAF_DATA_START_PTR
# compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR
push.LEAF_DATA_START_PTR
exec.compute_leaf_value
# => [LEAF_VALUE[8]]
end

#! Given a memory address where the unpacked leaf data starts, packs the leaf data in-place, and
#! computes the leaf value by hashing the packed bytes.
#!
#! Inputs: [LEAF_DATA_START_PTR]
#! Outputs: [LEAF_VALUE[8]]
#!
#! Invocation: exec
pub proc compute_leaf_value(leaf_data_start_ptr: MemoryAddress) -> DoubleWord
dup
# => [leaf_data_start_ptr, leaf_data_start_ptr]
exec.pack_leaf_data
# => [leaf_data_start_ptr]

push.LEAF_DATA_BYTES swap
# => [start_ptr, byte_len]

exec.keccak256::hash_bytes
Expand Down Expand Up @@ -197,3 +228,114 @@ proc calculate_root(
loc_loadw_be.CUR_HASH_HI_LOCAL swapw loc_loadw_be.CUR_HASH_LO_LOCAL
# => [ROOT_LO, ROOT_HI]
end

#! Packs the raw leaf data by shifting left 3 bytes to match Solidity's abi.encodePacked format.
#!
#! The raw data has leafType occupying 4 bytes (as a u32 felt) but Solidity's abi.encodePacked
#! only uses 1 byte for uint8 leafType. This procedure shifts all data left by 3 bytes so that:
#! - Byte 0: leafType (1 byte)
#! - Bytes 1-4: originNetwork (4 bytes)
#! - etc.
#!
#! The Keccak precompile expects u32 values packed in little-endian byte order.
#! For each packed element, we drop the leading 3 bytes and rebuild the u32 so that
#! bytes [b0, b1, b2, b3] map to u32::from_le_bytes([b0, b1, b2, b3]).
#! With little-endian input limbs, the first byte comes from the MSB of `curr` and
#! the next three bytes come from the LSBs of `next`:
#! packed = ((curr >> 24) & 0xFF)
#! | (next & 0xFF) << 8
#! | ((next >> 8) & 0xFF) << 16
#! | ((next >> 16) & 0xFF) << 24
#!
#! To help visualize the packing process, consider that each field element represents a 4-byte
#! value [u8; 4] (LE).
#! Memory before is:
#! ptr+0: 1 felt: [a, b, c, d]
#! ptr+1: 1 felt: [e, f, g, h]
#! ptr+2..6: 5 felts: [i, j, k, l, m, ...]
#!
#! Memory after:
#! ptr+0: 1 felt: [d, e, f, g]
#! ptr+1: 1 felt: [h, i, j, k]
#! ptr+2..6: 5 felts: [l, ...]
#!
#! Inputs: [leaf_data_start_ptr]
#! Outputs: []
#!
#! Invocation: exec
Comment on lines 240 to 265
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it would make a bit more sense if you outlined the state of memory before and after calling this procedure. Although it looks like the procedure does not take any inputs / outputs on the stack, it does but via memory. It would also make it more concrete what the before / after of memory looks like when calling this proc.

I think it is fine to use global memory like this because this procedure will only ever be called in the context of a network transaction, so it won't ever be used in a "user" context.

I think on all of my PRs where I had procedures like this which took global memory as an input, I think @bobbinth & @PhilippGackstatter thought it would be better to pass the data via the AdviceStack, that being said, I think its justified to use global memory like this because we are preparing the data before calling the Keccak precompile which requires the data to be in global memory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good call, will add a paragraph on the layout

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually I cherry-picked chore: refactor into get_and compute_ leaf_value, so that this procedure actually becomes parametrized, and no longer implicitly relies on hardcoded memory poiners.

@locals(1) # start_ptr
pub proc pack_leaf_data(leaf_data_start_ptr: MemoryAddress)
loc_store.PACKING_START_PTR_LOCAL
# => []

# initialize loop counter to 0
push.0

# push initial condition (true) to enter the loop
push.1

# loop through elements from 0 to PACKED_DATA_NUM_ELEMENTS - 1 (28)
while.true
# => [counter]

# compute source address: packing_start_ptr + counter
dup loc_load.PACKING_START_PTR_LOCAL add
# => [src_addr, counter]

# load current element
mem_load
# => [curr_elem, counter]

# extract MSB (upper 8 bits) which becomes the first little-endian byte
dup u32shr.24
# => [curr_msb, curr_elem, counter]

# compute source address for next element (counter + 1)
dup.2 loc_load.PACKING_START_PTR_LOCAL add add.1
# => [next_src_addr, curr_lsb, curr_elem, counter]

# load next element
mem_load
# => [next_elem, curr_lsb, curr_elem, counter]

# keep curr_msb on top for combination
swap
# => [curr_msb, next_elem, curr_elem, counter]

# add next byte0 (bits 0..7) into bits 8..15
dup.1 u32and.0xFF u32shl.8 u32or
# => [partial, next_elem, curr_elem, counter]

# add next byte1 (bits 8..15) into bits 16..23
dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or
# => [partial, next_elem, curr_elem, counter]

# add next byte2 (bits 16..23) into bits 24..31
dup.1 u32shr.16 u32and.0xFF u32shl.24 u32or
# => [packed_elem, next_elem, curr_elem, counter]

# drop the next and current elements (no longer needed)
movdn.2 drop drop
# => [packed_elem, counter]

# compute destination address: packing_start_ptr + counter (in-place)
dup.1 loc_load.PACKING_START_PTR_LOCAL add
# => [dest_addr, packed_elem, counter]

# store packed element
mem_store
# => [counter]

# increment counter
add.1
# => [counter + 1]

# check if we should continue (counter < PACKED_DATA_NUM_ELEMENTS)
dup push.PACKED_DATA_NUM_ELEMENTS lt
# => [should_continue, counter]
end
# => [counter]

drop
# => []
end
80 changes: 58 additions & 22 deletions crates/miden-agglayer/asm/bridge/eth_address.masm
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,37 @@ const U32_MAX=4294967295
const TWO_POW_32=4294967296

const ERR_NOT_U32="address limb is not u32"
const ERR_ADDR4_NONZERO="most-significant 4 bytes (addr4) must be zero"
const ERR_MSB_NONZERO="most-significant 4 bytes must be zero for AccountId"
const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field"


# ETHEREUM ADDRESS PROCEDURES
# =================================================================================================

#! Swaps byte order in a u32 limb (LE <-> BE).
#!
#! Inputs: [value]
#! Outputs: [swapped]
proc swap_u32_bytes
# part0 = (value & 0xFF) << 24
dup u32and.0xFF u32shl.24
# => [value, part0]

# part1 = ((value >> 8) & 0xFF) << 16
dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or
# => [value, part01]

# part2 = ((value >> 16) & 0xFF) << 8
dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or
# => [value, part012]

# part3 = (value >> 24)
dup.1 u32shr.24 u32or
# => [value, swapped]

swap drop
# => [swapped]
end

#! Builds a single felt from two u32 limbs (little-endian limb order).
#! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element.
#! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping
Expand All @@ -25,22 +49,29 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field"
proc build_felt
# --- validate u32 limbs ---
u32assert2.err=ERR_NOT_U32
# => [lo_be, hi_be]

# limbs are little-endian bytes; swap to big-endian for building account ID
exec.swap_u32_bytes
swap
exec.swap_u32_bytes
swap
# => [lo, hi]

# keep copies for the overflow check
dup.1 dup.1
# => [lo, hi, lo, hi]
# => [lo_be, hi_be, lo_be, hi_be]

# felt = (hi * 2^32) + lo
swap
push.TWO_POW_32 mul
add
# => [felt, lo, hi]
# => [felt, lo_be, hi_be]

# ensure no reduction mod p happened:
# split felt back into (hi, lo) and compare to inputs
dup u32split
# => [hi2, lo2, felt, lo, hi]
# => [hi2, lo2, felt, lo_be, hi_be]

movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD
# => [lo2, felt, lo]
Expand All @@ -51,37 +82,42 @@ end

#! Converts an Ethereum address format (address[5] type) back into an AccountId [prefix, suffix] type.
#!
#! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*:
#! addr0 = bytes[16..19] (least-significant 4 bytes)
#! addr1 = bytes[12..15]
#! addr2 = bytes[ 8..11]
#! addr3 = bytes[ 4.. 7]
#! addr4 = bytes[ 0.. 3] (most-significant 4 bytes)
#! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *big-endian limb order*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we going to have to change to make this little endian anyways with the new VM version? 🥲

#! (matching Solidity ABI encoding). Each limb encodes its 4 bytes in little-endian order:
#! limb0 = bytes[0..4] (most-significant 4 bytes, must be zero for AccountId)
#! limb1 = bytes[4..8]
#! limb2 = bytes[8..12]
#! limb3 = bytes[12..16]
#! limb4 = bytes[16..20] (least-significant 4 bytes)
#!
#! The most-significant 4 bytes must be zero for a valid AccountId conversion (addr4 == 0).
#! The most-significant 4 bytes must be zero for a valid AccountId conversion (be0 == 0).
#! The remaining 16 bytes are treated as two 8-byte words (conceptual u64 values):
#! prefix = (addr3 << 32) | addr2 # bytes[4..11]
#! suffix = (addr1 << 32) | addr0 # bytes[12..19]
#! prefix = (bswap(limb1) << 32) | bswap(limb2) # bytes[4..12]
#! suffix = (bswap(limb3) << 32) | bswap(limb4) # bytes[12..20]
#!
#! These 8-byte words are represented as field elements by packing two u32 limbs into a felt.
#! The packing is done via build_felt, which validates limbs are u32 and checks the packed value
#! did not reduce mod p (i.e. the word fits in the field).
#!
#! Inputs: [addr0, addr1, addr2, addr3, addr4]
#! Inputs: [limb0, limb1, limb2, limb3, limb4]
#! Outputs: [prefix, suffix]
#!
#! Invocation: exec
pub proc to_account_id
# addr4 must be 0 (most-significant limb)
movup.4
eq.0 assert.err=ERR_ADDR4_NONZERO
# => [addr0, addr1, addr2, addr3]
# limb0 must be 0 (most-significant limb, on top)
assertz.err=ERR_MSB_NONZERO
# => [limb1, limb2, limb3, limb4]

# Reorder for suffix = build_felt(limb4, limb3) where limb4=lo, limb3=hi
movup.2 movup.3
# => [limb4, limb3, limb1, limb2]

exec.build_felt
# => [suffix, addr2, addr3]
# => [suffix, limb1, limb2]

movdn.2
# => [addr2, addr3, suffix]
# Reorder for prefix = build_felt(limb2, limb1) where limb2=lo, limb1=hi
swap movup.2
# => [limb2, limb1, suffix]

exec.build_felt
# => [prefix, suffix]
Expand Down
5 changes: 4 additions & 1 deletion crates/miden-agglayer/solidity-compat/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ out = "out"
solc = "0.8.20"
src = "src"

remappings = ["@agglayer/=lib/agglayer-contracts/contracts/"]
remappings = [
"@agglayer/=lib/agglayer-contracts/contracts/",
"@openzeppelin/contracts-upgradeable4/=lib/openzeppelin-contracts-upgradeable/contracts/",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the OZ lib & submodule?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my reply here

]

# Emit extra output for test vector generation
ffi = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"amount": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000",
"destination_address": "0xD9b20Fe633b609B01081aD0428e81f8Dd604F5C5",
"destination_network": 7,
"leaf_type": 0,
"leaf_value": "0xb67e42971034605367b7e92d1ad1d4648c3ffe0bea9b08115cd9aa2e616b2f88",
"metadata_hash": "0x6c7a91a5fb41dee8f0bc1c86b5587334583186f14acfa253e2f7c2833d1d6fdf",
"origin_network": 0,
"origin_token_address": "0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7"
}
Loading