-
Notifications
You must be signed in to change notification settings - Fork 115
feat(AggLayer claim e2e #2): byte packing from felts #2401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d03e4de
68efbc1
5ef4c87
5993b3c
810995f
cd21c49
144b069
795f01a
5db378a
afae1dc
eb35e68
4b354b4
c7228fc
dd106c9
8ec5d14
509248e
b754eb2
ea8d6d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,10 +13,21 @@ type MemoryAddress = u32 | |
| # CONSTANTS | ||
| # ================================================================================================= | ||
|
|
||
| # the number of bytes in the leaf data to hash (matches Solidity's abi.encodePacked output) | ||
| const LEAF_DATA_BYTES = 113 | ||
|
|
||
| # the number of words (4 felts each) in the advice map data | ||
| const LEAF_DATA_NUM_WORDS = 8 | ||
|
|
||
| # the memory address where leaf data is stored | ||
| const LEAF_DATA_START_PTR = 0 | ||
|
|
||
| # the local memory offset where we store the leaf data start pointer | ||
| const PACKING_START_PTR_LOCAL= 0 | ||
|
|
||
| # the number of elements to pack (113 bytes = 29 elements, rounding up from 28.25) | ||
| const PACKED_DATA_NUM_ELEMENTS = 29 | ||
|
|
||
| # The offset of the first half of the current Keccak256 hash value in the local memory of the | ||
| # `calculate_root` procedure. | ||
| const CUR_HASH_LO_LOCAL = 0 | ||
|
|
@@ -28,7 +39,8 @@ const CUR_HASH_HI_LOCAL = 4 | |
| # PUBLIC INTERFACE | ||
| # ================================================================================================= | ||
|
|
||
| #! Given the leaf data key returns the leaf value. | ||
| #! Given the leaf data key, loads the leaf data from advice map to memory, packs the data in-place, | ||
| #! and computes the leaf value by hashing the packed bytes. | ||
| #! | ||
| #! Inputs: | ||
| #! Operand stack: [LEAF_DATA_KEY] | ||
|
|
@@ -40,7 +52,7 @@ const CUR_HASH_HI_LOCAL = 4 | |
| #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) | ||
| #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) | ||
| #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) | ||
| #! metadata[8], // ABI encoded metadata (8 felts, fixed size) | ||
| #! metadata_hash[8], // Metadata hash (8 felts, bytes32 as 8 u32 felts) | ||
| #! padding[3], // padding (3 felts) - not used in the hash | ||
| #! ], | ||
| #! } | ||
|
|
@@ -55,7 +67,26 @@ pub proc get_leaf_value(leaf_data_key: BeWord) -> DoubleWord | |
| exec.mem::pipe_preimage_to_memory drop | ||
| # => [] | ||
|
|
||
| push.LEAF_DATA_BYTES push.LEAF_DATA_START_PTR | ||
| # compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR | ||
| push.LEAF_DATA_START_PTR | ||
| exec.compute_leaf_value | ||
| # => [LEAF_VALUE[8]] | ||
| end | ||
|
|
||
| #! Given a memory address where the unpacked leaf data starts, packs the leaf data in-place, and | ||
| #! computes the leaf value by hashing the packed bytes. | ||
| #! | ||
| #! Inputs: [LEAF_DATA_START_PTR] | ||
| #! Outputs: [LEAF_VALUE[8]] | ||
| #! | ||
| #! Invocation: exec | ||
| pub proc compute_leaf_value(leaf_data_start_ptr: MemoryAddress) -> DoubleWord | ||
| dup | ||
| # => [leaf_data_start_ptr, leaf_data_start_ptr] | ||
| exec.pack_leaf_data | ||
| # => [leaf_data_start_ptr] | ||
|
|
||
| push.LEAF_DATA_BYTES swap | ||
| # => [start_ptr, byte_len] | ||
|
|
||
| exec.keccak256::hash_bytes | ||
|
|
@@ -197,3 +228,114 @@ proc calculate_root( | |
| loc_loadw_be.CUR_HASH_HI_LOCAL swapw loc_loadw_be.CUR_HASH_LO_LOCAL | ||
| # => [ROOT_LO, ROOT_HI] | ||
| end | ||
|
|
||
| #! Packs the raw leaf data by shifting left 3 bytes to match Solidity's abi.encodePacked format. | ||
| #! | ||
| #! The raw data has leafType occupying 4 bytes (as a u32 felt) but Solidity's abi.encodePacked | ||
| #! only uses 1 byte for uint8 leafType. This procedure shifts all data left by 3 bytes so that: | ||
| #! - Byte 0: leafType (1 byte) | ||
| #! - Bytes 1-4: originNetwork (4 bytes) | ||
| #! - etc. | ||
| #! | ||
| #! The Keccak precompile expects u32 values packed in little-endian byte order. | ||
| #! For each packed element, we drop the leading 3 bytes and rebuild the u32 so that | ||
| #! bytes [b0, b1, b2, b3] map to u32::from_le_bytes([b0, b1, b2, b3]). | ||
| #! With little-endian input limbs, the first byte comes from the MSB of `curr` and | ||
| #! the next three bytes come from the LSBs of `next`: | ||
| #! packed = ((curr >> 24) & 0xFF) | ||
| #! | (next & 0xFF) << 8 | ||
| #! | ((next >> 8) & 0xFF) << 16 | ||
| #! | ((next >> 16) & 0xFF) << 24 | ||
| #! | ||
| #! To help visualize the packing process, consider that each field element represents a 4-byte | ||
| #! value [u8; 4] (LE). | ||
| #! Memory before is: | ||
| #! ptr+0: 1 felt: [a, b, c, d] | ||
| #! ptr+1: 1 felt: [e, f, g, h] | ||
| #! ptr+2..6: 5 felts: [i, j, k, l, m, ...] | ||
| #! | ||
| #! Memory after: | ||
| #! ptr+0: 1 felt: [d, e, f, g] | ||
| #! ptr+1: 1 felt: [h, i, j, k] | ||
| #! ptr+2..6: 5 felts: [l, ...] | ||
| #! | ||
| #! Inputs: [leaf_data_start_ptr] | ||
| #! Outputs: [] | ||
| #! | ||
| #! Invocation: exec | ||
|
Comment on lines
240
to
265
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes good call, will add a paragraph on the layout
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
mmagician marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,13 +8,37 @@ const U32_MAX=4294967295 | |
| const TWO_POW_32=4294967296 | ||
|
|
||
| const ERR_NOT_U32="address limb is not u32" | ||
| const ERR_ADDR4_NONZERO="most-significant 4 bytes (addr4) must be zero" | ||
| const ERR_MSB_NONZERO="most-significant 4 bytes must be zero for AccountId" | ||
| const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" | ||
|
|
||
|
|
||
| # ETHEREUM ADDRESS PROCEDURES | ||
| # ================================================================================================= | ||
|
|
||
| #! Swaps byte order in a u32 limb (LE <-> BE). | ||
| #! | ||
| #! Inputs: [value] | ||
| #! Outputs: [swapped] | ||
| proc swap_u32_bytes | ||
| # part0 = (value & 0xFF) << 24 | ||
| dup u32and.0xFF u32shl.24 | ||
| # => [value, part0] | ||
|
|
||
| # part1 = ((value >> 8) & 0xFF) << 16 | ||
| dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or | ||
| # => [value, part01] | ||
|
|
||
| # part2 = ((value >> 16) & 0xFF) << 8 | ||
| dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or | ||
| # => [value, part012] | ||
|
|
||
| # part3 = (value >> 24) | ||
| dup.1 u32shr.24 u32or | ||
| # => [value, swapped] | ||
|
|
||
| swap drop | ||
| # => [swapped] | ||
| end | ||
|
|
||
| #! Builds a single felt from two u32 limbs (little-endian limb order). | ||
| #! Conceptually, this is packing a 64-bit word (lo + (hi << 32)) into a field element. | ||
| #! This proc additionally verifies that the packed value did *not* reduce mod p by round-tripping | ||
|
|
@@ -25,22 +49,29 @@ const ERR_FELT_OUT_OF_FIELD="combined u64 doesn't fit in field" | |
| proc build_felt | ||
| # --- validate u32 limbs --- | ||
| u32assert2.err=ERR_NOT_U32 | ||
| # => [lo_be, hi_be] | ||
|
|
||
| # limbs are little-endian bytes; swap to big-endian for building account ID | ||
| exec.swap_u32_bytes | ||
| swap | ||
| exec.swap_u32_bytes | ||
| swap | ||
| # => [lo, hi] | ||
|
|
||
| # keep copies for the overflow check | ||
| dup.1 dup.1 | ||
| # => [lo, hi, lo, hi] | ||
| # => [lo_be, hi_be, lo_be, hi_be] | ||
|
|
||
| # felt = (hi * 2^32) + lo | ||
| swap | ||
| push.TWO_POW_32 mul | ||
| add | ||
| # => [felt, lo, hi] | ||
| # => [felt, lo_be, hi_be] | ||
|
|
||
| # ensure no reduction mod p happened: | ||
| # split felt back into (hi, lo) and compare to inputs | ||
| dup u32split | ||
| # => [hi2, lo2, felt, lo, hi] | ||
| # => [hi2, lo2, felt, lo_be, hi_be] | ||
|
|
||
| movup.4 assert_eq.err=ERR_FELT_OUT_OF_FIELD | ||
| # => [lo2, felt, lo] | ||
|
|
@@ -51,37 +82,42 @@ end | |
|
|
||
| #! Converts an Ethereum address format (address[5] type) back into an AccountId [prefix, suffix] type. | ||
| #! | ||
| #! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *little-endian limb order*: | ||
| #! addr0 = bytes[16..19] (least-significant 4 bytes) | ||
| #! addr1 = bytes[12..15] | ||
| #! addr2 = bytes[ 8..11] | ||
| #! addr3 = bytes[ 4.. 7] | ||
| #! addr4 = bytes[ 0.. 3] (most-significant 4 bytes) | ||
| #! The Ethereum address format is represented as 5 u32 limbs (20 bytes total) in *big-endian limb order* | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). | ||
mmagician marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| #! 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] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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/", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the OZ lib & submodule?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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" | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.