diff --git a/.gitmodules b/.gitmodules index b02c269a3f..0333b7b402 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe91da527..ccb779a11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Added AggLayer faucet registry to bridge account with conversion metadata, `CONFIG_AGG_BRIDGE` note for faucet registration, and FPI-based asset conversion in `bridge_out` ([#2426](https://github.com/0xMiden/miden-base/pull/2426)). - Enable `CodeBuilder` to add advice map entries to compiled scripts ([#2275](https://github.com/0xMiden/miden-base/pull/2275)). - Added `BlockNumber::MAX` constant to represent the maximum block number ([#2324](https://github.com/0xMiden/miden-base/pull/2324)). - Added single-word `Array` standard ([#2203](https://github.com/0xMiden/miden-base/pull/2203)). @@ -12,6 +13,8 @@ - Implemented verification of AggLayer deposits (claims) against GER ([#2295](https://github.com/0xMiden/miden-base/pull/2295), [#2288](https://github.com/0xMiden/miden-base/pull/2288)). - Added `SignedBlock` struct ([#2355](https://github.com/0xMiden/miden-base/pull/2235)). - Added `PackageKind` and `ProcedureExport` ([#2358](https://github.com/0xMiden/miden-base/pull/2358)). +- Changed GER storage to a map ([#2388](https://github.com/0xMiden/miden-base/pull/2388)). +- Implemented `assert_valid_ger` procedure for verifying GER against storage ([#2388](https://github.com/0xMiden/miden-base/pull/2388)). - [BREAKING] Added `get_asset` and `get_initial_asset` kernel procedures and removed `get_balance`, `get_initial_balance` and `has_non_fungible_asset` kernel procedures ([#2369](https://github.com/0xMiden/miden-base/pull/2369)). - Introduced `TokenMetadata` type to encapsulate fungible faucet metadata ([#2344](https://github.com/0xMiden/miden-base/issues/2344)). - Added `StandardNote::from_script_root()` and `StandardNote::name()` methods, and exposed `NoteType` `PUBLIC`/`PRIVATE` masks as public constants ([#2411](https://github.com/0xMiden/miden-base/pull/2411)). @@ -39,6 +42,11 @@ - [BREAKING] Updated note tag length to support up to 32 bits ([#2329](https://github.com/0xMiden/miden-base/pull/2329)). - [BREAKING] Moved standard note code into individual note modules ([#2363](https://github.com/0xMiden/miden-base/pull/2363)). - [BREAKING] Added `miden::standards::note_tag` module for account target note tags ([#2366](https://github.com/0xMiden/miden-base/pull/2366)). +- Unified the underlying representation of `ExitRoot` and `SmtNode` and use type aliases ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). +- [BREAKING] Moved padding to the end of `CLAIM` `NoteStorage` layout ([#2405](https://github.com/0xMiden/miden-base/pull/2405)). + +### Fixes +- Fixed byte-to-felt conversion for `ExitRoot` and `SmtNode`'s `to_elements()` method ([#2387](https://github.com/0xMiden/miden-base/pull/2387)). ## 0.13.3 (2026-01-27) diff --git a/Cargo.lock b/Cargo.lock index ed558bfe4e..c5db704828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1392,7 +1392,9 @@ dependencies = [ "miden-protocol", "miden-standards", "miden-utils-sync", + "primitive-types", "regex", + "thiserror", "walkdir", ] diff --git a/Makefile b/Makefile index b2d5c45df3..e80f600592 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,8 @@ 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 + cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateClaimAssetVectors # --- benchmarking -------------------------------------------------------------------------------- diff --git a/crates/miden-agglayer/Cargo.toml b/crates/miden-agglayer/Cargo.toml index 7541b7ea8d..70ca24bc7a 100644 --- a/crates/miden-agglayer/Cargo.toml +++ b/crates/miden-agglayer/Cargo.toml @@ -23,10 +23,15 @@ testing = ["miden-protocol/testing"] # Miden dependencies miden-assembly = { workspace = true } miden-core = { workspace = true } +miden-core-lib = { workspace = true } miden-protocol = { workspace = true } miden-standards = { workspace = true } miden-utils-sync = { workspace = true } +# Third-party dependencies +primitive-types = { workspace = true } +thiserror = { workspace = true } + [dev-dependencies] miden-agglayer = { features = ["testing"], path = "." } diff --git a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm index ddf0e4b99c..7b7bd43e93 100644 --- a/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm +++ b/crates/miden-agglayer/asm/bridge/agglayer_faucet.masm @@ -1,4 +1,6 @@ use miden::agglayer::bridge_in +use miden::core::sys +use miden::agglayer::utils use miden::agglayer::asset_conversion use miden::agglayer::eth_address use miden::protocol::active_account @@ -16,6 +18,12 @@ use miden::core::word # The slot in this component's storage layout where the bridge account ID is stored. const BRIDGE_ID_SLOT = word("miden::agglayer::faucet") +# Storage slots for conversion metadata. +# Slot 1: [addr_felt0, addr_felt1, addr_felt2, addr_felt3] — first 4 felts of origin token address +const CONVERSION_INFO_1_SLOT = word("miden::agglayer::faucet::conversion_info_1") +# Slot 2: [addr_felt4, origin_network, scale, 0] — remaining address felt + origin network + scale +const CONVERSION_INFO_2_SLOT = word("miden::agglayer::faucet::conversion_info_2") + const PROOF_DATA_WORD_LEN = 134 const LEAF_DATA_WORD_LEN = 8 const OUTPUT_NOTE_DATA_WORD_LEN = 2 @@ -30,17 +38,33 @@ const LEAF_DATA_KEY_MEM_ADDR = 704 const OUTPUT_NOTE_DATA_MEM_ADDR = 708 const CLAIM_NOTE_DATA_MEM_ADDR = 712 -const OUTPUT_NOTE_INPUTS_MEM_ADDR = 0 +const OUTPUT_NOTE_STORAGE_MEM_ADDR = 0 const OUTPUT_NOTE_TAG_MEM_ADDR = 574 const OUTPUT_NOTE_SERIAL_NUM_MEM_ADDR = 568 -const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 552 -const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 556 - -const DESTINATION_ADDRESS_0 = 547 -const DESTINATION_ADDRESS_1 = 548 -const DESTINATION_ADDRESS_2 = 549 -const DESTINATION_ADDRESS_3 = 550 -const DESTINATION_ADDRESS_4 = 551 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = 549 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = 550 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 = 551 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 = 552 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = 553 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = 554 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = 555 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = 556 + +const DESTINATION_ADDRESS_0 = 544 +const DESTINATION_ADDRESS_1 = 545 +const DESTINATION_ADDRESS_2 = 546 +const DESTINATION_ADDRESS_3 = 547 +const DESTINATION_ADDRESS_4 = 548 + +# Memory locals in claim +const CLAIM_PREFIX_MEM_LOC = 5 +const CLAIM_SUFFIX_MEM_LOC = 6 +const CLAIM_AMOUNT_MEM_LOC_0 = 0 +const CLAIM_AMOUNT_MEM_LOC_1 = 4 + +# Memory locals in build_p2id_output_note +const BUILD_P2ID_AMOUNT_MEM_LOC_0 = 0 +const BUILD_P2ID_AMOUNT_MEM_LOC_1 = 4 # P2ID output note constants const P2ID_NOTE_NUM_STORAGE_ITEMS = 2 @@ -54,6 +78,120 @@ const P2ID_OUTPUT_NOTE_AMOUNT_MEM_PTR = 611 const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" +# CONVERSION METADATA HELPERS +# ================================================================================================= + +#! Returns the origin token address (5 felts) from faucet conversion storage. +#! +#! Reads conversion_info_1 (first 4 felts of address) and conversion_info_2 (5th felt) +#! from storage. +#! +#! Inputs: [] +#! Outputs: [addr0, addr1, addr2, addr3, addr4] +#! +#! Invocation: exec +pub proc get_origin_token_address + push.CONVERSION_INFO_1_SLOT[0..2] + exec.active_account::get_item + # => [addr3, addr2, addr1, addr0] + exec.word::reverse + # => [addr0, addr1, addr2, addr3] + + # Read slot 2: [0, scale, origin_network, addr4] + push.CONVERSION_INFO_2_SLOT[0..2] + exec.active_account::get_item + # => [0, scale, origin_network, addr4, addr0, addr1, addr2, addr3] + + # Keep only addr4 from slot 2 and append it after slot 1 limbs + drop drop drop + movdn.4 + # => [addr0, addr1, addr2, addr3, addr4] +end + +#! Returns the origin network identifier from faucet conversion storage. +#! +#! Inputs: [] +#! Outputs: [origin_network] +#! +#! Invocation: exec +pub proc get_origin_network + push.CONVERSION_INFO_2_SLOT[0..2] + exec.active_account::get_item + # => [0, scale, origin_network, addr4] + + drop drop swap drop + # => [origin_network] +end + +#! Returns the scale factor from faucet conversion storage. +#! +#! Inputs: [] +#! Outputs: [scale] +#! +#! Invocation: exec +pub proc get_scale + push.CONVERSION_INFO_2_SLOT[0..2] + exec.active_account::get_item + # => [0, scale, origin_network, addr4] + + drop movdn.2 drop drop + # => [scale] +end + +#! Converts a native Miden asset amount to origin asset data using the stored +#! conversion metadata (origin_token_address, origin_network, and scale). +#! +#! This procedure is intended to be called via FPI from the bridge account. +#! It reads the faucet's conversion metadata from storage, scales the native amount +#! to U256 format, and returns the result along with origin token address and network. +#! +#! Inputs: [amount, pad(15)] +#! Outputs: [AMOUNT_U256[0], AMOUNT_U256[1], addr0, addr1, addr2, addr3, addr4, origin_network, pad(2)] +#! +#! Where: +#! - amount: The native Miden asset amount +#! - AMOUNT_U256: The scaled amount as 8 u32 limbs (little-endian U256) +#! - addr0..addr4: Origin token address (5 felts, u32 limbs) +#! - origin_network: Origin network identifier +#! +#! Invocation: call +pub proc asset_to_origin_asset + # => [amount, pad(15)] + + # Step 1: Get scale from storage + exec.get_scale + # => [scale, amount, pad(15)] + swap + # => [amount, scale, pad(15)] + + # Step 2: Scale amount to U256 + exec.asset_conversion::scale_native_amount_to_u256 + exec.asset_conversion::reverse_limbs_and_change_byte_endianness + # => [U256_LO(4), U256_HI(4), pad(15)] + + # Step 3: Get origin token address + exec.get_origin_token_address + # => [addr0, addr1, addr2, addr3, addr4, U256_LO(4), U256_HI(4), pad(15)] + + # Move address below the U256 amount + repeat.5 movdn.12 end + # => [U256_LO(4), U256_HI(4), addr0, addr1, addr2, addr3, addr4, pad(15)] + + # Step 4: Get origin network + exec.get_origin_network + exec.utils::swap_u32_bytes + # => [origin_network, U256_LO(4), U256_HI(4), addr0..addr4, pad(15)] + + # Move origin_network after the address fields + movdn.13 + # => [U256_LO(4), U256_HI(4), addr0, addr1, addr2, addr3, addr4, origin_network, pad(15)] + + exec.sys::truncate_stack +end + +# CLAIM PROCEDURES +# ================================================================================================= + #! Inputs: [LEAF_DATA_KEY, PROOF_DATA_KEY] #! Outputs: [] #! @@ -86,8 +224,14 @@ end # Inputs: [] # Outputs: [U256[0], U256[1]] proc get_raw_claim_amount - padw mem_loadw_be.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 - padw mem_loadw_be.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 + mem_load.OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 end # Inputs: [U256[0], U256[1]] @@ -151,11 +295,19 @@ end #! It reads the destination account ID, amount, and other note parameters from memory to construct #! the output note. #! -#! Inputs: [] +#! Inputs: [prefix, suffix, AMOUNT[0], AMOUNT[1]] #! Outputs: [] #! #! Note: This procedure will be refactored in a follow-up to use leaf data to build the output note. +@locals(8) proc build_p2id_output_note + # write destination account id into memory for use in note::build_recipient + push.OUTPUT_NOTE_STORAGE_MEM_ADDR add.1 mem_store mem_store.OUTPUT_NOTE_STORAGE_MEM_ADDR + + # store amount in memory locals for use in faucets::distribute + loc_storew_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 dropw loc_storew_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 dropw + # => [pad(16)] + # Build P2ID output note procref.::miden::standards::notes::p2id::main # => [SCRIPT_ROOT] @@ -166,15 +318,8 @@ proc build_p2id_output_note push.P2ID_NOTE_NUM_STORAGE_ITEMS # => [note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - push.OUTPUT_NOTE_INPUTS_MEM_ADDR - # => [storage_ptr = 0, note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - - exec.get_destination_account_id_data - # => [prefix, suffix] - - # Write destination account id into memory - mem_store.1 mem_store.0 - # => [] + push.OUTPUT_NOTE_STORAGE_MEM_ADDR + # => [storage_ptr, note_num_storage_items, SERIAL_NUM, SCRIPT_ROOT] exec.note::build_recipient # => [RECIPIENT] @@ -185,11 +330,10 @@ proc build_p2id_output_note mem_load.OUTPUT_NOTE_TAG_MEM_ADDR # => [tag, RECIPIENT] - exec.get_raw_claim_amount - # => [AMOUNT[1], AMOUNT[0], tag, note_type, RECIPIENT] - # TODO: implement scale down logic; stubbed out for now - exec.asset_conversion::scale_u256_to_native_amount + padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_1 padw loc_loadw_be.BUILD_P2ID_AMOUNT_MEM_LOC_0 + # => [AMOUNT[0], AMOUNT[1], tag, note_type, RECIPIENT] + exec.asset_conversion::verify_u256_to_native_amount_conversion_stubbed # => [amount, tag, note_type, RECIPIENT] exec.faucets::distribute @@ -220,6 +364,7 @@ end #! rollupExitRoot[8], // Rollup exit root hash (8 felts, bytes32 as 8 u32 felts) #! ], #! LEAF_DATA_KEY => [ +#! leafType[1], // Leaf type (1 felt, uint8) #! 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) @@ -239,11 +384,22 @@ end #! - any of the validations in faucets::distribute fail. #! #! Invocation: call +@locals(10) # 2 for prefix and suffix, 8 for amount pub proc claim # Check AdviceMap values hash to keys & write CLAIM inputs & DATA_KEYs to global memory exec.batch_pipe_double_words # => [pad(16)] + # validate_claim will overwrite memory in-place, so we need to load the account and amount + # before calling validate_claim and store it in memory locals + exec.get_destination_account_id_data + loc_store.CLAIM_PREFIX_MEM_LOC loc_store.CLAIM_SUFFIX_MEM_LOC + # => [pad(16)] + + exec.get_raw_claim_amount + loc_storew_be.CLAIM_AMOUNT_MEM_LOC_0 dropw loc_storew_be.CLAIM_AMOUNT_MEM_LOC_1 dropw + # => [pad(16)] + # VALIDATE CLAIM mem_loadw_be.PROOF_DATA_KEY_MEM_ADDR # => [PROOF_DATA_KEY, pad(12)] @@ -256,6 +412,10 @@ pub proc claim # => [pad(16)] # Create P2ID output note + loc_loadw_be.CLAIM_AMOUNT_MEM_LOC_1 swapw loc_loadw_be.CLAIM_AMOUNT_MEM_LOC_0 + # => [AMOUNT[0], AMOUNT[1], pad(8)] + loc_load.CLAIM_SUFFIX_MEM_LOC loc_load.CLAIM_PREFIX_MEM_LOC + # => [prefix, suffix, AMOUNT[0], AMOUNT[1], pad(8)] exec.build_p2id_output_note # => [pad(16)] end diff --git a/crates/miden-agglayer/asm/bridge/asset_conversion.masm b/crates/miden-agglayer/asm/bridge/asset_conversion.masm index e4f59f17d4..c3e6201f03 100644 --- a/crates/miden-agglayer/asm/bridge/asset_conversion.masm +++ b/crates/miden-agglayer/asm/bridge/asset_conversion.masm @@ -1,14 +1,22 @@ use miden::core::math::u64 use miden::core::word +use miden::agglayer::utils +use ::miden::protocol::asset::FUNGIBLE_ASSET_MAX_AMOUNT # CONSTANTS # ================================================================================================= const MAX_SCALING_FACTOR=18 -# ERRORS +# ERRORS # ================================================================================================= + const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT="maximum scaling factor is 18" +const ERR_X_TOO_LARGE="x must fit into 128 bits (x4..x7 must be 0)" +const ERR_UNDERFLOW="x < y*10^s (underflow detected)" +const ERR_REMAINDER_TOO_LARGE="remainder z must be < 10^s" + +const ERR_Y_TOO_LARGE="y exceeds max fungible token amount" #! Calculate 10^scale where scale is a u8 exponent. #! @@ -105,10 +113,241 @@ pub proc scale_native_amount_to_u256 # => [RESULT_U256[0], RESULT_U256[1]] end -#! TODO: implement scaling down -#! -#! Inputs: [U256[0], U256[1]] -#! Outputs: [amount] -pub proc scale_u256_to_native_amount - repeat.7 drop end +#! Reverse the limbs and change the byte endianness of the result. +pub proc reverse_limbs_and_change_byte_endianness + # reverse the felts within each word + # [a, b, c, d, e, f, g, h] -> [h, g, f, e, d, c, b, a] + exec.word::reverse + swapw + exec.word::reverse + + # change the byte endianness of each felt + repeat.8 + exec.utils::swap_u32_bytes + movdn.7 + end + + # => [RESULT_U256[0], RESULT_U256[1]] end + +#! Subtract two 128-bit integers (little-endian u32 limbs) and assert no underflow. +#! +#! Computes: +#! z = x - y +#! with the constraint: +#! y <= x +#! +#! Inputs: [y0, y1, y2, y3, x0, x1, x2, x3] +#! Outputs: [z0, z1, z2, z3] +#! +#! Panics if: +#! - y > x (ERR_UNDERFLOW) +proc u128_sub_no_underflow + # Put x-word on top for easier access. + swapw + # => [x0, x1, x2, x3, y0, y1, y2, y3] + + # --------------------------------------------------------------------------------------------- + # Low 64 bits: (x1,x0) - (y1,y0) + # Arrange args for u64::overflowing_sub as: + # [y1, y0, x1, x0] + # --------------------------------------------------------------------------------------------- + swap + # => [x1, x0, x2, x3, y0, y1, y2, y3] + + movup.5 + movup.5 + swap + # => [y1, y0, x1, x0, x2, x3, y2, y3] + + exec.u64::overflowing_sub + # => [borrow_low, z1, z0, x2, x3, y2, y3] + + # --------------------------------------------------------------------------------------------- + # High 64 bits (raw): (x3,x2) - (y3,y2) + # Arrange args as: + # [y3, y2, x3, x2] + # --------------------------------------------------------------------------------------------- + movup.3 + movup.4 + # => [x3, x2, borrow_low, z1, z0, y2, y3] + + movup.6 + movup.6 + swap + # => [y3, y2, x3, x2, borrow_low, z1, z0] + + exec.u64::overflowing_sub + # => [underflow_high_raw, t_hi, t_lo, borrow_low, z1, z0] + + # --------------------------------------------------------------------------------------------- + # Apply propagated borrow from low 64-bit subtraction: + # (t_hi,t_lo) - borrow_low + # --------------------------------------------------------------------------------------------- + swap.3 + push.0 + exec.u64::overflowing_sub + # => [underflow_high_borrow, z3, z2, underflow_high_raw, z1, z0] + + # Underflow iff either high-half step underflowed. + movup.3 or + assertz.err=ERR_UNDERFLOW + # => [z3, z2, z1, z0] + + # Return little-endian limbs. + exec.word::reverse + # => [z0, z1, z2, z3] +end + +#! Verify conversion from an AggLayer U256 amount to a Miden native amount (Felt) +#! +#! Specification: +#! Verify that a provided y is the quotient of dividing x by 10^scale_exp: +#! y = floor(x / 10^scale_exp) +#! +#! This procedure does NOT perform division. It proves the quotient is correct by checking: +#! 1) y is within the allowed fungible token amount range +#! 2) y_scaled = y * 10^scale_exp (computed via scale_native_amount_to_u256) +#! 3) z = x - y_scaled (must not underflow, i.e. y_scaled <= x) +#! 4) z fits in 64 bits (upper 192 bits are zero) +#! 5) (z1, z0) < 10^scale_exp (remainder bound) +#! +#! These conditions prove: +#! x = y_scaled + z, with 0 <= z < 10^scale_exp +#! which uniquely implies: +#! y = floor(x / 10^scale_exp) +#! +#! Example (ETH -> Miden base 1e8): +#! - EVM amount: 100 ETH = 100 * 10^18 +#! - Miden amount: 100 ETH = 100 * 10^8 +#! - Therefore the scale-down factor is: +#! scale = 10^(18 - 8) = 10^10 +#! scale_exp = 10 +#! - Inputs/expected values: +#! x = 100 * 10^18 +#! y = floor(x / 10^10) = 100 * 10^8 +#! y_scaled = y * 10^10 = 100 * 10^18 +#! z = x - y_scaled = 0 +#! +#! NOTE: For efficiency, this verifier enforces x < 2^128 by requiring x4..x7 == 0. +#! +#! Inputs: [x7, x6, x5, x4, x3, x2, x1, x0, scale_exp, y] +#! Where x is encoded as 8 u32 limbs in big-endian order. +#! (x7 is most significant limb and is at the top of the stack) +#! Each limb is expected to contain little-endian bytes. +#! Outputs: [y] +#! +#! Where: +#! - x: The original AggLayer amount as an unsigned 256-bit integer (U256). +#! It is provided on the operand stack as 8 big-endian u32 limbs: +#! x = x0 + x1·2^32 + x2·2^64 + x3·2^96 + x4·2^128 + x5·2^160 + x6·2^192 + x7·2^224 +#! - x0..x7: 32-bit limbs of x in big-endian order (x0 is least significant). +#! - scale_exp: The base-10 exponent used for scaling down (an integer in [0, 18]). +#! - y: The provided quotient (Miden native amount) as a Felt interpreted as an unsigned u64. +#! - y_scaled: The 256-bit value y * 10^scale_exp represented as 8 u32 limbs (big-endian). +#! - z: The remainder-like difference z = x - y_scaled (essentially dust that is lost in the +#! conversion due to precision differences). This verifier requires z < 10^scale_exp. +#! +#! Panics if: +#! - scale_exp > 18 (asserted in pow10 via scale_native_amount_to_u256) +#! - y exceeds the max fungible token amount +#! - x does not fit into 128 bits (x4..x7 are not all zero) +#! - x < y * 10^scale_exp (underflow) +#! - z does not fit in 64 bits +#! - (z1, z0) >= 10^scale_exp (remainder too large) +pub proc verify_u256_to_native_amount_conversion + + # reverse limbs and byte endianness + exec.reverse_limbs_and_change_byte_endianness + # => [x0, x1, x2, x3, x4, x5, x6, x7, scale_exp, y] + + # ============================================================================================= + # Step 0: Enforce x < 2^128 + # Constraint: x4 == x5 == x6 == x7 == 0 + # ============================================================================================= + swapw + exec.word::eqz + assert.err=ERR_X_TOO_LARGE + # => [x0, x1, x2, x3, scale_exp, y] + + # ============================================================================================= + # Step 1: Enforce y <= MAX_FUNGIBLE_TOKEN_AMOUNT + # Constraint: y <= MAX_FUNGIBLE_TOKEN_AMOUNT + # ============================================================================================= + dup.5 + push.FUNGIBLE_ASSET_MAX_AMOUNT + lte + # => [is_lte, x0, x1, x2, x3, scale_exp, y] + + assert.err=ERR_Y_TOO_LARGE + # => [x0, x1, x2, x3, scale_exp, y] + + # ============================================================================================= + # Step 2: Compute y_scaled = y * 10^scale_exp + # + # Call: + # scale_native_amount_to_u256(amount=y, target_scale=scale_exp) + # ============================================================================================= + movup.4 + movup.5 + # => [y, scale_exp, x0, x1, x2, x3] + + dup.1 dup.1 + # => [y, scale_exp, y, scale_exp, x0, x1, x2, x3] + + exec.scale_native_amount_to_u256 + # => [y_scaled0..y_scaled7, y, scale_exp, x0, x1, x2, x3] + + # Drop the upper word as it's guaranteed to be zero since y_scaled will fit in 123 bits + # (amount: 63 bits, 10^target_scale: 60 bits). + swapw dropw + # => [y_scaled0, y_scaled1, y_scaled2, y_scaled3, y, scale_exp, x0, x1, x2, x3] + + # ============================================================================================= + # Step 3: Compute z = x - y_scaled and prove no underflow + # z := x - y_scaled + # Constraint: y_scaled <= x + # ============================================================================================= + movup.5 movup.5 + # => [y, scale_exp, y_scaled0, y_scaled1, y_scaled2, y_scaled3, x0, x1, x2, x3] + + movdn.9 movdn.9 + # => [y_scaled0, y_scaled1, y_scaled2, y_scaled3, x0, x1, x2, x3, y, scale_exp] + + exec.u128_sub_no_underflow + # => [z0, z1, z2, z3, y, scale_exp] + + # ============================================================================================= + # Step 4: Enforce z < 10^scale_exp (remainder bound) + # + # We compare z against 10^scale_exp using a u64 comparison on (z1, z0). + # To make that comparison complete, we must first prove z fits into 64 bits, i.e. z2 == z3 == 0. + # + # This is justified because scale_exp <= 18, so 10^scale_exp <= 10^18 < 2^60. + # Therefore any valid remainder z < 10^scale_exp must be < 2^60 and thus must have z2 == z3 == 0. + # ============================================================================================= + exec.word::reverse + # => [z3, z2, z1, z0, y, scale_exp] + + assertz.err=ERR_REMAINDER_TOO_LARGE # z3 == 0 + assertz.err=ERR_REMAINDER_TOO_LARGE # z2 == 0 + # => [z1, z0, y, scale_exp] + + movup.3 + exec.pow10 + # => [scale, z1, z0, y] + + u32split + # => [scale_hi, scale_lo, z1, z0, y] + + exec.u64::lt + # => [is_lt, y] + + assert.err=ERR_REMAINDER_TOO_LARGE + # => [y] +end + +# TODO: Rm & use verify_u256_to_native_amount_conversion +pub proc verify_u256_to_native_amount_conversion_stubbed + repeat.7 drop end +end \ No newline at end of file diff --git a/crates/miden-agglayer/asm/bridge/bridge_config.masm b/crates/miden-agglayer/asm/bridge/bridge_config.masm new file mode 100644 index 0000000000..38cb530767 --- /dev/null +++ b/crates/miden-agglayer/asm/bridge/bridge_config.masm @@ -0,0 +1,45 @@ +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================= + +const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") +const IS_REGISTERED_FLAG=1 + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Registers a faucet in the bridge's faucet registry. +#! +#! Writes `KEY -> [1, 0, 0, 0]` into the `faucet_registry` map, where +#! `KEY = [faucet_id_prefix, faucet_id_suffix, 0, 0]`. +#! +#! The sentinel value `[1, 0, 0, 0]` distinguishes registered faucets from +#! non-existent entries (SMTs return EMPTY_WORD for missing keys). +#! +#! TODO: Currently, no sender validation is performed — anyone can register a faucet. +#! Tracked in https://github.com/0xMiden/miden-base/issues/2450 +#! +#! Inputs: [faucet_id_prefix, faucet_id_suffix, pad(14)] +#! Outputs: [pad(16)] +#! +#! Invocation: call +pub proc register_faucet + # => [faucet_id_prefix, faucet_id_suffix, pad(14)] + + # set_map_item expects [slot_id(2), KEY(4), VALUE(4)] and returns [OLD_VALUE(4)]. + push.IS_REGISTERED_FLAG + # => [IS_REGISTERED_FLAG, slot_id_prefix, slot_id_suffix, pad(14)] + + movdn.7 + # => [[slot_id_prefix, slot_id_suffix, 0, 0], [0, 0, 0, IS_REGISTERED_FLAG], pad(9)] + + # Place slot ID on top + push.FAUCET_REGISTRY_SLOT[0..2] + # Stack: [slot0, slot1, [prefix, suffix, 0, 0], [0, 0, 0, 1], pad(9)] + + exec.native_account::set_map_item + # => [OLD_VALUE(4), pad(9)] + + dropw +end diff --git a/crates/miden-agglayer/asm/bridge/bridge_in.masm b/crates/miden-agglayer/asm/bridge/bridge_in.masm index 734c2a2fd9..50ef55aae6 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_in.masm @@ -1,5 +1,7 @@ use miden::agglayer::crypto_utils +use miden::agglayer::utils use miden::core::crypto::hashes::keccak256 +use miden::core::crypto::hashes::rpo256 use miden::core::mem use miden::protocol::active_account use miden::protocol::native_account @@ -8,6 +10,7 @@ use miden::protocol::native_account # ================================================================================================= const ERR_BRIDGE_NOT_MAINNET = "bridge not mainnet" +const ERR_GER_NOT_FOUND = "GER not found in storage" const ERR_LEADING_BITS_NON_ZERO = "leading bits of global index must be zero" const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet deposit" const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: provided SMT root does not match the computed root" @@ -22,34 +25,39 @@ const GLOBAL_INDEX_PTR = PROOF_DATA_PTR + 2 * 256 # 512 const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 # 520 const MAINNET_EXIT_ROOT_PTR = EXIT_ROOTS_PTR # it's the first exit root -const GER_UPPER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_upper") -const GER_LOWER_STORAGE_SLOT=word("miden::agglayer::bridge::ger_lower") +const GER_STORAGE_SLOT=word("miden::agglayer::bridge::ger") +const GER_KNOWN_FLAG=1 # PUBLIC INTERFACE # ================================================================================================= #! Updates the Global Exit Root (GER) in the bridge account storage. #! +#! Computes hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) and stores it in a map +#! with value [GER_KNOWN_FLAG, 0, 0, 0] to indicate the GER is known. +#! #! Inputs: [GER_LOWER[4], GER_UPPER[4], pad(8)] #! Outputs: [pad(16)] #! #! Invocation: call pub proc update_ger - push.GER_LOWER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_LOWER[4], GER_UPPER[4], pad(8)] + # compute hash(GER) = rpo256::merge(GER_UPPER, GER_LOWER) + # inputs: [B, A] => output: hash(A || B) + exec.rpo256::merge + # => [GER_HASH, pad(12)] - exec.native_account::set_item - # => [OLD_VALUE, GER_UPPER[4], pad(8)] + # prepare VALUE = [0, 0, 0, GER_KNOWN_FLAG] + push.GER_KNOWN_FLAG.0.0.0 + # => [0, 0, 0, GER_KNOWN_FLAG, GER_HASH, pad(12)] - dropw - # => [GER_UPPER[4], pad(12)] + swapw + # => [GER_HASH, VALUE, pad(12)] - push.GER_UPPER_STORAGE_SLOT[0..2] - # => [slot_id_prefix, slot_id_suffix, GER_UPPER[4], pad(12)] + push.GER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_HASH, VALUE, pad(12)] - exec.native_account::set_item + exec.native_account::set_map_item # => [OLD_VALUE, pad(12)] - dropw # => [pad(16)] end @@ -81,7 +89,7 @@ end #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) #! metadata[8], // ABI encoded metadata (8 felts, fixed size) -#! EMPTY_WORD // padding +#! padding[3], // padding (3 felts) - not used in the hash #! ], #! } #! @@ -116,12 +124,29 @@ end #! #! Invocation: exec proc assert_valid_ger - # TODO verify that GER is in storage - dropw dropw + # compute hash(GER) + exec.rpo256::merge + # => [GER_HASH] + + push.GER_STORAGE_SLOT[0..2] + # => [slot_id_prefix, slot_id_suffix, GER_HASH] + + exec.active_account::get_map_item + # => [VALUE] + + # assert the GER is known in storage (VALUE = [0, 0, 0, GER_KNOWN_FLAG]) + push.GER_KNOWN_FLAG.0.0.0 + # => [0, 0, 0, GER_KNOWN_FLAG, VALUE] + + assert_eqw.err=ERR_GER_NOT_FOUND + # => [] end #! Assert the global index is valid for a mainnet deposit. #! +#! Each element of the global index is a LE-packed u32 felt (as produced by +#! `bytes_to_packed_u32_felts` / `GlobalIndex::to_elements()`). +#! #! Inputs: [GLOBAL_INDEX[8]] #! Outputs: [leaf_index] #! @@ -136,15 +161,18 @@ pub proc process_global_index_mainnet # the top 191 bits of the global index are zero repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end - # the next element is a u32 mainnet flag bit - # enforce that this limb is one - # => [mainnet_flag, GLOBAL_INDEX[6..8], LEAF_VALUE[8]] + # the next element is the mainnet flag (LE-packed u32) + # byte-swap to get the BE value, then assert it is exactly 1 + # => [mainnet_flag_le, rollup_index_le, leaf_index_le, ...] + exec.utils::swap_u32_bytes assert.err=ERR_BRIDGE_NOT_MAINNET - # the next element is a u32 rollup index, must be zero for a mainnet deposit + # the next element is the rollup index, must be zero for a mainnet deposit + # (zero is byte-order-independent, so no swap needed) assertz.err=ERR_ROLLUP_INDEX_NON_ZERO - # finally, the leaf index = lowest 32 bits = last limb + # the leaf index is the last element; byte-swap from LE to BE to get the actual index + exec.utils::swap_u32_bytes # => [leaf_index] end diff --git a/crates/miden-agglayer/asm/bridge/bridge_out.masm b/crates/miden-agglayer/asm/bridge/bridge_out.masm index e78e2983e9..74f3319fdc 100644 --- a/crates/miden-agglayer/asm/bridge/bridge_out.masm +++ b/crates/miden-agglayer/asm/bridge/bridge_out.masm @@ -1,20 +1,427 @@ use miden::protocol::active_note +use miden::protocol::active_account +use miden::protocol::native_account use miden::protocol::note +use miden::protocol::tx use miden::standards::note_tag +use miden::standards::data_structures::double_word_array use miden::protocol::output_note use miden::core::crypto::hashes::keccak256 use miden::core::crypto::hashes::rpo256 use miden::core::word -use miden::agglayer::local_exit_tree +use miden::agglayer::utils +use miden::agglayer::agglayer_faucet +use miden::agglayer::crypto_utils +use miden::agglayer::mmr_frontier32_keccak + + +# TYPE ALIASES +# ================================================================================================= + +type EthereumAddressFormat = struct @bigendian { a: felt, b: felt, c: felt, d: felt, e: felt } +type MemoryAddress = u32 # CONSTANTS # ================================================================================================= -const MMR_PTR=42 -const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") + +# bridge_out memory locals +const BRIDGE_OUT_BURN_ASSET_LOC=0 +const DESTINATION_ADDRESS_0_LOC=4 +const DESTINATION_ADDRESS_1_LOC=5 +const DESTINATION_ADDRESS_2_LOC=6 +const DESTINATION_ADDRESS_3_LOC=7 +const DESTINATION_ADDRESS_4_LOC=8 +const DESTINATION_NETWORK_LOC=9 + +# create_burn_note memory locals +const CREATE_BURN_NOTE_BURN_ASSET_LOC=0 +const NETWORK_FAUCET_TAG_LOC=5 + +const LEAF_DATA_START_PTR=44 + +# Memory pointer for loading the LET (Local Exit Tree) frontier into memory. +# The memory layout at this address matches what append_and_update_frontier expects: +# [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +const LET_FRONTIER_MEM_PTR=100 + +const LEAF_TYPE_OFFSET=0 +const ORIGIN_NETWORK_OFFSET=1 +const ORIGIN_TOKEN_ADDRESS_OFFSET=2 +const DESTINATION_NETWORK_OFFSET=7 +const DESTINATION_ADDRESS_OFFSET=8 +const AMOUNT_OFFSET=13 +const METADATA_HASH_OFFSET=21 +const PADDING_OFFSET=29 const PUBLIC_NOTE=1 const BURN_NOTE_NUM_STORAGE_ITEMS=0 -const BURN_ASSET_MEM_PTR=24 + +# Storage slot constants for the LET (Local Exit Tree). +# The frontier is stored as a double-word array in a map slot. +# The root and num_leaves are stored in separate value slots. +const LOCAL_EXIT_TREE_SLOT=word("miden::agglayer::let") +const LET_ROOT_LO_SLOT=word("miden::agglayer::let::root_lo") +const LET_ROOT_HI_SLOT=word("miden::agglayer::let::root_hi") +const LET_NUM_LEAVES_SLOT=word("miden::agglayer::let::num_leaves") +const FAUCET_REGISTRY_SLOT=word("miden::agglayer::bridge::faucet_registry") + +const LEAF_TYPE_ASSET=0 + +# ERRORS +# ================================================================================================= + +const ERR_FAUCET_NOT_REGISTERED="faucet is not registered in the bridge's faucet registry" + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Bridges an asset out via the AggLayer. +#! +#! This procedure handles the complete bridge-out operation: +#! 1. Validates the asset's faucet is registered in the bridge's faucet registry +#! 2. Queries the faucet for origin asset conversion data via FPI +#! 3. Builds the leaf data (origin token, destination, amount, metadata) +#! 4. Computes Keccak hash and adds it to the MMR frontier +#! 5. Creates a BURN note with the bridged out asset +#! +#! Inputs: [ASSET, dest_network_id, dest_address(5), pad(4)] +#! Outputs: [] +#! +#! Where: +#! - ASSET is the asset to be bridged out (layout: [faucet_id_prefix, faucet_id_suffix, 0, amount]). +#! - dest_network_id is the u32 destination network/chain ID. +#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. +#! +#! Invocation: call +@locals(10) +pub proc bridge_out + # Save ASSET to memory for later BURN note creation + loc_storew_be.BRIDGE_OUT_BURN_ASSET_LOC dropw + loc_store.DESTINATION_NETWORK_LOC + loc_store.DESTINATION_ADDRESS_0_LOC + loc_store.DESTINATION_ADDRESS_1_LOC + loc_store.DESTINATION_ADDRESS_2_LOC + loc_store.DESTINATION_ADDRESS_3_LOC + loc_store.DESTINATION_ADDRESS_4_LOC + # => [] + + # --- 1. Validate faucet registration and convert asset via FPI --- + loc_loadw_be.BRIDGE_OUT_BURN_ASSET_LOC + exec.convert_asset + # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network, dest_network_id, dest_address(5)] + + # --- 2. Write all leaf data fields to memory --- + + # Store scaled AMOUNT (8 felts) + push.LEAF_DATA_START_PTR push.AMOUNT_OFFSET add + movdn.8 + exec.utils::mem_store_double_word_unaligned + # => [origin_addr(5), origin_network, dest_network_id, dest_address(5)] + + # Store origin_token_address (5 felts) + push.LEAF_DATA_START_PTR push.ORIGIN_TOKEN_ADDRESS_OFFSET add + exec.write_address_to_memory + # => [origin_network, dest_network_id, dest_address(5)] + + # Store origin_network + push.LEAF_DATA_START_PTR push.ORIGIN_NETWORK_OFFSET add + mem_store + # => [dest_network_id, dest_address(5)] + + # Store destination_network + loc_load.DESTINATION_NETWORK_LOC + push.LEAF_DATA_START_PTR push.DESTINATION_NETWORK_OFFSET add + mem_store + # => [dest_address(5)] + + # Store destination_address + loc_load.DESTINATION_ADDRESS_4_LOC + loc_load.DESTINATION_ADDRESS_3_LOC + loc_load.DESTINATION_ADDRESS_2_LOC + loc_load.DESTINATION_ADDRESS_1_LOC + loc_load.DESTINATION_ADDRESS_0_LOC + push.LEAF_DATA_START_PTR push.DESTINATION_ADDRESS_OFFSET add + exec.write_address_to_memory + # => [] + + # TODO construct metadata hash + padw padw + # => [METADATA_HASH[8]] + push.LEAF_DATA_START_PTR push.METADATA_HASH_OFFSET add + movdn.8 + # => [METADATA_HASH[8], metadata_hash_ptr] + exec.utils::mem_store_double_word_unaligned + + # Explicitly zero the 3 padding felts after METADATA_HASH for + # crypto_utils::pack_leaf_data + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add + mem_store + + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.1 add + mem_store + + push.0 + push.LEAF_DATA_START_PTR push.PADDING_OFFSET add.2 add + mem_store + + # Leaf type + push.LEAF_TYPE_ASSET + exec.utils::swap_u32_bytes + push.LEAF_DATA_START_PTR push.LEAF_TYPE_OFFSET add + # => [leaf_type_ptr, leaf_type] + mem_store + + # --- 3. Compute leaf value and add to MMR frontier --- + push.LEAF_DATA_START_PTR + exec.add_leaf_bridge + + # --- 4. Create BURN output note for ASSET --- + loc_loadw_be.BRIDGE_OUT_BURN_ASSET_LOC + exec.create_burn_note +end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Validates that a faucet is registered in the bridge's faucet registry, then performs +#! an FPI call to the faucet's `asset_to_origin_asset` procedure to obtain the scaled +#! amount, origin token address, and origin network. +#! +#! Inputs: [ASSET] +#! Outputs: [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network] +#! +#! Where: +#! - ASSET layout: [faucet_id_prefix, faucet_id_suffix, 0, amount] +#! - AMOUNT_U256: scaled amount as 8 u32 limbs (little-endian) +#! - origin_addr: origin token address (5 u32 felts) +#! - origin_network: origin network identifier +#! +#! Panics if: +#! - The faucet is not registered in the faucet registry. +#! - The FPI call to asset_to_origin_asset fails. +#! +#! Invocation: exec +proc convert_asset + # => [faucet_id_prefix, faucet_id_suffix, 0, amount] + + # --- Step 1: Assert faucet is registered --- + + # Build KEY = [faucet_id_prefix, faucet_id_suffix, 0, 0] for the map lookup. + # Duplicate faucet ID onto top of stack, then add zero padding. + dup.1 dup.1 + push.0.0 + movup.3 movup.3 + # => [faucet_id_prefix, faucet_id_suffix, 0, 0, faucet_id_prefix, faucet_id_suffix, 0, amount] + + push.FAUCET_REGISTRY_SLOT[0..2] + exec.active_account::get_map_item + # => [VALUE(4), faucet_id_prefix, faucet_id_suffix, 0, amount] + + # Check flag, the stored word must be [0, 0, 0, 1] for registered faucets + drop drop drop + assert.err=ERR_FAUCET_NOT_REGISTERED + # => [faucet_id_prefix, faucet_id_suffix, 0, amount] + + # --- Step 2: FPI to faucet's asset_to_origin_asset --- + + # Drop the zero padding between faucet_id and amount. + movup.2 drop + # => [faucet_id_prefix, faucet_id_suffix, amount] + + procref.agglayer_faucet::asset_to_origin_asset + # => [PROC_MAST_ROOT(4), faucet_id_prefix, faucet_id_suffix, amount] + + # Move faucet_id above PROC_MAST_ROOT + movup.5 movup.5 + # => [faucet_id_prefix, faucet_id_suffix, PROC_MAST_ROOT(4), amount] + + exec.tx::execute_foreign_procedure + # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network, pad(2)] + + # drop the 2 trailing padding elements + movup.15 drop movup.14 drop + # => [AMOUNT_U256[0](4), AMOUNT_U256[1](4), origin_addr(5), origin_network] +end + +#! Computes the leaf value from the leaf data in memory and adds it to the MMR frontier. +#! +#! Inputs: [leaf_data_start_ptr] +#! Outputs: [] +#! +#! Memory layout (starting at leaf_data_start_ptr): +#! [ +#! leafType[1], +#! originNetwork[1], +#! originTokenAddress[5], +#! destinationNetwork[1], +#! destinationAddress[5], +#! amount[8], +#! metadataHash[8], +#! padding[3], +#! ] +#! +#! Invocation: exec +proc add_leaf_bridge(leaf_data_start_ptr: MemoryAddress) + exec.crypto_utils::compute_leaf_value + # => [LEAF_VALUE_LO, LEAF_VALUE_HI] + + # Load the LET frontier from storage into memory at LET_FRONTIER_MEM_PTR + exec.load_let_frontier_to_memory + # => [LEAF_VALUE_LO, LEAF_VALUE_HI] + + # Push frontier pointer below the leaf value + push.LET_FRONTIER_MEM_PTR movdn.8 + # => [LEAF_VALUE_LO, LEAF_VALUE_HI, let_frontier_ptr] + + # Append the leaf to the frontier and compute the new root + exec.mmr_frontier32_keccak::append_and_update_frontier + # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] + + # Save the root and num_leaves to their value slots + exec.save_let_root_and_num_leaves + # => [] + + # Write the updated frontier from memory back to the map + exec.save_let_frontier_to_storage + # => [] +end + +#! Loads the LET (Local Exit Tree) frontier from account storage into memory. +#! +#! The num_leaves is read from its dedicated value slot, and the 32 frontier entries are read +#! from the LET map slot (double-word array, indices 0..31). The data is placed into memory at +#! LET_FRONTIER_MEM_PTR, matching the layout expected by append_and_update_frontier: +#! [num_leaves, 0, 0, 0, [[FRONTIER_NODE_LO, FRONTIER_NODE_HI]; 32]] +#! +#! Empty (uninitialized) map entries return zeros, which is the correct initial state for the +#! frontier when there are no leaves. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +proc load_let_frontier_to_memory + # 1. Load num_leaves from its value slot + push.LET_NUM_LEAVES_SLOT[0..2] + exec.active_account::get_item + # => [num_leaves_word] + + push.LET_FRONTIER_MEM_PTR mem_storew_be dropw + # => [] + + # 2. Load 32 frontier double-word entries from the map via double_word_array::get + push.0 + # => [h=0] + + repeat.32 + # => [h] + + # Read frontier[h] as a double word from the map + dup push.LOCAL_EXIT_TREE_SLOT[0..2] + exec.double_word_array::get + # => [VALUE_0, VALUE_1, h] + + # Compute memory address and store the double word + dup.8 mul.8 add.LET_FRONTIER_MEM_PTR add.4 movdn.8 + # => [VALUE_0, VALUE_1, mem_addr, h] + exec.utils::mem_store_double_word + dropw dropw drop + # => [h] + + add.1 + # => [h+1] + end + + drop + # => [] +end + +#! Saves the Local Exit Root and num_leaves to their dedicated value slots. +#! +#! Inputs: [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] +#! Outputs: [] +#! +#! Invocation: exec +proc save_let_root_and_num_leaves + # 1. Save root lo word to its value slot + push.LET_ROOT_LO_SLOT[0..2] + exec.native_account::set_item + dropw + # => [NEW_ROOT_HI, new_leaf_count] + + # 2. Save root hi word to its value slot + push.LET_ROOT_HI_SLOT[0..2] + exec.native_account::set_item + dropw + # => [new_leaf_count] + + # 3. Save new_leaf_count to its value slot as [new_leaf_count, 0, 0, 0] + push.0.0.0 + # => [0, 0, 0, new_leaf_count] + push.LET_NUM_LEAVES_SLOT[0..2] + exec.native_account::set_item + dropw + # => [] +end + +#! Writes the 32 frontier entries from memory back to the LET map slot. +#! +#! Each frontier entry is a double word (Keccak256 digest) stored at +#! LET_FRONTIER_MEM_PTR + 4 + h * 8, and is written to the map at double_word_array index h. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Invocation: exec +proc save_let_frontier_to_storage + push.0 + # => [h=0] + + repeat.32 + # => [h] + + # Load frontier[h] double word from memory + dup mul.8 add.LET_FRONTIER_MEM_PTR add.4 + exec.utils::mem_load_double_word + # => [VALUE_0, VALUE_1, h] + + # Write it back to the map at index h + dup.8 push.LOCAL_EXIT_TREE_SLOT[0..2] + exec.double_word_array::set + dropw dropw + # => [h] + + add.1 + # => [h+1] + end + + drop + # => [] +end + +#! Writes an Ethereum address (5 u32 felts) to consecutive memory locations. +#! +#! Inputs: [mem_ptr, address(5)] +#! Outputs: [] +#! +#! Invocation: exec +proc write_address_to_memory(mem_ptr: MemoryAddress, address: EthereumAddressFormat) + dup movdn.6 mem_store movup.4 add.1 + # => [mem_ptr+1, address(4)] + + dup movdn.5 mem_store movup.3 add.1 + # => [mem_ptr+2, address(3)] + + dup movdn.4 mem_store movup.2 add.1 + # => [mem_ptr+3, address(2)] + + dup movdn.3 mem_store swap add.1 + # => [mem_ptr+4, address(1)] + + mem_store +end #! Computes the SERIAL_NUM of the outputted BURN note. #! @@ -50,7 +457,7 @@ end #! Invocation: exec @locals(8) proc create_burn_note - loc_storew_be.0 dupw + loc_storew_be.CREATE_BURN_NOTE_BURN_ASSET_LOC dupw # => [ASSET, ASSET] movup.2 drop movup.2 drop @@ -59,7 +466,7 @@ proc create_burn_note exec.note_tag::create_account_target # => [network_faucet_tag, ASSET] - loc_store.5 + loc_store.NETWORK_FAUCET_TAG_LOC # => [ASSET] exec.compute_burn_note_serial_num @@ -68,92 +475,21 @@ proc create_burn_note procref.::miden::standards::notes::burn::main swapw # => [SERIAL_NUM, SCRIPT_ROOT] + # BURN note has no storage items, so we can set the pointer to 0 push.BURN_NOTE_NUM_STORAGE_ITEMS push.0 # => [storage_ptr, num_storage_items, SERIAL_NUM, SCRIPT_ROOT] - exec.note::build_recipient # => [RECIPIENT] push.PUBLIC_NOTE - loc_load.5 + loc_load.NETWORK_FAUCET_TAG_LOC # => [tag, note_type, RECIPIENT] call.output_note::create # => [note_idx] - movdn.4 loc_loadw_be.0 + movdn.4 loc_loadw_be.CREATE_BURN_NOTE_BURN_ASSET_LOC # => [ASSET, note_idx] exec.output_note::add_asset - # => [] -end - -#! Bridges an asset out via the AggLayer -#! -#! This procedure handles the complete bridge-out operation, including: -#! - Converting asset data to u32 format -#! - Computing Keccak hash of the data -#! - Adding the hash to the MMR frontier -#! - Storing the updated MMR root in account storage -#! - Creating a BURN note with the bridged out asset -#! -#! Inputs: [ASSET, dest_network, dest_address(5)] -#! Outputs: [] -#! -#! Where: -#! - ASSET is the asset to be bridged out. -#! - dest_network is the u32 destination network/chain ID. -#! - dest_address(5) are 5 u32 values representing a 20-byte Ethereum address. -#! -#! Invocation: call -pub proc bridge_out - mem_storew_be.BURN_ASSET_MEM_PTR - # => [ASSET, dest_network, dest_address(5)] - - # @dev TODO: Look up asset faucet id in asset registry - # -> return scaling factor - - # @dev TODO: Convert ASSET amount to EVM amount using scaling factor - # -> return amount from here: https://github.com/0xMiden/miden-base/pull/2141 - - # Converting SCALED_ASSET, dest_network, dest_address(5) to u32 representation - # in preparation for keccak256 hashing - - # keccak256 inputs: - # => [ASSET, dest_network, dest_address(5)] - # TODO we should convert Miden->Ethereum asset values, incl. amount conversion etc. - - # TODO: make building bridge message a separate procedure - # TODO: match Agglayer addLeafBridge logic - # TODO: convert Miden asset amount to Ethereum amount - # Store ASSET as u32 limbs in memory starting at address 0 - push.0 movdn.4 exec.word::store_word_u32s_le - # => [dest_network, dest_address(5)] - - # Store [dest_network, dest_address[0..3]] as u32 limbs in memory starting at address 8 - push.8 movdn.4 exec.word::store_word_u32s_le - # => [dest_address(2), 0, 0] - - # Store [dest_address[3..5], 0, 0] as u32 limbs in memory starting at address 16 - push.16 movdn.4 exec.word::store_word_u32s_le - # => [] - - # 1 u32 = 4 bytes - # 10 u32 values = 40 bytes - push.40 push.0 - # => [ptr, len_bytes] - - exec.keccak256::hash_bytes - # => [DIGEST_U32[8]] - - # adding DIGEST_U32 double word leaf to mmr frontier - exec.local_exit_tree::add_asset_message - # => [] - - # creating BURN output note for ASSET - mem_loadw_be.BURN_ASSET_MEM_PTR - # => [ASSET] - - exec.create_burn_note - # => [] end diff --git a/crates/miden-agglayer/asm/bridge/crypto_utils.masm b/crates/miden-agglayer/asm/bridge/crypto_utils.masm index 4a9534882d..a0d92bc582 100644 --- a/crates/miden-agglayer/asm/bridge/crypto_utils.masm +++ b/crates/miden-agglayer/asm/bridge/crypto_utils.masm @@ -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,18 +39,21 @@ 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] #! Advice map: { #! LEAF_DATA_KEY => [ +#! leafType[1], // Leaf type (1 felt, uint8) #! 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) +#! metadata_hash[8], // Metadata hash (8 felts, bytes32 as 8 u32 felts) +#! padding[3], // padding (3 felts) - not used in the hash #! ], #! } #! Outputs: [LEAF_VALUE[8]] @@ -53,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 @@ -128,70 +161,177 @@ end #! represented as 32 Keccak256Digest values (64 words). #! - leaf_idx is the index of the provided leaf in the SMT. #! - [ROOT_LO, ROOT_HI] is the calculated root. -@locals(9) # current hash + is_odd flag +@locals(8) # current hash proc calculate_root( leaf_value: DoubleWord, merkle_path_ptr: MemoryAddress, leaf_idx: u32 ) -> DoubleWord # Local memory stores the current hash. It is initialized to the leaf value - loc_storew_be.CUR_HASH_LO_LOCAL dropw loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [merkle_path_ptr, leaf_idx] - # prepare the stack for the hash computation cycle - padw padw padw - # => [PAD, PAD, PAD, merkle_path_ptr, leaf_idx] - # Merkle path is guaranteed to contain 32 nodes repeat.32 - # load the Merkle path node onto the stack - mem_stream - # => [PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # load the Merkle path node word-by-word in LE-felt order + padw dup.4 mem_loadw_le + # => [PATH_NODE_LO, merkle_path_ptr, leaf_idx] + padw dup.8 add.4 mem_loadw_le + swapw + # => [PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr, leaf_idx] - # determine whether the last `leaf_idx` bit is 1 (is `leaf_idx` odd) - dup.13 u32and.1 - # => [is_odd, PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # advance merkle_path_ptr by 8 (two words = 8 element addresses) + movup.8 add.8 movdn.8 + # => [PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr+8, leaf_idx] - # store the is_odd flag to the local memory, so we could use it while all 16 top elements - # are occupied by the nodes - loc_store.8 - # => [PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # determine whether the last `leaf_idx` bit is 1 (is `leaf_idx` odd) + dup.9 u32and.1 + # => [is_odd, PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr+8, leaf_idx] # load the hash respective to the current height from the local memory - padw loc_loadw_be.CUR_HASH_HI_LOCAL padw loc_loadw_be.CUR_HASH_LO_LOCAL - # => [CURR_HASH_LO, CURR_HASH_HI, PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL + # => [CURR_HASH_LO, CURR_HASH_HI, is_odd, PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr, leaf_idx] - # load the is_odd flag back to the stack - loc_load.8 - # => [is_odd, CURR_HASH_LO, CURR_HASH_HI, PATH_NODE_LO, PATH_NODE_HI, PAD, merkle_path_ptr, leaf_idx] + # move the `is_odd` flag to the top of the stack + movup.8 + # => [is_odd, CURR_HASH_LO, CURR_HASH_HI, PATH_NODE_LO, PATH_NODE_HI, merkle_path_ptr, leaf_idx] # if is_odd flag equals 1 (`leaf_idx` is odd), change the order of the nodes on the stack - if.true - # rearrange the hashes: current position of the hash is odd, so it should be on the + if.true + # rearrange the hashes: current position of the hash is odd, so it should be on the # right swapdw - # => [PATH_NODE_LO, PATH_NODE_HI, CURR_HASH_LO, CURR_HASH_HI, PAD, merkle_path_ptr, leaf_idx] + # => [PATH_NODE_LO, PATH_NODE_HI, CURR_HASH_LO, CURR_HASH_HI, merkle_path_ptr, leaf_idx] end # compute the next height hash exec.keccak256::merge - # => [CURR_HASH_LO', CURR_HASH_HI', PAD, merkle_path_ptr, leaf_idx] + # => [CURR_HASH_LO', CURR_HASH_HI', merkle_path_ptr, leaf_idx] - # store the resulting hash to the local memory - loc_storew_be.CUR_HASH_LO_LOCAL swapw loc_storew_be.CUR_HASH_HI_LOCAL - # => [CURR_HASH_HI', CURR_HASH_LO', PAD, merkle_path_ptr, leaf_idx] + # store the resulting hash to the local memory and drop the hash words + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw + # => [merkle_path_ptr, leaf_idx] # update the `leaf_idx` (shift it right by 1 bit) - movup.13 u32shr.1 movdn.13 - # => [CURR_HASH_HI', CURR_HASH_LO', PAD, merkle_path_ptr, leaf_idx>>1] + swap u32shr.1 swap + # => [merkle_path_ptr, leaf_idx>>1] end # after all 32 hashes have been computed, the current hash stored in local memory represents # the root of the SMT, which should be returned - # - # remove 6 elements from the stack so that exactly 8 are remaining and rewrite them with the - # root value from the local memory - dropw drop drop - loc_loadw_be.CUR_HASH_HI_LOCAL swapw loc_loadw_be.CUR_HASH_LO_LOCAL + drop drop + padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.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 +@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 \ No newline at end of file diff --git a/crates/miden-agglayer/asm/bridge/eth_address.masm b/crates/miden-agglayer/asm/bridge/eth_address.masm index 57a8e9f298..bcf418ce21 100644 --- a/crates/miden-agglayer/asm/bridge/eth_address.masm +++ b/crates/miden-agglayer/asm/bridge/eth_address.masm @@ -1,3 +1,4 @@ +use miden::agglayer::utils use miden::core::crypto::hashes::keccak256 use miden::core::word @@ -8,10 +9,9 @@ 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 # ================================================================================================= @@ -25,22 +25,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.utils::swap_u32_bytes + swap + exec.utils::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 +58,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* +#! (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] diff --git a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm index b789847639..f8be97dfa6 100644 --- a/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm +++ b/crates/miden-agglayer/asm/bridge/mmr_frontier32_keccak.masm @@ -136,8 +136,8 @@ const CANONICAL_ZEROES_LOCAL = 8 @locals(264) # new_leaf/curr_hash + canonical_zeros pub proc append_and_update_frontier # set CUR_HASH = NEW_LEAF and store to local memory - loc_storew_be.CUR_HASH_LO_LOCAL dropw - loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [mmr_frontier_ptr] # get the current leaves number @@ -203,8 +203,8 @@ pub proc append_and_update_frontier # load the current hash from the local memory back to the stack # # in the first iteration the current hash will be equal to the new node - padw loc_loadw_be.CUR_HASH_HI_LOCAL - padw loc_loadw_be.CUR_HASH_LO_LOCAL + padw loc_loadw_le.CUR_HASH_HI_LOCAL + padw loc_loadw_le.CUR_HASH_LO_LOCAL swapdw # => [ # FRONTIER[curr_tree_height]_LO, FRONTIER[curr_tree_height]_HI, CUR_HASH_LO, @@ -217,16 +217,16 @@ pub proc append_and_update_frontier # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mmr_frontier_ptr] # store the current hash of the next height back to the local memory - loc_storew_be.CUR_HASH_LO_LOCAL dropw - loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [curr_tree_height, num_leaves, mmr_frontier_ptr] else # => [frontier[curr_tree_height]_ptr, curr_tree_height, num_leaves, mmr_frontier_ptr] # # this height wasn't "occupied" yet: store the current hash as the subtree root # (frontier node) at height `curr_tree_height` - padw loc_loadw_be.CUR_HASH_HI_LOCAL - padw loc_loadw_be.CUR_HASH_LO_LOCAL + padw loc_loadw_le.CUR_HASH_HI_LOCAL + padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ # CUR_HASH_LO, CUR_HASH_HI, frontier[curr_tree_height]_ptr, curr_tree_height, # num_leaves, mmr_frontier_ptr @@ -256,8 +256,8 @@ pub proc append_and_update_frontier # => [CUR_HASH_LO', CUR_HASH_HI', curr_tree_height, num_leaves, mmr_frontier_ptr] # store the current hash of the next height back to the local memory - loc_storew_be.CUR_HASH_LO_LOCAL dropw - loc_storew_be.CUR_HASH_HI_LOCAL dropw + loc_storew_le.CUR_HASH_LO_LOCAL dropw + loc_storew_le.CUR_HASH_HI_LOCAL dropw # => [curr_tree_height, num_leaves, mmr_frontier_ptr] end # => [curr_tree_height, num_leaves, mmr_frontier_ptr] @@ -292,8 +292,8 @@ pub proc append_and_update_frontier # compute. # load the final hash (which is also the root of the tree) - padw loc_loadw_be.CUR_HASH_HI_LOCAL - padw loc_loadw_be.CUR_HASH_LO_LOCAL + padw loc_loadw_le.CUR_HASH_HI_LOCAL + padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] end diff --git a/crates/miden-agglayer/asm/bridge/utils.masm b/crates/miden-agglayer/asm/bridge/utils.masm index 598a392509..6a17598b2c 100644 --- a/crates/miden-agglayer/asm/bridge/utils.masm +++ b/crates/miden-agglayer/asm/bridge/utils.masm @@ -1,4 +1,4 @@ -# Utility module containing helper procedures for the double word handling. +# Utility module containing helper procedures for double word handling and byte manipulation. # TYPE ALIASES # ================================================================================================= @@ -7,7 +7,35 @@ type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt } type DoubleWord = struct { word_lo: BeWord, word_hi: BeWord } type MemoryAddress = u32 -# PUBLIC INTERFACE +# BYTE MANIPULATION +# ================================================================================================= + +#! Swaps byte order in a u32 limb (LE <-> BE). +#! +#! Inputs: [value] +#! Outputs: [swapped] +pub proc swap_u32_bytes + # part0 = (value & 0xFF) << 24 + dup u32and.0xFF u32shl.24 + # => [part0, value] + + # part1 = ((value >> 8) & 0xFF) << 16 + dup.1 u32shr.8 u32and.0xFF u32shl.16 u32or + # => [part01, value] + + # part2 = ((value >> 16) & 0xFF) << 8 + dup.1 u32shr.16 u32and.0xFF u32shl.8 u32or + # => [part012, value] + + # part3 = (value >> 24) + dup.1 u32shr.24 u32or + # => [swapped, value] + + swap drop + # => [swapped] +end + +# DOUBLE WORD MEMORY OPERATIONS # ================================================================================================= #! Stores two words to the provided global memory address. @@ -18,21 +46,49 @@ pub proc mem_store_double_word( double_word_to_store: DoubleWord, mem_ptr: MemoryAddress ) -> (DoubleWord, MemoryAddress) - dup.8 mem_storew_be swapw + dup.8 mem_storew_le swapw # => [WORD_2, WORD_1, ptr] - dup.8 add.4 mem_storew_be swapw + dup.8 add.4 mem_storew_le swapw # => [WORD_1, WORD_2, ptr] end +#! Stores two words to the provided unaligned (not a multiple of 4) memory address. +#! +#! Inputs: [WORD_1, WORD_2, ptr] +#! Outputs: [] +pub proc mem_store_double_word_unaligned( + double_word_to_store: DoubleWord, + mem_ptr: MemoryAddress +) + # bring ptr to the top of the stack + dup.8 + # => [ptr, WORD_1, WORD_2, ptr] + + # store each element individually at consecutive addresses + mem_store dup.7 add.1 + mem_store dup.6 add.2 + mem_store dup.5 add.3 + mem_store + # => [WORD_2, ptr] + + dup.4 add.4 + mem_store dup.3 add.5 + mem_store dup.2 add.6 + mem_store dup.1 add.7 + mem_store + drop + # => [] +end + #! Loads two words from the provided global memory address. #! #! Inputs: [ptr] #! Outputs: [WORD_1, WORD_2] pub proc mem_load_double_word(mem_ptr: MemoryAddress) -> DoubleWord - padw dup.4 add.4 mem_loadw_be + padw dup.4 add.4 mem_loadw_le # => [WORD_2, ptr] - padw movup.8 mem_loadw_be + padw movup.8 mem_loadw_le # => [WORD_1, WORD_2] end diff --git a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm index 9523160e9c..31afc1d83c 100644 --- a/crates/miden-agglayer/asm/note_scripts/B2AGG.masm +++ b/crates/miden-agglayer/asm/note_scripts/B2AGG.masm @@ -11,6 +11,10 @@ use miden::standards::wallets::basic->basic_wallet const B2AGG_NOTE_NUM_STORAGE_ITEMS=6 +const STORAGE_START_PTR=8 +const STORAGE_END_PTR=STORAGE_START_PTR + 8 +const ASSET_PTR=0 + # ERRORS # ================================================================================================= const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS="B2AGG script requires exactly 1 note asset" @@ -30,14 +34,14 @@ const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH="B2AGG note attachment target account do #! Inputs: [] #! Outputs: [] #! -#! Note storage is assumed to be as follows: -#! - destination_network: u32 value representing the target chain ID -#! - destination_address: split into 5 u32 values representing a 20-byte Ethereum address: -#! - destination_address_0: bytes 0-3 -#! - destination_address_1: bytes 4-7 -#! - destination_address_2: bytes 8-11 -#! - destination_address_3: bytes 12-15 -#! - destination_address_4: bytes 16-19 +#! Note storage layout (6 felts total): +#! - destination_network [0] : 1 felt +#! - destination_address [1..5] : 5 felts +#! +#! Where: +#! - destination_network: Destination network identifier (uint32) +#! - destination_address: 20-byte Ethereum address as 5 u32 felts +#! #! Note attachment is constructed from a NetworkAccountTarget standard: #! - [0, exec_hint_tag, target_id_prefix, target_id_suffix] #! @@ -71,14 +75,14 @@ begin # => [pad(16)] # Store note storage -> mem[8..14] - push.8 exec.active_note::get_storage + push.STORAGE_START_PTR exec.active_note::get_storage # => [num_storage_items, dest_ptr, pad(16)] push.B2AGG_NOTE_NUM_STORAGE_ITEMS assert_eq.err=ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS drop # => [pad(16)] # Store note assets -> mem[0..4] - push.0 exec.active_note::get_assets + push.ASSET_PTR exec.active_note::get_assets # => [num_assets, ptr, pad(16)] # Must be exactly 1 asset @@ -86,11 +90,12 @@ begin # => [pad(16)] # load the 6 B2AGG felts from note storage as two words - mem_loadw_be.12 swapw.2 mem_loadw_be.8 swapw - # => [EMPTY_WORD, dest_network, dest_address(5), pad(6)] + push.STORAGE_START_PTR add.4 mem_loadw_le swapw mem_loadw_le.STORAGE_START_PTR + # => [dest_network, dest_address(5), pad(10)] # Load ASSET onto the stack - mem_loadw_be.0 + movupw.2 + mem_loadw_be.ASSET_PTR # => [ASSET, dest_network, dest_address(5), pad(6)] call.bridge_out::bridge_out diff --git a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm index e213a9f1ff..c682993550 100644 --- a/crates/miden-agglayer/asm/note_scripts/CLAIM.masm +++ b/crates/miden-agglayer/asm/note_scripts/CLAIM.masm @@ -69,13 +69,13 @@ end #! #! LEAF_DATA_KEY => [ #! leafType[1], // Leaf type (1 felt, uint32) -#! padding[3], // padding (3 felts) #! originNetwork[1], // Origin network identifier (1 felt, uint32) #! originTokenAddress[5], // Origin token address (5 felts, address as 5 u32 felts) #! destinationNetwork[1], // Destination network identifier (1 felt, uint32) #! destinationAddress[5], // Destination address (5 felts, address as 5 u32 felts) #! amount[8], // Amount of tokens (8 felts, uint256 as 8 u32 felts) #! metadata[8], // ABI encoded metadata (8 felts, fixed size) +#! padding[3], // padding (3 felts) #! ] #! #! TODO: Will be removed in future PR @@ -154,13 +154,13 @@ end #! - mainnetExitRoot [520..527]: 8 felts #! - rollupExitRoot [528..535]: 8 felts #! - leafType [536] : 1 felt -#! - padding [537..539]: 3 felts -#! - originNetwork [540] : 1 felt -#! - originTokenAddress [541..545]: 5 felts -#! - destinationNetwork [546] : 1 felt -#! - destinationAddress [547..551]: 5 felts -#! - amount [552..559]: 8 felts -#! - metadata [560..567]: 8 felts +#! - originNetwork [537] : 1 felt +#! - originTokenAddress [538..542]: 5 felts +#! - destinationNetwork [543] : 1 felt +#! - destinationAddress [544..548]: 5 felts +#! - amount [549..556]: 8 felts +#! - metadata [557..564]: 8 felts +#! - padding [565..567]: 3 felts #! - output_p2id_serial_num [568..571]: 4 felts #! - target_faucet_account_id [572..573]: 2 felts #! - output_note_tag [574] : 1 felt @@ -180,7 +180,7 @@ end #! - originNetwork: Origin network identifier (uint32) #! - originTokenAddress: Origin token address (address as 5 u32 felts) #! - destinationNetwork: Destination network identifier (uint32) -#! - destinationAddress: Destination address (address as 5 u32 felts) +#! - destinationAddress: 20-byte Ethereum address decodable into a Miden AccountId (5 u32 felts) #! - amount: Amount of tokens (uint256 as 8 u32 felts) #! - metadata: ABI encoded metadata (fixed size) #! - padding (3 felts) diff --git a/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm new file mode 100644 index 0000000000..99dcee284f --- /dev/null +++ b/crates/miden-agglayer/asm/note_scripts/CONFIG_AGG_BRIDGE.masm @@ -0,0 +1,69 @@ +use miden::agglayer::bridge_config +use miden::protocol::active_note +use miden::protocol::active_account +use miden::protocol::account_id +use miden::standards::attachments::network_account_target + +# CONSTANTS +# ================================================================================================= + +const STORAGE_START_PTR = 0 +const CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS = 2 + +# ERRORS +# ================================================================================================= + +const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS = "CONFIG_AGG_BRIDGE expects exactly 2 note storage items" +const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH = "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" + +#! Registers a faucet in the bridge's faucet registry. +#! +#! This note can only be consumed by the Agglayer Bridge account that is targeted by the note +#! attachment. Upon consumption, it registers the faucet ID from note storage in the bridge's +#! faucet registry. + +#! Note: Currently, there are no sender validation checks, so anyone can register a faucet. +#! +#! Requires that the account exposes: +#! - agglayer::bridge_config::register_faucet procedure. +#! +#! Inputs: [ARGS, pad(12)] +#! Outputs: [pad(16)] +#! +#! NoteStorage layout (2 felts total): +#! - faucet_id_prefix [0]: 1 felt +#! - faucet_id_suffix [1]: 1 felt +#! +#! Where: +#! - faucet_id_prefix: Prefix felt of the faucet account ID to register. +#! - faucet_id_suffix: Suffix felt of the faucet account ID to register. +#! +#! Panics if: +#! - The note attachment target account does not match the consuming bridge account. +#! - The note does not contain exactly 2 storage items. +#! - The account does not expose the register_faucet procedure. +#! +begin + dropw + # => [pad(16)] + + # Ensure note attachment targets the consuming bridge account. + exec.network_account_target::active_account_matches_target_account + assert.err=ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH + # => [pad(16)] + + # Load note storage to memory + push.STORAGE_START_PTR exec.active_note::get_storage + # => [num_storage_items, dest_ptr, pad(16)] + + push.CONFIG_AGG_BRIDGE_NUM_STORAGE_ITEMS assert_eq.err=ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS drop + # => [pad(16)] + + # Load the faucet ID from memory, replacing the top 4 zeros + mem_loadw_le.STORAGE_START_PTR + # => [faucet_id_prefix, faucet_id_suffix, pad(14)] + + # Register the faucet in the bridge's faucet registry + call.bridge_config::register_faucet + # => [pad(16)] +end diff --git a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm index 1ca3d1ab9d..87a29aaef9 100644 --- a/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm +++ b/crates/miden-agglayer/asm/note_scripts/UPDATE_GER.masm @@ -55,10 +55,10 @@ begin # => [pad(16)] # Load GER_LOWER and GER_UPPER from note storage - mem_loadw_be.STORAGE_PTR_GER_UPPER + mem_loadw_le.STORAGE_PTR_GER_UPPER # => [GER_UPPER[4], pad(12)] swapw - mem_loadw_be.STORAGE_PTR_GER_LOWER + mem_loadw_le.STORAGE_PTR_GER_LOWER # => [GER_LOWER[4], GER_UPPER[4], pad(8)] call.bridge_in::update_ger diff --git a/crates/miden-agglayer/solidity-compat/README.md b/crates/miden-agglayer/solidity-compat/README.md index f93f83b5bc..b45b6edced 100644 --- a/crates/miden-agglayer/solidity-compat/README.md +++ b/crates/miden-agglayer/solidity-compat/README.md @@ -46,5 +46,12 @@ The canonical zeros should match the constants in: ### MMR Frontier Vectors -The `test_generateVectors` adds leaves `0, 1, 2, ...` (as left-padded 32-byte values) -and outputs the root after each addition. +The `test_generateVectors` adds 32 leaves and outputs the root after each addition. +Each leaf uses: + +- `amounts[i] = i + 1` +- `destination_networks[i]` and `destination_addresses[i]` generated deterministically from + a fixed seed in `MMRTestVectors.t.sol` + +This gives reproducible "random-looking" destination parameters while keeping vector generation +stable across machines and reruns. diff --git a/crates/miden-agglayer/solidity-compat/foundry.lock b/crates/miden-agglayer/solidity-compat/foundry.lock index 8aa165ad75..196c826d70 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.lock +++ b/crates/miden-agglayer/solidity-compat/foundry.lock @@ -7,5 +7,11 @@ "name": "v1.14.0", "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" } + }, + "lib/openzeppelin-contracts-upgradeable": { + "branch": { + "name": "release-v4.9", + "rev": "2d081f24cac1a867f6f73d512f2022e1fa987854" + } } } \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/foundry.toml b/crates/miden-agglayer/solidity-compat/foundry.toml index c22ad7e3f6..d8633d0d32 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.toml +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -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/", +] # Emit extra output for test vector generation ffi = false diff --git a/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000000..2d081f24ca --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json new file mode 100644 index 0000000000..b0819ea63d --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json @@ -0,0 +1,82 @@ +{ + "amount": 100000000000000, + "destination_address": "0x00000000b0E79c68cafC54802726C6F102Cca300", + "destination_network": 20, + "global_exit_root": "0xe1cbfbde30bd598ee9aa2ac913b60d53e3297e51ed138bf86c500dd7d2391e7d", + "global_index": "0x0000000000000000000000000000000000000000000000010000000000039e88", + "leaf_type": 0, + "leaf_value": "0xc58420b9b4ba439bb5f6f68096270f4df656553ec67150d4d087416b9ef6ea9d", + "mainnet_exit_root": "0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c", + "metadata_hash": "0x945d61756eddd06a335ceff22d61480fc2086e85e74a55db5485f814626247d5", + "origin_network": 0, + "origin_token_address": "0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF", + "rollup_exit_root": "0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858", + "smt_proof_local_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0xe37d456460231cf80063f57ee83a02f70d810c568b3bfb71156d52445f7a885a", + "0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344", + "0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d", + "0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968", + "0x3236bf576fca1adf85917ec7888c4b89cce988564b6028f7d66807763aaa7b04", + "0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af", + "0x054ba828046324ff4794fce22adefb23b3ce749cd4df75ade2dc9f41dd327c31", + "0x4e9220076c344bf223c7e7cb2d47c9f0096c48def6a9056e41568de4f01d2716", + "0xca6369acd49a7515892f5936227037cc978a75853409b20f1145f1d44ceb7622", + "0x5a925caf7bfdf31344037ba5b42657130d049f7cb9e87877317e79fce2543a0c", + "0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb", + "0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc", + "0x4111a1a05cc06ad682bb0f213170d7d57049920d20fc4e0f7556a21b283a7e2a", + "0x77a0f8b0e0b4e5a57f5e381b3892bb41a0bcdbfdf3c7d591fae02081159b594d", + "0x361122b4b1d18ab577f2aeb6632c690713456a66a5670649ceb2c0a31e43ab46", + "0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0", + "0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0", + "0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2", + "0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9", + "0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377", + "0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652", + "0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef", + "0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d", + "0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0", + "0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e", + "0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e", + "0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322", + "0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735", + "0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9" + ], + "smt_proof_rollup_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json b/crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json new file mode 100644 index 0000000000..b04fbdf844 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/exit_roots.json @@ -0,0 +1,14 @@ +{ + "global_exit_roots": [ + "0x207f0b7db488bbc423fc3d12db21b97e574453e12b49ca21205181af677d7b04", + "0x8e10e03b7db5ffe76edbea651052f8045289ece97947297de6279ce9f6730252" + ], + "mainnet_exit_roots": [ + "0x98c911b6dcface93fd0bb490d09390f2f7f9fcf36fc208cbb36528a229298326", + "0xbb71d991caf89fe64878259a61ae8d0b4310c176e66d90fd2370b02573e80c90" + ], + "rollup_exit_roots": [ + "0x6a2533a24cc2a3feecf5c09b6a270bbb24a5e2ce02c18c0e26cd54c3dddc2d70", + "0xd9b546933b59acd388dc0c6520cbf2d4dbb9bac66f74f167ba70f221d82a440c" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json new file mode 100644 index 0000000000..8d89835c88 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json @@ -0,0 +1,10 @@ +{ + "amount": 2000000000000000000, + "destination_address": "0xD9b20Fe633b609B01081aD0428e81f8Dd604F5C5", + "destination_network": 7, + "leaf_type": 0, + "leaf_value": "0xb67e42971034605367b7e92d1ad1d4648c3ffe0bea9b08115cd9aa2e616b2f88", + "metadata_hash": "0x6c7a91a5fb41dee8f0bc1c86b5587334583186f14acfa253e2f7c2833d1d6fdf", + "origin_network": 0, + "origin_token_address": "0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7" +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json index e51ea4e4e9..79c76364dc 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json @@ -1,4 +1,38 @@ { + "amounts": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32 + ], "counts": [ 1, 2, @@ -33,72 +67,141 @@ 31, 32 ], + "destination_addresses": [ + "0xB48074703337bEf6e94A9e2E1FfFe71632B42D56", + "0xBA60cd3cBD12619e6983B5D0E1CbcF2f4fed9d7b", + "0x89510362d6EdeB958F059727C9eD0F99298aAFa4", + "0xD62Cf6356E0a48e2014b71Cf942BEbBbFb00F7d7", + "0xFA5eacb9668731D74F2BB5Ad5bfB319f5A91c87D", + "0x90DD6647e5c91f9104a548876868a54795696B34", + "0x0E76F5f993A9a7f961e06397BC71d15c278A0b6c", + "0xe022226D1fFcCf12ac0e84D0aB9430F3fd56C613", + "0x1F9ecff77E28Bca8Ef18434B842A30579Bfd4EaA", + "0xe51D207B549Db157BeE9faeBd51C35aB47d180EF", + "0x9f30d6d0335E91e0593f13a567E4Fee661e1259F", + "0xE8F13Da1BDb719ba364a890a623454040A932eCf", + "0xb6EE19bf265563aA76dbe202e8dC71F8f42a58B1", + "0xf62d45e4D0DC57259B4557b5d79Ea23F67D0E381", + "0xaa94f5480aD0C906044E5E7Da8BB6BC4395aA498", + "0x060ddd9f6e6CF285004e33C30b46710ad75918Dd", + "0x8B743c166e1dA1444781AD2b5Fe2291578ABCeb1", + "0x8B08d9A773273Df976fb7448D38FeEeB15Dc34F8", + "0xbe931f6F189e6F8Da14f7B67Eb2E67b5D7f71c1d", + "0x2F891C182b23d1422D8Fddd9CC30B25BB849Bd5F", + "0x93fD7DEd75058ABA1B76C35c4Ac4e9355e596EdC", + "0x25B9eBC8D7d48a6B0e71e82Aa66832aCC9419E3A", + "0xbb086ECaC1316B81107e3CA591ef645831094E5a", + "0x08c7a5Db749DEf9280108Ec5e0354d4957CB17cF", + "0x0da76aA44116fad143F778f25907046E52F8c4d3", + "0xcFd0a3bfA35E771aad88C64EF0A310efF6730cDa", + "0xa7439b51638F31f054C93EC869C8c7E982699BAC", + "0x5C9A97f096CB18903994C44ddC07FfD921490B2c", + "0x0e52786aF0b48D764a255f6506C9C297d5BA2Dc3", + "0x5C2093921171F2c2d657eAA681D463Fe36c965d1", + "0xf8de801F1ba2a676d96Eb1F1ccB0B0CADFCbbE9e", + "0x31D230FAbAd05777Bb3E1a062e781446Bc422b80" + ], + "destination_networks": [ + 1538671592, + 1271685039, + 2812858243, + 1717044446, + 1618236512, + 1846799397, + 1114625417, + 1980472020, + 3445581035, + 1216050355, + 1334555263, + 1595653741, + 1406956437, + 2339872987, + 1591634953, + 2036330440, + 948554316, + 1629580568, + 4209912969, + 3528172732, + 4197496357, + 2020389543, + 1365501531, + 2591126838, + 273689805, + 543018504, + 3291055054, + 2685286074, + 3030491074, + 4166649488, + 1541470110, + 1181416010 + ], "leaves": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000000000000000000000000000003", - "0x0000000000000000000000000000000000000000000000000000000000000004", - "0x0000000000000000000000000000000000000000000000000000000000000005", - "0x0000000000000000000000000000000000000000000000000000000000000006", - "0x0000000000000000000000000000000000000000000000000000000000000007", - "0x0000000000000000000000000000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000000000000000000000000000009", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b", - "0x000000000000000000000000000000000000000000000000000000000000000c", - "0x000000000000000000000000000000000000000000000000000000000000000d", - "0x000000000000000000000000000000000000000000000000000000000000000e", - "0x000000000000000000000000000000000000000000000000000000000000000f", - "0x0000000000000000000000000000000000000000000000000000000000000010", - "0x0000000000000000000000000000000000000000000000000000000000000011", - "0x0000000000000000000000000000000000000000000000000000000000000012", - "0x0000000000000000000000000000000000000000000000000000000000000013", - "0x0000000000000000000000000000000000000000000000000000000000000014", - "0x0000000000000000000000000000000000000000000000000000000000000015", - "0x0000000000000000000000000000000000000000000000000000000000000016", - "0x0000000000000000000000000000000000000000000000000000000000000017", - "0x0000000000000000000000000000000000000000000000000000000000000018", - "0x0000000000000000000000000000000000000000000000000000000000000019", - "0x000000000000000000000000000000000000000000000000000000000000001a", - "0x000000000000000000000000000000000000000000000000000000000000001b", - "0x000000000000000000000000000000000000000000000000000000000000001c", - "0x000000000000000000000000000000000000000000000000000000000000001d", - "0x000000000000000000000000000000000000000000000000000000000000001e", - "0x000000000000000000000000000000000000000000000000000000000000001f" + "0xe460585d9b2385592b26a34d6908ea58165586cb39e5e6cb365b68246d29d7f8", + "0x5a7295b074b2ffeb07bd8bacbdd97aa97b0b269db43779112ef24b52548a9a2a", + "0xde239e1e8b54de83c9b0e3f32c269b265dd0efcda92c93a2146f44302e604080", + "0x98681050a4c0e39d25f1a44d95b343a05f7139cc882f628c569b3a1ae889f0e6", + "0xd3d70b40cc2a71e9a4996a2afaabcafe93af95ba9de147e3835ccddba2d82fdd", + "0xd46fec5943f6d40c9a68076fbc325daf7763607aaa60ca9be297cade5a1efca5", + "0x4c54e0aab6332cea9a9f867933caee83c6167aa78f663129d10e56cee35aacdd", + "0xf487aba0c467c53aa4fc9a7319817e1448efd774dedb235a1ab95a5dd2592d21", + "0xc734b7fd5abe87f4dff06da98980e19894117e92738e27e8dc0826eb4dee7202", + "0x8bcc65728c792dfaa58c6b63d192c2e37cd3db7c62774e7b40b9b3232597073a", + "0x6dbb052d9082cf78a0464cae809cd6c1be9d5657fc75a0fa1efade46f047aa01", + "0x11ea20a8fb14ed8b5ba47e83935f4dc1c032be3a3a9895a65fabed6e1adbef5c", + "0xd108801a4cfa732a19995a6f930ccdda98e91ce393f55eae7f63781568b44c74", + "0x423b7a7716ba307d27c05a6bbfde03b35c9544dffcf6702f69a205cff40a51da", + "0xed832ce8f80ed861bd13b1104490724dd38ab1c9ff18fd8e02ad13eb287af68f", + "0x6eca57794d8d55ec934427971898952017d87bd2773b64c554629f32f55fc7dc", + "0xc7abf795f5ebe46e9f86ba72d58f38ef535475cc41a11913fa1ec51cf902ee1a", + "0xbffebb2a3584cb6f96af4f8da6f5eea2e64066f0caa4bc6f44abb69b621a2b79", + "0x04de39dc7a9f11eba923271d07b5fda4f6b38012858a9a5a9d8f6557706981bd", + "0xef5e2f249fce6c67f5483b52e87384c6a6f6b5f8f102ecfede50cc9f8dfa78af", + "0x34e1511b36260dd619fcb205311055b87d31bb6440c9fb2a8b272bc1dcb1d699", + "0x0640b605ee9f8d8b38118c8dd1f51ca30f3b3f9037c29e598f39b91326825c46", + "0xfe7de1151f56cc10894b6bd63fc995a741c54d9069ee97247cb28627a4838da8", + "0xf17bb6827fe8873b839ecefe872776f757ca087dd65c2c2882523b71dcd24f05", + "0x7a11106b01c8d98348739c89007dddca673f18e9c38ef2d953315a1a49b23ce0", + "0xa7f0a37834fab9ce2cfbe364ccc4c50c88d48a061f0901889cc4fdc6b088a3ea", + "0xb386fae6a43e096a3d66147212a4fc756f7ed921febb2404f1d060111e4521e8", + "0x98484766860a98231a6834276f1ca84c8cf381e4931d635268b9b7d9db976958", + "0xd5007290e81283abd144a619da55be689e7b3eeb8a8b79f0de5e1f2793b056fe", + "0xac6812ede94056979e789ac4bd7dc5e4e682ea93aaaa1aadb22645ec44e21772", + "0xdc0662d88af437d468ed541ee9088464770bbd149a5ce5b3cdd9e836888c5b9d", + "0x6c8e78ff6214e87c5a791423385e31659921f3bb09376b302dd3933f98f346b4" ], + "origin_token_address": "0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891", "roots": [ - "0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757", - "0x4a90a2c108a29b7755a0a915b9bb950233ce71f8a01859350d7b73cc56f57a62", - "0x2757cc260a62cc7c7708c387ea99f2a6bb5f034ed00da845734bec4d3fa3abfe", - "0xcb305ccda4331eb3fd9e17b81a5a0b336fb37a33f927698e9fb0604e534c6a01", - "0xa377a6262d3bae7be0ce09c2cc9f767b0f31848c268a4bdc12b63a451bb97281", - "0x440213f4dff167e3f5c655fbb6a3327af3512affed50ce3c1a3f139458a8a6d1", - "0xdd716d2905f2881005341ff1046ced5ee15cc63139716f56ed6be1d075c3f4a7", - "0xd6ebf96fcc3344fa755057b148162f95a93491bc6e8be756d06ec64df4df90fc", - "0x8b3bf2c95f3d0f941c109adfc3b652fadfeaf6f34be52524360a001cb151b5c9", - "0x74a5712654eccd015c44aca31817fd8bee8da400ada986a78384ef3594f2d459", - "0x95dd1209b92cce04311dfc8670b03428408c4ff62beb389e71847971f73702fa", - "0x0a83f3b2a75e19b7255b1de379ea9a71aef9716a3aef20a86abe625f088bbebf", - "0x601ba73b45858be76c8d02799fd70a5e1713e04031aa3be6746f95a17c343173", - "0x93d741c47aa73e36d3c7697758843d6af02b10ed38785f367d1602c8638adb0d", - "0x578f0d0a9b8ed5a4f86181b7e479da7ad72576ba7d3f36a1b72516aa0900c8ac", - "0x995c30e6b58c6e00e06faf4b5c94a21eb820b9db7ad30703f8e3370c2af10c11", - "0x49fb7257be1e954c377dc2557f5ca3f6fc7002d213f2772ab6899000e465236c", - "0x06fee72550896c50e28b894c60a3132bfe670e5c7a77ab4bb6a8ffb4abcf9446", - "0xbba3a807e79d33c6506cd5ecb5d50417360f8be58139f6dbe2f02c92e4d82491", - "0x1243fbd4d21287dbdaa542fa18a6a172b60d1af2c517b242914bdf8d82a98293", - "0x02b7b57e407fbccb506ed3199922d6d9bd0f703a1919d388c76867399ed44286", - "0xa15e7890d8f860a2ef391f9f58602dec7027c19e8f380980f140bbb92a3e00ba", - "0x2cb7eff4deb9bf6bbb906792bc152f1e63759b30e7829bfb5f3257ee600303f5", - "0xb1b034b4784411dc6858a0da771acef31be60216be0520a7950d29f66aee1fc5", - "0x3b17098f521ca0719e144a12bb79fdc51a3bc70385b5c2ee46b5762aae741f4f", - "0xd3e054489aa750d41938143011666a83e5e6b1477cce5ad612447059c2d8b939", - "0x6d15443ab2f39cce7fbe131843cdad6f27400eb179efb866569dd48baaf3ed4d", - "0xf9386ef40320c369185e48132f8fbf2f3e78d9598495dd342bcf4f41388d460d", - "0xb618ebe1f7675ef246a8cbb93519469076d5caacd4656330801537933e27b172", - "0x6c8c90b5aa967c98061a2dd09ea74dfb61fd9e86e308f14453e9e0ae991116de", - "0x06f51cfc733d71220d6e5b70a6b33a8d47a1ab55ac045fac75f26c762d7b29c9", - "0x82d1ddf8c6d986dee7fc6fa2d7120592d1dc5026b1bb349fcc9d5c73ac026f56" + "0xacd6f8510c036081e605dd2c8749d2b7d3b289913514d10af9538cb4b32b7ded", + "0x2d7b622637d38f862a074a0160bc1e54ad7df147ff3374af82777b37021b22e1", + "0xf9bdf29ab9c4cbd2927b759b9f8ddafa90317bdb91f388b8eee08038ff5ded00", + "0x80134ca84d0d742662f3ec22543f4cf33f02dc0b628f51d1df1c521ef3018395", + "0x21d6f3b63306929d624f01ffdbe216acb822bf080bcf04b7e6021db957e7bee4", + "0x7932d55a970d094161976d0b562805779d55a81b08a501983c2b121a0c989a1e", + "0x43f09c6c8a277ee6fbc0e3f8261ba4570f32d1cbfff06bf662aa8e5feeb742bc", + "0x9ae3a76a5c7fcc2af6e3cb937b7e1a4ba397a46029987b06fec29257ba408564", + "0x007e432139766ea419be4aeda41a59e20114c0b772b61e43b3a344fa1c4e1196", + "0xdf60f37334585bc10d67b107b417a14181158ac9828f56a9337684a81e7405d9", + "0xba49ac55a723278ef6cd8f12193a405bc90cd2b6e88f8791f8d48d69fe952104", + "0x4ab8529bce44bcfb8c8e90c9adebebca9e51f44b0e8a048d99bf7717cb58eae7", + "0xf9313f060db170a5287bcc78443267e893f638731dd48a9131b120f9c5833f88", + "0x49a9e6e504f2a6938bbefba42ec2b4930eed298a04eac403af1e0a6286017960", + "0xe318ce76597523c02da0094bcfd970e88c9544c6393d9bfe17d96e2a17f4856d", + "0x00d4099acc3d2a2cdd76f693fb527b218f369bc8e92af4a39328809738497a9d", + "0xf4db3da65c8fda88ad4a1ad1aca66e9260d5230a962791b57d396948a78fe96e", + "0x6813db5a7b4ac98c11d84412df7d6552941d30c7adb95e7025b13d747cf0f3f7", + "0xf1e93cbb96e5fabaee7cbb44f87f44832c9c290a5f85631d8c86493bab6ba0d5", + "0x654a2e78a6e49c969a0fedad0e4372862950ca371406c122779cf62e16dfe7e7", + "0x1a07ce13254cfb6697256a401063d6c43e5a89b8b1945c90bce62c464da1ba27", + "0xedaf2d835d1e6fdd801555835b2cadcd04517f8668f30658019869d0376c6c36", + "0x82adda5fd38a4718f37b2d4fe9fe99b364cced5de9bdfa4c6bdcd118da42c64c", + "0x2d28e62dd13f99153b5e9eb4d68cc1f99a5bd510375f2d1ed522c0062a2d38d7", + "0xd87e80ebe2f69df6735911707780df6b882189db786b5507310249a26d3db69d", + "0x5406d2fbc12edcccd2b8c755b7063ababc760ce23da62032d500a10d49756994", + "0xce99e7d0f9d77226cae034297dfec349d866f892eb753a8c7f5bba4bec52364c", + "0x4419d0e6c47cac3e4fc917f91d878582ed4496bef8e7df219be4d483e496ff0a", + "0xafe2c2b44e58c34576299a201d4918f47d5a48b6fa7a229eaf59e226120b12ac", + "0x9d2989190f9edb660b043a55f3051412280dd7bb7d4d042e3695d3a2b23f5b8d", + "0x18b772e2e093d5f69151c3b6da00d42a2066d1f5980e5f9210ae902f5a5643ca", + "0x6717e563a6c40e1562235c4cbbc2ba0de5be6be07101715e8d3922361b77d394" ] } \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol new file mode 100644 index 0000000000..4352c232eb --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectors.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; + +/** + * @title ClaimAssetTestVectors + * @notice Test contract that generates comprehensive test vectors for verifying + * compatibility between Solidity's claimAsset and Miden's implementation. + * + * Generates vectors for both LeafData and ProofData from a real transaction. + * + * Run with: forge test -vv --match-contract ClaimAssetTestVectors + * + * The output can be compared against the Rust ClaimNoteStorage implementation. + */ +contract ClaimAssetTestVectors is Test, DepositContractV2 { + /** + * @notice Generates claim asset test vectors from real Katana transaction and saves to JSON. + * Uses real transaction data from Katana explorer: + * https://katanascan.com/tx/0x685f6437c4a54f5d6c59ea33de74fe51bc2401fea65dc3d72a976def859309bf + * + * Output file: test-vectors/claim_asset_vectors.json + */ + function test_generateClaimAssetVectors() public { + string memory obj = "root"; + + // ====== PROOF DATA ====== + // Scoped block keeps stack usage under Solidity limits. + { + // SMT proof for local exit root (32 nodes) + bytes32[32] memory smtProofLocalExitRoot = [ + bytes32(0x0000000000000000000000000000000000000000000000000000000000000000), + bytes32(0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5), + bytes32(0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30), + bytes32(0xe37d456460231cf80063f57ee83a02f70d810c568b3bfb71156d52445f7a885a), + bytes32(0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344), + bytes32(0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d), + bytes32(0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968), + bytes32(0x3236bf576fca1adf85917ec7888c4b89cce988564b6028f7d66807763aaa7b04), + bytes32(0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af), + bytes32(0x054ba828046324ff4794fce22adefb23b3ce749cd4df75ade2dc9f41dd327c31), + bytes32(0x4e9220076c344bf223c7e7cb2d47c9f0096c48def6a9056e41568de4f01d2716), + bytes32(0xca6369acd49a7515892f5936227037cc978a75853409b20f1145f1d44ceb7622), + bytes32(0x5a925caf7bfdf31344037ba5b42657130d049f7cb9e87877317e79fce2543a0c), + bytes32(0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb), + bytes32(0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc), + bytes32(0x4111a1a05cc06ad682bb0f213170d7d57049920d20fc4e0f7556a21b283a7e2a), + bytes32(0x77a0f8b0e0b4e5a57f5e381b3892bb41a0bcdbfdf3c7d591fae02081159b594d), + bytes32(0x361122b4b1d18ab577f2aeb6632c690713456a66a5670649ceb2c0a31e43ab46), + bytes32(0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0), + bytes32(0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0), + bytes32(0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2), + bytes32(0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9), + bytes32(0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377), + bytes32(0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652), + bytes32(0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef), + bytes32(0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d), + bytes32(0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0), + bytes32(0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e), + bytes32(0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e), + bytes32(0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322), + bytes32(0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735), + bytes32(0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9) + ]; + + // forge-std JSON serialization supports `bytes32[]` but not `bytes32[32]`. + bytes32[] memory smtProofLocalExitRootDyn = new bytes32[](32); + for (uint i = 0; i < 32; i++) { + smtProofLocalExitRootDyn[i] = smtProofLocalExitRoot[i]; + } + + // SMT proof for rollup exit root (32 nodes - all zeros for this rollup claim). + bytes32[] memory smtProofRollupExitRootDyn = new bytes32[](32); + + // Global index (uint256) - encodes rollup_id and deposit_count. + uint256 globalIndex = 18446744073709788808; + + // Exit roots + bytes32 mainnetExitRoot = 0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c; + bytes32 rollupExitRoot = 0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858; + + // Compute global exit root: keccak256(mainnetExitRoot || rollupExitRoot) + bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoot, rollupExitRoot); + + vm.serializeBytes32(obj, "smt_proof_local_exit_root", smtProofLocalExitRootDyn); + vm.serializeBytes32(obj, "smt_proof_rollup_exit_root", smtProofRollupExitRootDyn); + vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); + vm.serializeBytes32(obj, "mainnet_exit_root", mainnetExitRoot); + vm.serializeBytes32(obj, "rollup_exit_root", rollupExitRoot); + vm.serializeBytes32(obj, "global_exit_root", globalExitRoot); + } + + // ====== LEAF DATA ====== + // Scoped block keeps stack usage under Solidity limits. + { + uint8 leafType = 0; // 0 for ERC20/ETH transfer + uint32 originNetwork = 0; + address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; + uint32 destinationNetwork = 20; + address destinationAddress = 0x00000000b0E79c68cafC54802726C6F102Cca300; + uint256 amount = 100000000000000; // 1e14 (0.0001 vbETH) + + // Original metadata from the transaction (ABI encoded: name, symbol, decimals) + // name = "Vault Bridge ETH", symbol = "vbETH", decimals = 18 + bytes memory metadata = hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000105661756c7420427269646765204554480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000057662455448000000000000000000000000000000000000000000000000000000"; + bytes32 metadataHash = keccak256(metadata); + + // Compute the leaf value using the official DepositContractV2 implementation + bytes32 leafValue = getLeafValue( + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + vm.serializeUint(obj, "leaf_type", leafType); + vm.serializeUint(obj, "origin_network", originNetwork); + vm.serializeAddress(obj, "origin_token_address", originTokenAddress); + vm.serializeUint(obj, "destination_network", destinationNetwork); + vm.serializeAddress(obj, "destination_address", destinationAddress); + vm.serializeUint(obj, "amount", amount); + vm.serializeBytes32(obj, "metadata_hash", metadataHash); + string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); + + // Save to file + string memory outputPath = "test-vectors/claim_asset_vectors.json"; + vm.writeJson(json, outputPath); + } + } +} diff --git a/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol b/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol new file mode 100644 index 0000000000..9224231a00 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ExitRoots.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; + +/** + * @title ExitRootsTestVectors + * @notice Test contract that generates global exit root test vectors from + * mainnet-rollup exit root pairs. + * + * Run with: forge test -vv --match-contract ExitRootsTestVectors + * + * The output can be compared against Rust implementations that compute + * the global exit root as keccak256(mainnetExitRoot || rollupExitRoot). + */ +contract ExitRootsTestVectors is Test { + + /** + * @notice Generates global exit root vectors from mainnet-rollup pairs + * and saves to JSON file. + * + * Output file: test-vectors/exit_roots.json + */ + function test_generateExitRootVectors() public { + // Input: pairs of (mainnetExitRoot, rollupExitRoot) from mainnet transactions + // Source transaction hashes from https://explorer.lumia.org/: + // TX 1: 0xe1a20811d757c48eba534f63041f58cd39eec762bfb6e4496dccf4e675fd5619 + // TX 2: 0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188 + bytes32[] memory mainnetExitRoots = new bytes32[](2); + bytes32[] memory rollupExitRoots = new bytes32[](2); + + // Pair 1 (TX: 0xe1a20811d757c48eba534f63041f58cd39eec762bfb6e4496dccf4e675fd5619) + mainnetExitRoots[0] = bytes32(0x98c911b6dcface93fd0bb490d09390f2f7f9fcf36fc208cbb36528a229298326); + rollupExitRoots[0] = bytes32(0x6a2533a24cc2a3feecf5c09b6a270bbb24a5e2ce02c18c0e26cd54c3dddc2d70); + + // Pair 2 (TX: 0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188) + mainnetExitRoots[1] = bytes32(0xbb71d991caf89fe64878259a61ae8d0b4310c176e66d90fd2370b02573e80c90); + rollupExitRoots[1] = bytes32(0xd9b546933b59acd388dc0c6520cbf2d4dbb9bac66f74f167ba70f221d82a440c); + + // Compute global exit roots + bytes32[] memory globalExitRoots = new bytes32[](mainnetExitRoots.length); + for (uint256 i = 0; i < mainnetExitRoots.length; i++) { + globalExitRoots[i] = GlobalExitRootLib.calculateGlobalExitRoot( + mainnetExitRoots[i], + rollupExitRoots[i] + ); + } + + // Serialize parallel arrays to JSON + string memory obj = "root"; + vm.serializeBytes32(obj, "mainnet_exit_roots", mainnetExitRoots); + vm.serializeBytes32(obj, "rollup_exit_roots", rollupExitRoots); + string memory json = vm.serializeBytes32(obj, "global_exit_roots", globalExitRoots); + + // Save to file + string memory outputPath = "test-vectors/exit_roots.json"; + vm.writeJson(json, outputPath); + console.log("Saved exit root vectors to:", outputPath); + } +} diff --git a/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol new file mode 100644 index 0000000000..ab4fdf8443 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/LeafValueTestVectors.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; + +/** + * @title LeafValueTestVectors + * @notice Test contract that generates test vectors for verifying compatibility + * between Solidity's getLeafValue and Miden's keccak hash implementation. + * + * Run with: forge test -vv --match-contract LeafValueTestVectors + * + * The output can be compared against the Rust get_leaf_value implementation. + */ +contract LeafValueTestVectors is Test, DepositContractV2 { + /** + * @notice Generates leaf value test vectors and saves to JSON file. + * Uses real transaction data from Lumia explorer: + * https://explorer.lumia.org/tx/0xe64254ff002b3d46b46af077fa24c6ef5b54d950759d70d6d9a693b1d36de188 + * + * Output file: test-vectors/leaf_value_vectors.json + */ + function test_generateLeafValueVectors() public { + // Test vector from real Lumia bridge transaction + uint8 leafType = 0; // 0 for ERC20/ETH transfer + uint32 originNetwork = 0; + address originTokenAddress = 0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7; + uint32 destinationNetwork = 7; + address destinationAddress = 0xD9b20Fe633b609B01081aD0428e81f8Dd604F5C5; + uint256 amount = 2000000000000000000; // 2e18 + + // Original metadata from the transaction (ABI encoded: name, symbol, decimals) + bytes memory metadata = hex"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000b4c756d696120546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054c554d4941000000000000000000000000000000000000000000000000000000"; + bytes32 metadataHash = keccak256(metadata); + + // Compute the leaf value using the official DepositContractV2 implementation + bytes32 leafValue = getLeafValue( + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + // Serialize to JSON + string memory obj = "root"; + vm.serializeUint(obj, "leaf_type", leafType); + vm.serializeUint(obj, "origin_network", originNetwork); + vm.serializeAddress(obj, "origin_token_address", originTokenAddress); + vm.serializeUint(obj, "destination_network", destinationNetwork); + vm.serializeAddress(obj, "destination_address", destinationAddress); + vm.serializeUint(obj, "amount", amount); + vm.serializeBytes32(obj, "metadata_hash", metadataHash); + string memory json = vm.serializeBytes32(obj, "leaf_value", leafValue); + + // Save to file + string memory outputPath = "test-vectors/leaf_value_vectors.json"; + vm.writeJson(json, outputPath); + } +} diff --git a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol index 2e5b016232..1f6ab63d2c 100644 --- a/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/MMRTestVectors.t.sol @@ -2,20 +2,63 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "@agglayer/v2/lib/DepositContractBase.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; /** * @title MMRTestVectors * @notice Test contract that generates test vectors for verifying compatibility * between Solidity's DepositContractBase and Miden's MMR Frontier implementation. + * + * Leaves are constructed via getLeafValue using the same hardcoded fields that + * bridge_out.masm uses (leafType=0, originNetwork=64, originTokenAddress=fixed random value, + * metadataHash=0), parametrised by amount (i+1) and deterministic per-leaf + * destination network/address values derived from a fixed seed. * * Run with: forge test -vv --match-contract MMRTestVectors * * The output can be compared against the Rust KeccakMmrFrontier32 implementation * in crates/miden-testing/tests/agglayer/mmr_frontier.rs */ -contract MMRTestVectors is Test, DepositContractBase { - +contract MMRTestVectors is Test, DepositContractV2 { + + // Constants matching bridge_out.masm hardcoded values + uint8 constant LEAF_TYPE = 0; + uint32 constant ORIGIN_NETWORK = 64; + address constant ORIGIN_TOKEN_ADDR = 0x7a6fC3e8b57c6D1924F1A9d0E2b3c4D5e6F70891; + bytes32 constant METADATA_HASH = bytes32(0); + + // Fixed seed for deterministic "random" destination vectors. + // Keeping this constant ensures everyone regenerates the exact same JSON vectors. + uint256 constant VECTOR_SEED = uint256(keccak256("miden::agglayer::mmr_frontier_vectors::v2")); + + /** + * @notice Builds a leaf hash identical to what bridge_out.masm would produce for the + * given amount. + */ + function _createLeaf( + uint256 amount, + uint32 destinationNetwork, + address destinationAddress + ) internal pure returns (bytes32) { + return getLeafValue( + LEAF_TYPE, + ORIGIN_NETWORK, + ORIGIN_TOKEN_ADDR, + destinationNetwork, + destinationAddress, + amount, + METADATA_HASH + ); + } + + function _destinationNetworkAt(uint256 idx) internal pure returns (uint32) { + return uint32(uint256(keccak256(abi.encodePacked(VECTOR_SEED, bytes1(0x01), idx)))); + } + + function _destinationAddressAt(uint256 idx) internal pure returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(VECTOR_SEED, bytes1(0x02), idx))))); + } + /** * @notice Generates the canonical zeros and saves to JSON file. * ZERO_0 = 0x0...0 (32 zero bytes) @@ -43,28 +86,47 @@ contract MMRTestVectors is Test, DepositContractBase { /** * @notice Generates MMR frontier vectors (leaf-root pairs) and saves to JSON file. - * Uses parallel arrays instead of array of objects for cleaner serialization. + * Each leaf is created via _createLeaf(i+1, network[i], address[i]) so that: + * - amounts are 1..32 + * - destination networks/addresses are deterministic per index from VECTOR_SEED + * + * The destination vectors are also written to JSON so the Rust bridge_out test + * can construct matching B2AGG notes. + * * Output file: test-vectors/mmr_frontier_vectors.json */ function test_generateVectors() public { bytes32[] memory leaves = new bytes32[](32); bytes32[] memory roots = new bytes32[](32); uint256[] memory counts = new uint256[](32); + uint256[] memory amounts = new uint256[](32); + uint256[] memory destinationNetworks = new uint256[](32); + address[] memory destinationAddresses = new address[](32); for (uint256 i = 0; i < 32; i++) { - bytes32 leaf = bytes32(i); + uint256 amount = i + 1; + uint32 destinationNetwork = _destinationNetworkAt(i); + address destinationAddress = _destinationAddressAt(i); + bytes32 leaf = _createLeaf(amount, destinationNetwork, destinationAddress); _addLeaf(leaf); leaves[i] = leaf; roots[i] = getRoot(); counts[i] = depositCount; + amounts[i] = amount; + destinationNetworks[i] = destinationNetwork; + destinationAddresses[i] = destinationAddress; } // Serialize parallel arrays to JSON string memory obj = "root"; vm.serializeBytes32(obj, "leaves", leaves); vm.serializeBytes32(obj, "roots", roots); - string memory json = vm.serializeUint(obj, "counts", counts); + vm.serializeUint(obj, "counts", counts); + vm.serializeUint(obj, "amounts", amounts); + vm.serializeUint(obj, "destination_networks", destinationNetworks); + vm.serializeAddress(obj, "origin_token_address", ORIGIN_TOKEN_ADDR); + string memory json = vm.serializeAddress(obj, "destination_addresses", destinationAddresses); // Save to file string memory outputPath = "test-vectors/mmr_frontier_vectors.json"; diff --git a/crates/miden-agglayer/src/b2agg_note.rs b/crates/miden-agglayer/src/b2agg_note.rs index 255082032a..602fdacfac 100644 --- a/crates/miden-agglayer/src/b2agg_note.rs +++ b/crates/miden-agglayer/src/b2agg_note.rs @@ -123,7 +123,8 @@ fn build_note_storage( ) -> Result { let mut elements = Vec::with_capacity(6); - elements.push(Felt::new(destination_network as u64)); + let destination_network = u32::from_le_bytes(destination_network.to_be_bytes()); + elements.push(Felt::from(destination_network)); elements.extend(destination_address.to_elements()); NoteStorage::new(elements) diff --git a/crates/miden-agglayer/src/claim_note.rs b/crates/miden-agglayer/src/claim_note.rs index 0e919fd28f..bc1108f74d 100644 --- a/crates/miden-agglayer/src/claim_note.rs +++ b/crates/miden-agglayer/src/claim_note.rs @@ -3,6 +3,7 @@ use alloc::vec; use alloc::vec::Vec; use miden_core::{Felt, FieldElement, Word}; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::account::AccountId; use miden_protocol::crypto::SequentialCommit; use miden_protocol::crypto::rand::FeltRng; @@ -18,18 +19,17 @@ use miden_protocol::note::{ }; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; -use crate::utils::bytes32_to_felts; -use crate::{EthAddressFormat, EthAmount, claim_script}; +use crate::{EthAddressFormat, EthAmount, GlobalIndex, MetadataHash, claim_script}; // CLAIM NOTE STRUCTURES // ================================================================================================ -/// SMT node representation (32-byte hash) +/// Keccak256 output representation (32-byte hash) #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SmtNode([u8; 32]); +pub struct Keccak256Output([u8; 32]); -impl SmtNode { - /// Creates a new SMT node from a 32-byte array +impl Keccak256Output { + /// Creates a new Keccak256 output from a 32-byte array pub fn new(bytes: [u8; 32]) -> Self { Self(bytes) } @@ -39,45 +39,37 @@ impl SmtNode { &self.0 } - /// Converts the SMT node to 8 Felt elements (32-byte value as 8 u32 values in big-endian) - pub fn to_elements(&self) -> [Felt; 8] { - bytes32_to_felts(&self.0) - } -} - -impl From<[u8; 32]> for SmtNode { - fn from(bytes: [u8; 32]) -> Self { - Self::new(bytes) - } -} - -/// Exit root representation (32-byte hash) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ExitRoot([u8; 32]); - -impl ExitRoot { - /// Creates a new exit root from a 32-byte array - pub fn new(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - /// Returns the inner 32-byte array - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 + /// Converts the Keccak256 output to 8 Felt elements (32-byte value as 8 u32 values in + /// little-endian) + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) } - /// Converts the exit root to 8 Felt elements - pub fn to_elements(&self) -> [Felt; 8] { - bytes32_to_felts(&self.0) + /// Converts the Keccak256 output to two [`Word`]s: `[lo, hi]`. + /// + /// - `lo` contains the first 4 u32-packed felts (bytes 0..16). + /// - `hi` contains the last 4 u32-packed felts (bytes 16..32). + #[cfg(any(test, feature = "testing"))] + pub fn to_words(&self) -> [Word; 2] { + let elements = self.to_elements(); + let lo: [Felt; 4] = elements[0..4].try_into().expect("to_elements returns 8 felts"); + let hi: [Felt; 4] = elements[4..8].try_into().expect("to_elements returns 8 felts"); + [Word::new(lo), Word::new(hi)] } } -impl From<[u8; 32]> for ExitRoot { +impl From<[u8; 32]> for Keccak256Output { fn from(bytes: [u8; 32]) -> Self { Self::new(bytes) } } +/// SMT node representation (32-byte Keccak256 hash) +pub type SmtNode = Keccak256Output; + +/// Exit root representation (32-byte Keccak256 hash) +pub type ExitRoot = Keccak256Output; + /// Proof data for CLAIM note creation. /// Contains SMT proofs and root hashes using typed representations. #[derive(Clone)] @@ -86,8 +78,8 @@ pub struct ProofData { pub smt_proof_local_exit_root: [SmtNode; 32], /// SMT proof for rollup exit root (32 SMT nodes) pub smt_proof_rollup_exit_root: [SmtNode; 32], - /// Global index (uint256 as 8 u32 values) - pub global_index: [u32; 8], + /// Global index (uint256 as 32 bytes) + pub global_index: GlobalIndex, /// Mainnet exit root hash pub mainnet_exit_root: ExitRoot, /// Rollup exit root hash @@ -103,25 +95,19 @@ impl SequentialCommit for ProofData { // Convert SMT proof elements to felts (each node is 8 felts) for node in self.smt_proof_local_exit_root.iter() { - let node_felts = node.to_elements(); - elements.extend(node_felts); + elements.extend(node.to_elements()); } for node in self.smt_proof_rollup_exit_root.iter() { - let node_felts = node.to_elements(); - elements.extend(node_felts); + elements.extend(node.to_elements()); } - // Global index (uint256 as 8 u32 felts) - elements.extend(self.global_index.iter().map(|&v| Felt::new(v as u64))); - - // Mainnet exit root (bytes32 as 8 u32 felts) - let mainnet_exit_root_felts = self.mainnet_exit_root.to_elements(); - elements.extend(mainnet_exit_root_felts); + // Global index (uint256 as 32 bytes) + elements.extend(self.global_index.to_elements()); - // Rollup exit root (bytes32 as 8 u32 felts) - let rollup_exit_root_felts = self.rollup_exit_root.to_elements(); - elements.extend(rollup_exit_root_felts); + // Mainnet and rollup exit roots + elements.extend(self.mainnet_exit_root.to_elements()); + elements.extend(self.rollup_exit_root.to_elements()); elements } @@ -141,15 +127,15 @@ pub struct LeafData { pub destination_address: EthAddressFormat, /// Amount of tokens (uint256) pub amount: EthAmount, - /// ABI encoded metadata (fixed size of 8 u32 values) - pub metadata: [u32; 8], + /// Metadata hash (32 bytes) + pub metadata_hash: MetadataHash, } impl SequentialCommit for LeafData { type Commitment = Word; fn to_elements(&self) -> Vec { - const LEAF_DATA_ELEMENT_COUNT: usize = 32; // 1 + 3 + 1 + 5 + 1 + 5 + 8 + 8 (leafType + padding + networks + addresses + amount + metadata) + const LEAF_DATA_ELEMENT_COUNT: usize = 32; // 1 + 1 + 5 + 1 + 5 + 8 + 8 + 3 (leafType + networks + addresses + amount + metadata + padding) let mut elements = Vec::with_capacity(LEAF_DATA_ELEMENT_COUNT); // LeafType (uint32 as Felt): 0u32 for transfer Ether / ERC20 tokens, 1u32 for message @@ -157,17 +143,16 @@ impl SequentialCommit for LeafData { // for a `CLAIM` note, leafType is always 0 (transfer Ether / ERC20 tokens) elements.push(Felt::ZERO); - // Padding - elements.extend(vec![Felt::ZERO; 3]); - - // Origin network - elements.push(Felt::new(self.origin_network as u64)); + // Origin network (encode as little-endian bytes for keccak) + let origin_network = u32::from_le_bytes(self.origin_network.to_be_bytes()); + elements.push(Felt::from(origin_network)); // Origin token address (5 u32 felts) elements.extend(self.origin_token_address.to_elements()); - // Destination network - elements.push(Felt::new(self.destination_network as u64)); + // Destination network (encode as little-endian bytes for keccak) + let destination_network = u32::from_le_bytes(self.destination_network.to_be_bytes()); + elements.push(Felt::from(destination_network)); // Destination address (5 u32 felts) elements.extend(self.destination_address.to_elements()); @@ -175,8 +160,11 @@ impl SequentialCommit for LeafData { // Amount (uint256 as 8 u32 felts) elements.extend(self.amount.to_elements()); - // Metadata (8 u32 felts) - elements.extend(self.metadata.iter().map(|&v| Felt::new(v as u64))); + // Metadata hash (8 u32 felts) + elements.extend(self.metadata_hash.to_elements()); + + // Padding + elements.extend(vec![Felt::ZERO; 3]); elements } diff --git a/crates/miden-agglayer/src/config_note.rs b/crates/miden-agglayer/src/config_note.rs new file mode 100644 index 0000000000..c8c259a7b2 --- /dev/null +++ b/crates/miden-agglayer/src/config_note.rs @@ -0,0 +1,115 @@ +//! CONFIG_AGG_BRIDGE note creation utilities. +//! +//! This module provides helpers for creating CONFIG_AGG_BRIDGE notes, +//! which are used to register faucets in the bridge's faucet registry. + +extern crate alloc; + +use alloc::string::ToString; +use alloc::vec; + +use miden_assembly::utils::Deserializable; +use miden_core::{Program, Word}; +use miden_protocol::account::AccountId; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::errors::NoteError; +use miden_protocol::note::{ + Note, + NoteAssets, + NoteAttachment, + NoteMetadata, + NoteRecipient, + NoteScript, + NoteStorage, + NoteType, +}; +use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; +use miden_utils_sync::LazyLock; + +// NOTE SCRIPT +// ================================================================================================ + +// Initialize the CONFIG_AGG_BRIDGE note script only once +static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock = LazyLock::new(|| { + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/CONFIG_AGG_BRIDGE.masb")); + let program = + Program::read_from_bytes(bytes).expect("Shipped CONFIG_AGG_BRIDGE script is well-formed"); + NoteScript::new(program) +}); + +// CONFIG_AGG_BRIDGE NOTE +// ================================================================================================ + +/// CONFIG_AGG_BRIDGE note. +/// +/// This note is used to register a faucet in the bridge's faucet registry. +/// It carries the faucet account ID and is always public. +pub struct ConfigAggBridgeNote; + +impl ConfigAggBridgeNote { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Expected number of storage items for a CONFIG_AGG_BRIDGE note. + pub const NUM_STORAGE_ITEMS: usize = 2; + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the CONFIG_AGG_BRIDGE note script. + pub fn script() -> NoteScript { + CONFIG_AGG_BRIDGE_SCRIPT.clone() + } + + /// Returns the CONFIG_AGG_BRIDGE note script root. + pub fn script_root() -> Word { + CONFIG_AGG_BRIDGE_SCRIPT.root() + } + + // BUILDERS + // -------------------------------------------------------------------------------------------- + + /// Creates a CONFIG_AGG_BRIDGE note to register a faucet in the bridge's registry. + /// + /// The note storage contains 2 felts: + /// - `faucet_id_prefix`: The prefix of the faucet account ID + /// - `faucet_id_suffix`: The suffix of the faucet account ID + /// + /// # Parameters + /// - `faucet_account_id`: The account ID of the faucet to register + /// - `sender_account_id`: The account ID of the note creator + /// - `target_account_id`: The bridge account ID that will consume this note + /// - `rng`: Random number generator for creating the note serial number + /// + /// # Errors + /// Returns an error if note creation fails. + pub fn create( + faucet_account_id: AccountId, + sender_account_id: AccountId, + target_account_id: AccountId, + rng: &mut R, + ) -> Result { + // Create note storage with 2 felts: [faucet_id_prefix, faucet_id_suffix] + let storage_values = vec![faucet_account_id.prefix().as_felt(), faucet_account_id.suffix()]; + + let note_storage = NoteStorage::new(storage_values)?; + + // Generate a serial number for the note + let serial_num = rng.draw_word(); + + let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage); + + let attachment = NoteAttachment::from( + NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always) + .map_err(|e| NoteError::other(e.to_string()))?, + ); + let metadata = + NoteMetadata::new(sender_account_id, NoteType::Public).with_attachment(attachment); + + // CONFIG_AGG_BRIDGE notes don't carry assets + let assets = NoteAssets::new(vec![])?; + + Ok(Note::new(assets, metadata, recipient)) + } +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index a1874001d9..ba836a0cfa 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -9,9 +9,6 @@ use miden_protocol::errors::MasmError; // AGGLAYER ERRORS // ================================================================================================ -/// Error Message: "most-significant 4 bytes (addr4) must be zero" -pub const ERR_ADDR4_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes (addr4) must be zero"); - /// Error Message: "B2AGG note attachment target account does not match consuming account" pub const ERR_B2AGG_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("B2AGG note attachment target account does not match consuming account"); /// Error Message: "B2AGG script expects exactly 6 note storage items" @@ -25,9 +22,20 @@ pub const ERR_BRIDGE_NOT_MAINNET: MasmError = MasmError::from_static_str("bridge /// 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: "CONFIG_AGG_BRIDGE note attachment target account does not match consuming account" +pub const ERR_CONFIG_AGG_BRIDGE_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE note attachment target account does not match consuming account"); +/// Error Message: "CONFIG_AGG_BRIDGE expects exactly 2 note storage items" +pub const ERR_CONFIG_AGG_BRIDGE_UNEXPECTED_STORAGE_ITEMS: MasmError = MasmError::from_static_str("CONFIG_AGG_BRIDGE expects exactly 2 note storage items"); + +/// Error Message: "faucet is not registered in the bridge's faucet registry" +pub const ERR_FAUCET_NOT_REGISTERED: MasmError = MasmError::from_static_str("faucet is not registered in the bridge's faucet registry"); + /// 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: "GER not found in storage" +pub const ERR_GER_NOT_FOUND: MasmError = MasmError::from_static_str("GER not found in storage"); + /// Error Message: "invalid claim proof" pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("invalid claim proof"); @@ -37,9 +45,15 @@ pub const ERR_LEADING_BITS_NON_ZERO: MasmError = MasmError::from_static_str("lea /// Error Message: "number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)" pub const ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)"); +/// Error Message: "most-significant 4 bytes must be zero for AccountId" +pub const ERR_MSB_NONZERO: MasmError = MasmError::from_static_str("most-significant 4 bytes must be zero for AccountId"); + /// Error Message: "address limb is not u32" pub const ERR_NOT_U32: MasmError = MasmError::from_static_str("address limb is not u32"); +/// Error Message: "remainder z must be < 10^s" +pub const ERR_REMAINDER_TOO_LARGE: MasmError = MasmError::from_static_str("remainder z must be < 10^s"); + /// Error Message: "rollup index must be zero for a mainnet deposit" pub const ERR_ROLLUP_INDEX_NON_ZERO: MasmError = MasmError::from_static_str("rollup index must be zero for a mainnet deposit"); @@ -49,7 +63,16 @@ pub const ERR_SCALE_AMOUNT_EXCEEDED_LIMIT: MasmError = MasmError::from_static_st /// Error Message: "merkle proof verification failed: provided SMT root does not match the computed root" pub const ERR_SMT_ROOT_VERIFICATION_FAILED: MasmError = MasmError::from_static_str("merkle proof verification failed: provided SMT root does not match the computed root"); +/// Error Message: "x < y*10^s (underflow detected)" +pub const ERR_UNDERFLOW: MasmError = MasmError::from_static_str("x < y*10^s (underflow detected)"); + /// Error Message: "UPDATE_GER note attachment target account does not match consuming account" pub const ERR_UPDATE_GER_TARGET_ACCOUNT_MISMATCH: MasmError = MasmError::from_static_str("UPDATE_GER note attachment target account does not match consuming account"); /// Error Message: "UPDATE_GER script expects exactly 8 note storage items" pub const ERR_UPDATE_GER_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::from_static_str("UPDATE_GER script expects exactly 8 note storage items"); + +/// Error Message: "x must fit into 128 bits (x4..x7 must be 0)" +pub const ERR_X_TOO_LARGE: MasmError = MasmError::from_static_str("x must fit into 128 bits (x4..x7 must be 0)"); + +/// Error Message: "y exceeds max fungible token amount" +pub const ERR_Y_TOO_LARGE: MasmError = MasmError::from_static_str("y exceeds max fungible token amount"); diff --git a/crates/miden-agglayer/src/eth_types/address.rs b/crates/miden-agglayer/src/eth_types/address.rs index f2a94ed6df..7f8810c79d 100644 --- a/crates/miden-agglayer/src/eth_types/address.rs +++ b/crates/miden-agglayer/src/eth_types/address.rs @@ -1,8 +1,9 @@ use alloc::format; use alloc::string::{String, ToString}; +use alloc::vec::Vec; use core::fmt; -use miden_core::FieldElement; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::Felt; use miden_protocol::account::AccountId; use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; @@ -17,15 +18,16 @@ use miden_protocol::utils::{HexParseError, bytes_to_hex_string, hex_to_bytes}; /// /// - 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) +/// - MASM "address\[5\]" limbs: 5 x u32 limbs in *big-endian limb order* (each limb encodes its 4 +/// bytes in little-endian order so felts map to keccak bytes directly): +/// - `address[0]` = bytes[0..4] (most-significant 4 bytes, zero for embedded AccountId) +/// - `address[1]` = bytes[4..8] +/// - `address[2]` = bytes[8..12] +/// - `address[3]` = bytes[12..16] +/// - `address[4]` = bytes[16..20] (least-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 +/// - prefix = bytes[4..12] as a big-endian u64 +/// - suffix = bytes[12..20] 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`). @@ -104,31 +106,22 @@ impl EthAddressFormat { // INTERNAL API - For CLAIM note processing // -------------------------------------------------------------------------------------------- - /// Converts the Ethereum address format into an array of 5 [`Felt`] values for MASM processing. + /// Converts the Ethereum address format into an array of 5 [`Felt`] values for Miden VM. /// /// **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 + /// the address into the MASM `address[5]` representation expected by the /// `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) + /// The returned order matches the Solidity ABI encoding convention (*big-endian limb order*): + /// - `address[0]` = bytes[0..4] (most-significant 4 bytes, zero for embedded AccountId) + /// - `address[1]` = bytes[4..8] + /// - `address[2]` = bytes[8..12] + /// - `address[3]` = bytes[12..16] + /// - `address[4]` = bytes[16..20] (least-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 (felt, chunk) in result.iter_mut().zip(self.0.chunks(4).skip(1).rev()) { - let value = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - // u32 values always fit in Felt, so this conversion is safe - *felt = Felt::try_from(value as u64).expect("u32 value should always fit in Felt"); - } - - result + /// Each limb is interpreted as a little-endian `u32` and stored in a [`Felt`]. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) } /// Converts the Ethereum address format back to an [`AccountId`]. @@ -162,7 +155,7 @@ impl EthAddressFormat { /// 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: + /// Returns prefix and suffix values that match the MASM little-endian limb byte encoding: /// - 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), AddressConversionError> { diff --git a/crates/miden-agglayer/src/eth_types/amount.rs b/crates/miden-agglayer/src/eth_types/amount.rs index dc0b9948cd..3a74d2a8af 100644 --- a/crates/miden-agglayer/src/eth_types/amount.rs +++ b/crates/miden-agglayer/src/eth_types/amount.rs @@ -1,27 +1,30 @@ -use core::fmt; +use alloc::vec::Vec; -use miden_core::FieldElement; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; use miden_protocol::Felt; +use miden_protocol::asset::FungibleAsset; +use primitive_types::U256; +use thiserror::Error; // ================================================================================================ // ETHEREUM AMOUNT ERROR // ================================================================================================ /// Error type for Ethereum amount conversions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] pub enum EthAmountError { /// The amount doesn't fit in the target type. + #[error("amount overflow: value doesn't fit in target type")] Overflow, -} - -impl fmt::Display for EthAmountError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - EthAmountError::Overflow => { - write!(f, "amount overflow: value doesn't fit in target type") - }, - } - } + /// The scaling factor is too large (> 18). + #[error("scaling factor too large: maximum is 18")] + ScaleTooLarge, + /// The scaled-down value doesn't fit in a u64. + #[error("scaled value doesn't fit in u64")] + ScaledValueDoesNotFitU64, + /// The scaled-down value exceeds the maximum fungible token amount. + #[error("scaled value exceeds the maximum fungible token amount")] + ScaledValueExceedsMaxFungibleAmount, } // ================================================================================================ @@ -33,119 +36,111 @@ impl fmt::Display for EthAmountError { /// This type provides a more typed representation of Ethereum amounts compared to raw `[u32; 8]` /// arrays, while maintaining compatibility with the existing MASM processing pipeline. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct EthAmount([u32; 8]); +pub struct EthAmount([u8; 32]); impl EthAmount { - /// Creates a new [`EthAmount`] from an array of 8 u32 values. - /// - /// The values are stored in little-endian order where `values[0]` contains - /// the least significant 32 bits. - pub const fn new(values: [u32; 8]) -> Self { - Self(values) + /// Creates an [`EthAmount`] from a 32-byte array. + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) } - /// Creates an [`EthAmount`] from a single u64 value. + /// Creates an [`EthAmount`] from a decimal (uint) string. /// - /// This is useful for smaller amounts that fit in a u64. The value is - /// stored in the first two u32 slots with the remaining slots set to zero. - pub const fn from_u64(value: u64) -> Self { - let low = value as u32; - let high = (value >> 32) as u32; - Self([low, high, 0, 0, 0, 0, 0, 0]) - } - - /// Creates an [`EthAmount`] from a single u32 value. + /// The string should contain only ASCII decimal digits (e.g. `"2000000000000000000"`). + /// The value is stored as a 32-byte big-endian array, matching the Solidity uint256 layout. /// - /// This is useful for smaller amounts that fit in a u32. The value is - /// stored in the first u32 slot with the remaining slots set to zero. - pub const fn from_u32(value: u32) -> Self { - Self([value, 0, 0, 0, 0, 0, 0, 0]) - } - - /// Returns the raw array of 8 u32 values. - pub const fn as_array(&self) -> &[u32; 8] { - &self.0 - } - - /// Converts the amount into an array of 8 u32 values. - pub const fn into_array(self) -> [u32; 8] { - self.0 + /// # Errors + /// + /// Returns [`EthAmountError`] if the string is empty, contains non-digit characters, + /// or represents a value that overflows uint256. + pub fn from_uint_str(s: &str) -> Result { + let value = U256::from_dec_str(s).map_err(|_| EthAmountError::Overflow)?; + Ok(Self(value.to_big_endian())) } - /// Returns true if the amount is zero. - pub fn is_zero(&self) -> bool { - self.0.iter().all(|&x| x == 0) + /// Converts the EthAmount to a U256 for easier arithmetic operations. + pub fn to_u256(&self) -> U256 { + U256::from_big_endian(&self.0) } - /// Attempts to convert the amount to a u64. + /// Creates an EthAmount from a U256 value. /// - /// # Errors - /// Returns [`EthAmountError::Overflow`] if the amount doesn't fit in a u64 - /// (i.e., if any of the upper 6 u32 values are non-zero). - pub fn try_to_u64(&self) -> Result { - if self.0[2..].iter().any(|&x| x != 0) { - Err(EthAmountError::Overflow) - } else { - Ok((self.0[1] as u64) << 32 | self.0[0] as u64) - } - } - - /// Attempts to convert the amount to a u32. - /// - /// # Errors - /// Returns [`EthAmountError::Overflow`] if the amount doesn't fit in a u32 - /// (i.e., if any of the upper 7 u32 values are non-zero). - pub fn try_to_u32(&self) -> Result { - if self.0[1..].iter().any(|&x| x != 0) { - Err(EthAmountError::Overflow) - } else { - Ok(self.0[0]) - } + /// This constructor is only available in test code to make test arithmetic easier. + #[cfg(any(test, feature = "testing"))] + pub fn from_u256(value: U256) -> Self { + Self(value.to_big_endian()) } /// Converts the amount to a vector of field elements for note storage. /// /// Each u32 value in the amount array is converted to a [`Felt`]. - pub fn to_elements(&self) -> [Felt; 8] { - let mut result = [Felt::ZERO; 8]; - for (i, &value) in self.0.iter().enumerate() { - result[i] = Felt::from(value); - } - result + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) } -} -impl From<[u32; 8]> for EthAmount { - fn from(values: [u32; 8]) -> Self { - Self(values) + /// Returns the raw 32-byte array. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 } } -impl From for [u32; 8] { - fn from(amount: EthAmount) -> Self { - amount.0 - } -} +// ================================================================================================ +// U256 SCALING DOWN HELPERS +// ================================================================================================ -impl From for EthAmount { - fn from(value: u64) -> Self { - Self::from_u64(value) - } -} +/// Maximum scaling factor for decimal conversions +const MAX_SCALING_FACTOR: u32 = 18; -impl From for EthAmount { - fn from(value: u32) -> Self { - Self::from_u32(value) +/// Calculate 10^scale where scale is a u32 exponent. +/// +/// # Errors +/// Returns [`EthAmountError::ScaleTooLarge`] if scale > 18. +fn pow10_u64(scale: u32) -> Result { + if scale > MAX_SCALING_FACTOR { + return Err(EthAmountError::ScaleTooLarge); } + Ok(10_u64.pow(scale)) } -impl fmt::Display for EthAmount { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // For display purposes, show as a hex string of the full 256-bit value - write!(f, "0x")?; - for &value in self.0.iter().rev() { - write!(f, "{:08x}", value)?; +impl EthAmount { + /// Converts a U256 amount to a Miden Felt by scaling down by 10^scale_exp. + /// + /// This is the deterministic reference implementation that computes: + /// - `y = floor(x / 10^scale_exp)` (the Miden amount as a Felt) + /// + /// # Arguments + /// * `scale_exp` - The scaling exponent (0-18) + /// + /// # Returns + /// The scaled-down Miden amount as a Felt + /// + /// # Errors + /// - [`EthAmountError::ScaleTooLarge`] if scale_exp > 18 + /// - [`EthAmountError::ScaledValueDoesNotFitU64`] if the result doesn't fit in a u64 + /// - [`EthAmountError::ScaledValueExceedsMaxFungibleAmount`] if the scaled value exceeds the + /// maximum fungible token amount + /// + /// # Example + /// ```ignore + /// let eth_amount = EthAmount::from_u64(1_000_000_000_000_000_000); // 1 ETH in wei + /// let miden_amount = eth_amount.scale_to_token_amount(12)?; + /// // Result: 1_000_000 (1e6, Miden representation) + /// ``` + pub fn scale_to_token_amount(&self, scale_exp: u32) -> Result { + let x = self.to_u256(); + let scale = U256::from(pow10_u64(scale_exp)?); + + let y_u256 = x / scale; + + // y must fit into u64; canonical Felt is guaranteed by max amount bound + let y_u64: u64 = y_u256.try_into().map_err(|_| EthAmountError::ScaledValueDoesNotFitU64)?; + + if y_u64 > FungibleAsset::MAX_AMOUNT { + return Err(EthAmountError::ScaledValueExceedsMaxFungibleAmount); } - Ok(()) + + // Safe because FungibleAsset::MAX_AMOUNT < Felt modulus + let y_felt = Felt::try_from(y_u64).expect("scaled value must fit into canonical Felt"); + Ok(y_felt) } } diff --git a/crates/miden-agglayer/src/eth_types/global_index.rs b/crates/miden-agglayer/src/eth_types/global_index.rs new file mode 100644 index 0000000000..4c4688edc0 --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/global_index.rs @@ -0,0 +1,154 @@ +use alloc::vec::Vec; + +use miden_core_lib::handlers::bytes_to_packed_u32_felts; +use miden_protocol::Felt; +use miden_protocol::utils::{HexParseError, hex_to_bytes}; + +// ================================================================================================ +// GLOBAL INDEX ERROR +// ================================================================================================ + +/// Error type for GlobalIndex validation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GlobalIndexError { + /// The leading 160 bits of the global index are not zero. + LeadingBitsNonZero, + /// The mainnet flag is not 1. + InvalidMainnetFlag, + /// The rollup index is not zero for a mainnet deposit. + RollupIndexNonZero, +} + +// ================================================================================================ +// GLOBAL INDEX +// ================================================================================================ + +/// Represents an AggLayer global index as a 256-bit value (32 bytes). +/// +/// The global index is a uint256 that encodes (from MSB to LSB): +/// - Top 160 bits (limbs 0-4): must be zero +/// - 32 bits (limb 5): mainnet flag (value = 1 for mainnet, 0 for rollup) +/// - 32 bits (limb 6): rollup index (must be 0 for mainnet deposits) +/// - 32 bits (limb 7): leaf index (deposit index in the local exit tree) +/// +/// Bytes are stored in big-endian order, matching Solidity's uint256 representation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GlobalIndex([u8; 32]); + +impl GlobalIndex { + /// Creates a [`GlobalIndex`] from a hex string (with or without "0x" prefix). + /// + /// The hex string should represent a Solidity uint256 in big-endian format + /// (64 hex characters for 32 bytes). + pub fn from_hex(hex_str: &str) -> Result { + let bytes: [u8; 32] = hex_to_bytes(hex_str)?; + Ok(Self(bytes)) + } + + /// Creates a new [`GlobalIndex`] from a 32-byte array (big-endian). + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Validates that this is a valid mainnet deposit global index. + /// + /// Checks that: + /// - The top 160 bits (limbs 0-4, bytes 0-19) are zero + /// - The mainnet flag (limb 5, bytes 20-23) is exactly 1 + /// - The rollup index (limb 6, bytes 24-27) is 0 + pub fn validate_mainnet(&self) -> Result<(), GlobalIndexError> { + // Check limbs 0-4 are zero (bytes 0-19) + if self.0[0..20].iter().any(|&b| b != 0) { + return Err(GlobalIndexError::LeadingBitsNonZero); + } + + // Check mainnet flag limb (bytes 20-23) is exactly 1 + if !self.is_mainnet() { + return Err(GlobalIndexError::InvalidMainnetFlag); + } + + // Check rollup index is zero (bytes 24-27) + if u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) != 0 { + return Err(GlobalIndexError::RollupIndexNonZero); + } + + Ok(()) + } + + /// Returns the leaf index (limb 7, lowest 32 bits). + pub fn leaf_index(&self) -> u32 { + u32::from_be_bytes([self.0[28], self.0[29], self.0[30], self.0[31]]) + } + + /// Returns the rollup index (limb 6). + pub fn rollup_index(&self) -> u32 { + u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) + } + + /// Returns true if this is a mainnet deposit (mainnet flag = 1). + pub fn is_mainnet(&self) -> bool { + u32::from_be_bytes([self.0[20], self.0[21], self.0[22], self.0[23]]) == 1 + } + + /// Converts to field elements for note storage / MASM processing. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) + } + + /// Returns the raw 32-byte array (big-endian). + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use miden_core::FieldElement; + + use super::*; + + #[test] + fn test_mainnet_global_indices_from_production() { + // Real mainnet global indices from production + // Format: (1 << 64) + leaf_index for mainnet deposits + // 18446744073709786619 = 0x1_0000_0000_0003_95FB (leaf_index = 235003) + // 18446744073709786590 = 0x1_0000_0000_0003_95DE (leaf_index = 234974) + let test_cases = [ + ("0x00000000000000000000000000000000000000000000000100000000000395fb", 235003u32), + ("0x00000000000000000000000000000000000000000000000100000000000395de", 234974u32), + ]; + + for (hex, expected_leaf_index) in test_cases { + let gi = GlobalIndex::from_hex(hex).expect("valid hex"); + + // Validate as mainnet + assert!(gi.validate_mainnet().is_ok(), "should be valid mainnet global index"); + + // Construction sanity checks + assert!(gi.is_mainnet()); + assert_eq!(gi.rollup_index(), 0); + assert_eq!(gi.leaf_index(), expected_leaf_index); + + // Verify to_elements produces correct LE-packed u32 felts + // -------------------------------------------------------------------------------- + + let elements = gi.to_elements(); + assert_eq!(elements.len(), 8); + + // leading zeros + assert_eq!(elements[0..5], [Felt::ZERO; 5]); + + // mainnet flag: BE value 1 → LE-packed as 0x01000000 + assert_eq!(elements[5], Felt::new(u32::from_le_bytes(1u32.to_be_bytes()) as u64)); + + // rollup index + assert_eq!(elements[6], Felt::ZERO); + + // leaf index: BE value → LE-packed + assert_eq!( + elements[7], + Felt::new(u32::from_le_bytes(expected_leaf_index.to_be_bytes()) as u64) + ); + } + } +} diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs new file mode 100644 index 0000000000..287f66e71a --- /dev/null +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -0,0 +1,34 @@ +use alloc::vec::Vec; + +use miden_core_lib::handlers::bytes_to_packed_u32_felts; +use miden_protocol::Felt; + +// ================================================================================================ +// METADATA HASH +// ================================================================================================ + +/// Represents a Keccak256 metadata hash as 32 bytes. +/// +/// This type provides a typed representation of metadata hashes for the agglayer bridge, +/// while maintaining compatibility with the existing MASM processing pipeline. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MetadataHash([u8; 32]); + +impl MetadataHash { + /// Creates a new [`MetadataHash`] from a 32-byte array. + pub const fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the raw 32-byte array. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Converts the metadata hash to 8 Felt elements for MASM processing. + /// + /// Each 4-byte chunk is converted to a u32 using little-endian byte order. + pub fn to_elements(&self) -> Vec { + bytes_to_packed_u32_felts(&self.0) + } +} diff --git a/crates/miden-agglayer/src/eth_types/mod.rs b/crates/miden-agglayer/src/eth_types/mod.rs index c8184cbc8d..3bee167e5d 100644 --- a/crates/miden-agglayer/src/eth_types/mod.rs +++ b/crates/miden-agglayer/src/eth_types/mod.rs @@ -1,5 +1,9 @@ pub mod address; pub mod amount; +pub mod global_index; +pub mod metadata_hash; pub use address::EthAddressFormat; pub use amount::{EthAmount, EthAmountError}; +pub use global_index::{GlobalIndex, GlobalIndexError}; +pub use metadata_hash::MetadataHash; diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index e4e0eae9c9..5db0c68195 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -27,6 +27,7 @@ use miden_utils_sync::LazyLock; pub mod b2agg_note; pub mod claim_note; +pub mod config_note; pub mod errors; pub mod eth_types; pub mod update_ger_note; @@ -42,7 +43,15 @@ pub use claim_note::{ SmtNode, create_claim_note, }; -pub use eth_types::{EthAddressFormat, EthAmount, EthAmountError}; +pub use config_note::ConfigAggBridgeNote; +pub use eth_types::{ + EthAddressFormat, + EthAmount, + EthAmountError, + GlobalIndex, + GlobalIndexError, + MetadataHash, +}; pub use update_ger_note::UpdateGerNote; // AGGLAYER NOTE SCRIPTS @@ -195,6 +204,49 @@ pub fn asset_conversion_component(storage_slots: Vec) -> AccountCom ) } +// FAUCET CONVERSION STORAGE HELPERS +// ================================================================================================ + +/// Builds the two storage slot values for faucet conversion metadata. +/// +/// The conversion metadata is stored in two value storage slots: +/// - Slot 1 (`miden::agglayer::faucet::conversion_info_1`): `[addr0, addr1, addr2, addr3]` — first +/// 4 felts of the origin token address (5 × u32 limbs). +/// - Slot 2 (`miden::agglayer::faucet::conversion_info_2`): `[addr4, origin_network, scale, 0]` — +/// remaining address felt + origin network + scale factor. +/// +/// # Parameters +/// - `origin_token_address`: The EVM token address in Ethereum format +/// - `origin_network`: The origin network/chain ID +/// - `scale`: The decimal scaling factor (exponent for 10^scale) +/// +/// # Returns +/// A tuple of two `Word` values representing the two storage slot contents. +pub fn agglayer_faucet_conversion_slots( + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, +) -> (Word, Word) { + let addr_elements = origin_token_address.to_elements(); + + let slot1 = Word::new([addr_elements[0], addr_elements[1], addr_elements[2], addr_elements[3]]); + + let slot2 = + Word::new([addr_elements[4], Felt::from(origin_network), Felt::from(scale), Felt::ZERO]); + + (slot1, slot2) +} + +// FAUCET REGISTRY HELPERS +// ================================================================================================ + +/// Creates a faucet registry map key from a faucet account ID. +/// +/// The key format is `[faucet_id_prefix, faucet_id_suffix, 0, 0]`. +pub fn faucet_registry_key(faucet_id: AccountId) -> Word { + Word::new([Felt::ZERO, Felt::ZERO, faucet_id.suffix(), faucet_id.prefix().as_felt()]) +} + // AGGLAYER ACCOUNT CREATION HELPERS // ================================================================================================ @@ -215,14 +267,20 @@ pub fn create_bridge_account_component() -> AccountComponent { /// Creates an agglayer faucet account component with the specified configuration. /// /// This function creates all the necessary storage slots for an agglayer faucet: -/// - Network faucet metadata slot (max_supply, decimals, token_symbol) +/// - Network faucet metadata slot (token_supply, max_supply, decimals, token_symbol) /// - Bridge account reference slot for FPI validation +/// - Conversion info slot 1: first 4 felts of origin token address +/// - Conversion info slot 2: 5th address felt + origin network + scale /// /// # Parameters /// - `token_symbol`: The symbol for the fungible token (e.g., "AGG") /// - `decimals`: Number of decimal places for the token /// - `max_supply`: Maximum supply of the token +/// - `token_supply`: Initial outstanding token supply (0 for new faucets) /// - `bridge_account_id`: The account ID of the bridge account for validation +/// - `origin_token_address`: The EVM origin token address +/// - `origin_network`: The origin network/chain ID +/// - `scale`: The decimal scaling factor (exponent for 10^scale) /// /// # Returns /// Returns an [`AccountComponent`] configured for agglayer faucet operations. @@ -233,12 +291,16 @@ pub fn create_agglayer_faucet_component( token_symbol: &str, decimals: u8, max_supply: Felt, + token_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> AccountComponent { - // Create network faucet metadata slot: [0, max_supply, decimals, token_symbol] + // Create network faucet metadata slot: [token_supply, max_supply, decimals, token_symbol] let token_symbol = TokenSymbol::new(token_symbol).expect("Token symbol should be valid"); let metadata_word = - Word::new([FieldElement::ZERO, max_supply, Felt::from(decimals), token_symbol.into()]); + Word::new([token_supply, max_supply, Felt::from(decimals), token_symbol.into()]); let metadata_slot = StorageSlot::with_value(NetworkFungibleFaucet::metadata_slot().clone(), metadata_word); @@ -253,31 +315,62 @@ pub fn create_agglayer_faucet_component( .expect("Agglayer faucet storage slot name should be valid"); let bridge_slot = StorageSlot::with_value(agglayer_storage_slot_name, bridge_account_id_word); + // Create conversion metadata storage slots + let (conversion_slot1_word, conversion_slot2_word) = + agglayer_faucet_conversion_slots(origin_token_address, origin_network, scale); + + let conversion_info_1_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_1") + .expect("Conversion info 1 storage slot name should be valid"); + let conversion_slot1 = StorageSlot::with_value(conversion_info_1_name, conversion_slot1_word); + + let conversion_info_2_name = StorageSlotName::new("miden::agglayer::faucet::conversion_info_2") + .expect("Conversion info 2 storage slot name should be valid"); + let conversion_slot2 = StorageSlot::with_value(conversion_info_2_name, conversion_slot2_word); + // Combine all storage slots for the agglayer faucet component - let agglayer_storage_slots = vec![metadata_slot, bridge_slot]; + let agglayer_storage_slots = + vec![metadata_slot, bridge_slot, conversion_slot1, conversion_slot2]; agglayer_faucet_component(agglayer_storage_slots) } /// Creates a complete bridge account builder with the standard configuration. +/// +/// The bridge starts with an empty faucet registry. Faucets are registered at runtime +/// via CONFIG_AGG_BRIDGE notes that call `bridge_config::register_faucet`. pub fn create_bridge_account_builder(seed: Word) -> AccountBuilder { - // Create the "bridge_in" component - let ger_upper_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_upper") + let ger_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger") .expect("Bridge storage slot name should be valid"); - let ger_lower_storage_slot_name = StorageSlotName::new("miden::agglayer::bridge::ger_lower") - .expect("Bridge storage slot name should be valid"); - let bridge_in_storage_slots = vec![ - StorageSlot::with_value(ger_upper_storage_slot_name, Word::empty()), - StorageSlot::with_value(ger_lower_storage_slot_name, Word::empty()), - ]; + let bridge_in_storage_slots = vec![StorageSlot::with_empty_map(ger_storage_slot_name)]; let bridge_in_component = bridge_in_component(bridge_in_storage_slots); - // Create the "bridge_out" component - let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let").unwrap(); - let bridge_out_storage_slots = vec![StorageSlot::with_empty_map(let_storage_slot_name)]; + // Create the "bridge_out" component. + // - The map slot stores the frontier as a double-word array (absent keys return zeros, which is + // the correct initial state for a frontier with no leaves). + // - The root and num_leaves each get their own value slots. + let let_storage_slot_name = StorageSlotName::new("miden::agglayer::let") + .expect("LET storage slot name should be valid"); + let let_root_lo_slot_name = StorageSlotName::new("miden::agglayer::let::root_lo") + .expect("LET root_lo storage slot name should be valid"); + let let_root_hi_slot_name = StorageSlotName::new("miden::agglayer::let::root_hi") + .expect("LET root_hi storage slot name should be valid"); + let let_num_leaves_slot_name = StorageSlotName::new("miden::agglayer::let::num_leaves") + .expect("LET num_leaves storage slot name should be valid"); + let faucet_registry_slot_name = + StorageSlotName::new("miden::agglayer::bridge::faucet_registry") + .expect("Faucet registry storage slot name should be valid"); + let bridge_out_storage_slots = vec![ + StorageSlot::with_empty_map(let_storage_slot_name), + StorageSlot::with_value(let_root_lo_slot_name, Word::empty()), + StorageSlot::with_value(let_root_hi_slot_name, Word::empty()), + StorageSlot::with_value(let_num_leaves_slot_name, Word::empty()), + StorageSlot::with_empty_map(faucet_registry_slot_name), + ]; let bridge_out_component = bridge_out_component(bridge_out_storage_slots); - // Combine the components into a single account(builder) + // Combine the components into a single account(builder). + // Note: bridge_config::register_faucet is also exposed via the agglayer library + // included in bridge_out_component, using the faucet_registry storage slot above. Account::builder(seed.into()) .storage_mode(AccountStorageMode::Network) .with_component(bridge_out_component) @@ -306,15 +399,28 @@ pub fn create_existing_bridge_account(seed: Word) -> Account { } /// Creates a complete agglayer faucet account builder with the specified configuration. +#[allow(clippy::too_many_arguments)] pub fn create_agglayer_faucet_builder( seed: Word, token_symbol: &str, decimals: u8, max_supply: Felt, + token_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> AccountBuilder { - let agglayer_component = - create_agglayer_faucet_component(token_symbol, decimals, max_supply, bridge_account_id); + let agglayer_component = create_agglayer_faucet_component( + token_symbol, + decimals, + max_supply, + token_supply, + bridge_account_id, + origin_token_address, + origin_network, + scale, + ); Account::builder(seed.into()) .account_type(AccountType::FungibleFaucet) @@ -331,26 +437,54 @@ pub fn create_agglayer_faucet( decimals: u8, max_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> Account { - create_agglayer_faucet_builder(seed, token_symbol, decimals, max_supply, bridge_account_id) - .with_auth_component(AccountComponent::from(NoAuth)) - .build() - .expect("Agglayer faucet account should be valid") + create_agglayer_faucet_builder( + seed, + token_symbol, + decimals, + max_supply, + Felt::ZERO, + bridge_account_id, + origin_token_address, + origin_network, + scale, + ) + .with_auth_component(AccountComponent::from(NoAuth)) + .build() + .expect("Agglayer faucet account should be valid") } /// Creates an existing agglayer faucet account with the specified configuration. /// /// This creates an existing account suitable for testing scenarios. #[cfg(any(feature = "testing", test))] +#[allow(clippy::too_many_arguments)] pub fn create_existing_agglayer_faucet( seed: Word, token_symbol: &str, decimals: u8, max_supply: Felt, + token_supply: Felt, bridge_account_id: AccountId, + origin_token_address: &EthAddressFormat, + origin_network: u32, + scale: u8, ) -> Account { - create_agglayer_faucet_builder(seed, token_symbol, decimals, max_supply, bridge_account_id) - .with_auth_component(AccountComponent::from(NoAuth)) - .build_existing() - .expect("Agglayer faucet account should be valid") + create_agglayer_faucet_builder( + seed, + token_symbol, + decimals, + max_supply, + token_supply, + bridge_account_id, + origin_token_address, + origin_network, + scale, + ) + .with_auth_component(AccountComponent::from(NoAuth)) + .build_existing() + .expect("Agglayer faucet account should be valid") } diff --git a/crates/miden-agglayer/src/utils.rs b/crates/miden-agglayer/src/utils.rs index 88850de58c..8b5e8d3820 100644 --- a/crates/miden-agglayer/src/utils.rs +++ b/crates/miden-agglayer/src/utils.rs @@ -1,28 +1,18 @@ -use miden_core::FieldElement; +use alloc::vec::Vec; + use miden_protocol::Felt; // UTILITY FUNCTIONS // ================================================================================================ -/// Converts a bytes32 value (32 bytes) into an array of 8 Felt values. -/// -/// Note: These utility functions will eventually be replaced with similar functions from miden-vm. -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::from(value); - } - result -} - -/// 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() { +/// Converts Felt u32 limbs to bytes using little-endian byte order. +/// TODO remove once we move to v0.21.0 which has `packed_u32_elements_to_bytes` +pub fn felts_to_bytes(limbs: &[Felt]) -> Vec { + let mut bytes = Vec::with_capacity(limbs.len() * 4); + for limb in limbs.iter() { 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.extend_from_slice(&limb_bytes); } bytes } diff --git a/crates/miden-testing/tests/agglayer/asset_conversion.rs b/crates/miden-testing/tests/agglayer/asset_conversion.rs index c37b1c206b..7a7565c743 100644 --- a/crates/miden-testing/tests/agglayer/asset_conversion.rs +++ b/crates/miden-testing/tests/agglayer/asset_conversion.rs @@ -1,40 +1,32 @@ extern crate alloc; -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; +use miden_agglayer::errors::{ + ERR_REMAINDER_TOO_LARGE, + ERR_SCALE_AMOUNT_EXCEEDED_LIMIT, + ERR_UNDERFLOW, + ERR_X_TOO_LARGE, +}; +use miden_agglayer::eth_types::amount::EthAmount; +use miden_agglayer::utils; use miden_protocol::Felt; +use miden_protocol::asset::FungibleAsset; +use miden_protocol::errors::MasmError; use primitive_types::U256; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; -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"); - let array: [Felt; 8] = - [felts[0], felts[1], felts[2], felts[3], felts[4], felts[5], felts[6], felts[7]]; - let bytes = utils::felts_to_u256_bytes(array); - U256::from_little_endian(&bytes) -} +use super::test_utils::{assert_execution_fails_with, execute_masm_script}; -/// Convert the top 8 u32 values from the execution stack to a U256 -fn stack_to_u256(exec_output: &ExecutionOutput) -> U256 { - let felts: Vec = exec_output.stack[0..8].to_vec(); - felts_to_u256(felts) -} +// ================================================================================================ +// SCALE UP TESTS (Felt -> U256) +// ================================================================================================ -/// Helper function to test convert_felt_to_u256_scaled with given parameters -async fn test_convert_to_u256_helper( +/// Helper function to test scale_native_amount_to_u256 with given parameters +async fn test_scale_up_helper( miden_amount: Felt, scale_exponent: Felt, - expected_result_array: [u32; 8], - expected_result_u256: U256, + expected_result: EthAmount, ) -> anyhow::Result<()> { - let asset_conversion_lib = agglayer_library(); - let script_code = format!( " use miden::core::sys @@ -49,53 +41,35 @@ async fn test_convert_to_u256_helper( scale_exponent, miden_amount, ); - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(asset_conversion_lib.clone()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); - - 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] = [ - exec_output.stack[0].as_int() as u32, - exec_output.stack[1].as_int() as u32, - exec_output.stack[2].as_int() as u32, - exec_output.stack[3].as_int() as u32, - exec_output.stack[4].as_int() as u32, - exec_output.stack[5].as_int() as u32, - exec_output.stack[6].as_int() as u32, - exec_output.stack[7].as_int() as u32, - ]; + let exec_output = execute_masm_script(&script_code).await?; + let actual_felts: Vec = exec_output.stack[0..8].to_vec(); - let actual_result_u256 = stack_to_u256(&exec_output); + // to_elements() returns big-endian limb order with each limb byte-swapped (LE-interpreted + // from BE source bytes). The scale-up output is native u32 limbs in LE limb order, so we + // reverse the limbs and swap bytes within each u32 to match. + let expected_felts: Vec = expected_result + .to_elements() + .into_iter() + .rev() + .map(|f| Felt::new((f.as_int() as u32).swap_bytes() as u64)) + .collect(); - assert_eq!(actual_result, expected_result_array); - assert_eq!(actual_result_u256, expected_result_u256); + assert_eq!(actual_felts, expected_felts); Ok(()) } #[tokio::test] -async fn test_convert_to_u256_basic_examples() -> anyhow::Result<()> { +async fn test_scale_up_basic_examples() -> anyhow::Result<()> { // Test case 1: amount=1, no scaling (scale_exponent=0) - test_convert_to_u256_helper( - Felt::new(1), - Felt::new(0), - [1, 0, 0, 0, 0, 0, 0, 0], - U256::from(1u64), - ) - .await?; + test_scale_up_helper(Felt::new(1), Felt::new(0), EthAmount::from_uint_str("1").unwrap()) + .await?; // Test case 2: amount=1, scale to 1e18 (scale_exponent=18) - test_convert_to_u256_helper( + test_scale_up_helper( Felt::new(1), Felt::new(18), - [2808348672, 232830643, 0, 0, 0, 0, 0, 0], - U256::from_dec_str("1000000000000000000").unwrap(), + EthAmount::from_uint_str("1000000000000000000").unwrap(), ) .await?; @@ -103,86 +77,280 @@ async fn test_convert_to_u256_basic_examples() -> anyhow::Result<()> { } #[tokio::test] -async fn test_convert_to_u256_scaled_eth() -> anyhow::Result<()> { - // 100 units base 1e6 - let miden_amount = Felt::new(100_000_000); +async fn test_scale_up_realistic_amounts() -> anyhow::Result<()> { + // 100 units base 1e6, scale to 1e18 + test_scale_up_helper( + Felt::new(100_000_000), + Felt::new(12), + EthAmount::from_uint_str("100000000000000000000").unwrap(), + ) + .await?; - // scale to 1e18 - let target_scale = Felt::new(12); + // Large amount: 1e18 units scaled by 8 + test_scale_up_helper( + Felt::new(1000000000000000000), + Felt::new(8), + EthAmount::from_uint_str("100000000000000000000000000").unwrap(), + ) + .await?; - let asset_conversion_lib = agglayer_library(); + Ok(()) +} - let script_code = format!( - " +#[tokio::test] +async fn test_scale_up_exceeds_max_scale() { + // scale_exp = 19 should fail + let script_code = " use miden::core::sys use miden::agglayer::asset_conversion begin - push.{}.{} + push.19.1 exec.asset_conversion::scale_native_amount_to_u256 exec.sys::truncate_stack end - ", - target_scale, miden_amount, - ); + "; - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(asset_conversion_lib.clone()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); + assert_execution_fails_with(script_code, "maximum scaling factor is 18").await; +} - let exec_output = execute_program_with_default_host(program, None).await?; +// ================================================================================================ +// SCALE DOWN TESTS (U256 -> Felt) +// ================================================================================================ - let expected_result = U256::from_dec_str("100000000000000000000").unwrap(); - let actual_result = stack_to_u256(&exec_output); +/// Build MASM script for verify_u256_to_native_amount_conversion +fn build_scale_down_script(x: EthAmount, scale_exp: u32, y: u64) -> String { + let x_felts = x.to_elements(); + format!( + r#" + use miden::core::sys + use miden::agglayer::asset_conversion + + begin + push.{}.{}.{}.{}.{}.{}.{}.{}.{}.{} + exec.asset_conversion::verify_u256_to_native_amount_conversion + exec.sys::truncate_stack + end + "#, + y, + scale_exp, + x_felts[7].as_int(), + x_felts[6].as_int(), + x_felts[5].as_int(), + x_felts[4].as_int(), + x_felts[3].as_int(), + x_felts[2].as_int(), + x_felts[1].as_int(), + x_felts[0].as_int(), + ) +} - assert_eq!(actual_result, expected_result); +/// Assert that scaling down succeeds with the correct result +async fn assert_scale_down_ok(x: EthAmount, scale: u32) -> anyhow::Result { + let y = x.scale_to_token_amount(scale).unwrap().as_int(); + let script = build_scale_down_script(x, scale, y); + let out = execute_masm_script(&script).await?; + assert_eq!(out.stack[0].as_int(), y); + Ok(y) +} +/// Assert that scaling down fails with the given y and expected error +async fn assert_scale_down_fails(x: EthAmount, scale: u32, y: u64, expected_error: MasmError) { + let script = build_scale_down_script(x, scale, y); + assert_execution_fails_with(&script, expected_error.message()).await; +} + +/// Test that y-1 and y+1 both fail appropriately +async fn assert_y_plus_minus_one_behavior(x: EthAmount, scale: u32) -> anyhow::Result<()> { + let y = assert_scale_down_ok(x, scale).await?; + if y > 0 { + assert_scale_down_fails(x, scale, y - 1, ERR_REMAINDER_TOO_LARGE).await; + } + assert_scale_down_fails(x, scale, y + 1, ERR_UNDERFLOW).await; + Ok(()) +} + +#[tokio::test] +async fn test_scale_down_basic_examples() -> anyhow::Result<()> { + let cases = [ + (EthAmount::from_uint_str("1000000000000000000").unwrap(), 10u32), + (EthAmount::from_uint_str("1000").unwrap(), 0u32), + (EthAmount::from_uint_str("10000000000000000000").unwrap(), 18u32), + ]; + + for (x, s) in cases { + assert_scale_down_ok(x, s).await?; + } Ok(()) } +// ================================================================================================ +// FUZZING TESTS +// ================================================================================================ + +// Fuzz test that validates verify_u256_to_native_amount_conversion (U256 → Felt) +// with random realistic amounts for all scale exponents (0..=18). #[tokio::test] -async fn test_convert_to_u256_scaled_large_amount() -> anyhow::Result<()> { - // 100,000,000 units (base 1e10) - let miden_amount = Felt::new(1000000000000000000); +async fn test_scale_down_realistic_scenarios_fuzzing() -> anyhow::Result<()> { + const CASES_PER_SCALE: usize = 2; + const MAX_SCALE: u32 = 18; + + let mut rng = StdRng::seed_from_u64(42); - // scale to base 1e18 - let scale_exponent = Felt::new(8); + let min_x = U256::from(10_000_000_000_000u64); // 1e13 + let desired_max_x = U256::from_dec_str("1000000000000000000000000").unwrap(); // 1e24 + let max_y = U256::from(FungibleAsset::MAX_AMOUNT); // 2^63 - 2^31 - let asset_conversion_lib = agglayer_library(); + for scale in 0..=MAX_SCALE { + let scale_factor = U256::from(10u64).pow(U256::from(scale)); + // Ensure x always scales down into a y that fits the fungible-token bound. + let max_x = desired_max_x.min(max_y * scale_factor); + + assert!(max_x > min_x, "max_x must exceed min_x for scale={scale}"); + + // Sample x uniformly from [min_x, max_x). + let span: u128 = (max_x - min_x).try_into().expect("span fits in u128"); + + for _ in 0..CASES_PER_SCALE { + let offset: u128 = rng.random_range(0..span); + let x = EthAmount::from_u256(min_x + U256::from(offset)); + assert_scale_down_ok(x, scale).await?; + } + } + + Ok(()) +} + +// ================================================================================================ +// NEGATIVE TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_scale_down_wrong_y_clean_case() -> anyhow::Result<()> { + let x = EthAmount::from_uint_str("10000000000000000000").unwrap(); + assert_y_plus_minus_one_behavior(x, 18).await +} + +#[tokio::test] +async fn test_scale_down_wrong_y_with_remainder() -> anyhow::Result<()> { + let x = EthAmount::from_uint_str("1500000000000000000").unwrap(); + assert_y_plus_minus_one_behavior(x, 18).await +} + +// ================================================================================================ +// NEGATIVE TESTS - BOUNDS +// ================================================================================================ + +#[tokio::test] +async fn test_scale_down_exceeds_max_scale() { + let x = EthAmount::from_uint_str("1000").unwrap(); + let s = 19u32; + let y = 1u64; + assert_scale_down_fails(x, s, y, ERR_SCALE_AMOUNT_EXCEEDED_LIMIT).await; +} + +#[tokio::test] +async fn test_scale_down_x_too_large() { + // Construct x with upper limbs non-zero (>= 2^128) + let x = EthAmount::from_u256(U256::from(1u64) << 128); + let s = 0u32; + let y = 0u64; + assert_scale_down_fails(x, s, y, ERR_X_TOO_LARGE).await; +} + +// ================================================================================================ +// REMAINDER EDGE TEST +// ================================================================================================ + +#[tokio::test] +async fn test_scale_down_remainder_edge() -> anyhow::Result<()> { + // Force z = scale - 1: pick y=5, s=10, so scale=10^10 + // Set x = y*scale + (scale-1) = 5*10^10 + (10^10 - 1) = 59999999999 + let scale_exp = 10u32; + let scale = 10u64.pow(scale_exp); + let x_val = 5u64 * scale + (scale - 1); + let x = EthAmount::from_u256(U256::from(x_val)); + + assert_scale_down_ok(x, scale_exp).await?; + Ok(()) +} + +#[tokio::test] +async fn test_scale_down_remainder_exactly_scale_fails() { + // If remainder z = scale, it should fail + // Pick s=10, x = 6*scale (where scale = 10^10) + // The correct y should be 6, so providing y=5 should fail + let scale_exp = 10u32; + let scale = 10u64.pow(scale_exp); + let x = EthAmount::from_u256(U256::from(6u64 * scale)); + + // Calculate the correct y using scale_to_token_amount + let correct_y = x.scale_to_token_amount(scale_exp).unwrap().as_int(); + assert_eq!(correct_y, 6); + + // Providing wrong_y = correct_y - 1 should fail with ERR_REMAINDER_TOO_LARGE + let wrong_y = correct_y - 1; + assert_scale_down_fails(x, scale_exp, wrong_y, ERR_REMAINDER_TOO_LARGE).await; +} + +// ================================================================================================ +// INLINE SCALE DOWN TEST +// ================================================================================================ + +#[tokio::test] +async fn test_verify_scale_down_inline() -> anyhow::Result<()> { + // Test: Take 100 * 1e18 and scale to base 1e8 + // This means we divide by 1e10 (scale_exp = 10) + // x = 100 * 1e18 = 100000000000000000000 + // y = x / 1e10 = 10000000000 (100 * 1e8) + let x = EthAmount::from_uint_str("100000000000000000000").unwrap(); + let scale_exp = 10u32; + let y = x.scale_to_token_amount(scale_exp).unwrap().as_int(); + + let x_felts = x.to_elements(); + + // Build the MASM script inline let script_code = format!( - " + r#" use miden::core::sys use miden::agglayer::asset_conversion - + begin - push.{}.{} - - exec.asset_conversion::scale_native_amount_to_u256 + # Push y (expected quotient) + push.{} + + # Push scale_exp + push.{} + + # Push x as 8 u32 limbs (little-endian, x0 at top) + push.{}.{}.{}.{}.{}.{}.{}.{} + + # Call the scale down procedure + exec.asset_conversion::verify_u256_to_native_amount_conversion + + # Truncate stack to just return y exec.sys::truncate_stack end - ", - scale_exponent, miden_amount, + "#, + y, + scale_exp, + x_felts[7].as_int(), + x_felts[6].as_int(), + x_felts[5].as_int(), + x_felts[4].as_int(), + x_felts[3].as_int(), + x_felts[2].as_int(), + x_felts[1].as_int(), + x_felts[0].as_int(), ); - let program = Assembler::new(Arc::new(DefaultSourceManager::default())) - .with_dynamic_library(CoreLibrary::default()) - .unwrap() - .with_dynamic_library(asset_conversion_lib.clone()) - .unwrap() - .assemble_program(&script_code) - .unwrap(); - - 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); + // Execute the script + let exec_output = execute_masm_script(&script_code).await?; - assert_eq!(actual_result, expected_result); + // Verify the result + let result = exec_output.stack[0].as_int(); + assert_eq!(result, y); Ok(()) } @@ -199,7 +367,7 @@ fn test_felts_to_u256_bytes_sequential_values() { Felt::new(7), Felt::new(8), ]; - let result = utils::felts_to_u256_bytes(limbs); + let result = utils::felts_to_bytes(&limbs); assert_eq!(result.len(), 32); // Verify the byte layout: limbs are processed in little-endian order, each as little-endian u32 @@ -214,13 +382,13 @@ fn test_felts_to_u256_bytes_sequential_values() { fn test_felts_to_u256_bytes_edge_cases() { // Test case 1: All zeros (minimum) let limbs = [Felt::new(0); 8]; - let result = utils::felts_to_u256_bytes(limbs); + let result = utils::felts_to_bytes(&limbs); assert_eq!(result.len(), 32); assert!(result.iter().all(|&b| b == 0)); // Test case 2: All max u32 values (maximum) let limbs = [Felt::new(u32::MAX as u64); 8]; - let result = utils::felts_to_u256_bytes(limbs); + let result = utils::felts_to_bytes(&limbs); assert_eq!(result.len(), 32); assert!(result.iter().all(|&b| b == 255)); } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 173e6a8d57..9c29812ab2 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -1,41 +1,35 @@ extern crate alloc; -use core::slice; - -use miden_agglayer::claim_note::{ExitRoot, SmtNode}; use miden_agglayer::{ ClaimNoteStorage, EthAddressFormat, - EthAmount, - LeafData, OutputNoteData, - ProofData, + UpdateGerNote, create_claim_note, create_existing_agglayer_faucet, create_existing_bridge_account, }; -use miden_protocol::Felt; use miden_protocol::account::Account; -use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::asset::FungibleAsset; use miden_protocol::crypto::rand::FeltRng; -use miden_protocol::note::{ - Note, - NoteAssets, - NoteMetadata, - NoteRecipient, - NoteStorage, - NoteTag, - NoteType, -}; +use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, FieldElement}; use miden_standards::account::wallets::BasicWallet; -use miden_standards::note::StandardNote; use miden_testing::{AccountState, Auth, MockChain}; use rand::Rng; -use super::test_utils::claim_note_test_inputs; - -/// Tests the bridge-in flow: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID note created. +use super::test_utils::real_claim_data; + +/// Tests the bridge-in flow using real claim data: CLAIM note -> Aggfaucet (FPI to Bridge) -> P2ID +/// note created. +/// +/// This test uses real ProofData and LeafData deserialized from claim_asset_vectors.json. +/// The claim note is processed against the agglayer faucet, which validates the Merkle proof +/// and creates a P2ID note for the destination address. +/// +/// Note: Modifying anything in the test vectors would invalidate the Merkle proof, +/// as the proof was computed for the original leaf_data including the original destination. #[tokio::test] async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -50,122 +44,87 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { // -------------------------------------------------------------------------------------------- let token_symbol = "AGG"; let decimals = 8u8; - let max_supply = Felt::new(1000000); + let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); let agglayer_faucet_seed = builder.rng_mut().draw_word(); + // Origin token address for the faucet's conversion metadata + let origin_token_address = EthAddressFormat::new([0u8; 20]); + let origin_network = 0u32; + let scale = 0u8; + let agglayer_faucet = create_existing_agglayer_faucet( agglayer_faucet_seed, token_symbol, decimals, max_supply, + Felt::ZERO, bridge_account.id(), + &origin_token_address, + origin_network, + scale, ); builder.add_account(agglayer_faucet.clone())?; - // CREATE USER ACCOUNT TO RECEIVE P2ID NOTE + // GET REAL CLAIM DATA FROM JSON // -------------------------------------------------------------------------------------------- - let user_account_builder = + let (proof_data, leaf_data, ger) = real_claim_data(); + + // Get the destination account ID from the leaf data + // This requires the destination_address to be in the embedded Miden AccountId format + // (first 4 bytes must be zero). + let destination_account_id = leaf_data + .destination_address + .to_account_id() + .expect("destination address is not an embedded Miden AccountId"); + + // CREATE SENDER ACCOUNT (for creating the claim note) + // -------------------------------------------------------------------------------------------- + let sender_account_builder = Account::builder(builder.rng_mut().random()).with_component(BasicWallet); - let user_account = builder.add_account_from_builder( + let sender_account = builder.add_account_from_builder( Auth::IncrNonce, - user_account_builder, + sender_account_builder, AccountState::Exists, )?; - // CREATE CLAIM NOTE WITH P2ID OUTPUT NOTE DETAILS + // CREATE CLAIM NOTE WITH REAL PROOF DATA AND LEAF DATA // -------------------------------------------------------------------------------------------- - // Define amount values for the test - let claim_amount = 100u32; - - // Create CLAIM note using the new test inputs function - let ( - smt_proof_local_exit_root, - smt_proof_rollup_exit_root, - global_index, - mainnet_exit_root, - rollup_exit_root, - origin_network, - origin_token_address, - destination_network, - metadata, - ) = claim_note_test_inputs(); - - // Convert AccountId to destination address bytes in the test - let destination_address = EthAddressFormat::from_account_id(user_account.id()).into_bytes(); - // Generate a serial number for the P2ID note let serial_num = builder.rng_mut().draw_word(); - // Convert amount to EthAmount for the LeafData - let amount_eth = EthAmount::from_u32(claim_amount); - - // Convert Vec<[u8; 32]> to [SmtNode; 32] for SMT proofs - let local_proof_array: [SmtNode; 32] = smt_proof_local_exit_root[0..32] - .iter() - .map(|&bytes| SmtNode::from(bytes)) - .collect::>() - .try_into() - .expect("should have exactly 32 elements"); - - let rollup_proof_array: [SmtNode; 32] = smt_proof_rollup_exit_root[0..32] - .iter() - .map(|&bytes| SmtNode::from(bytes)) - .collect::>() - .try_into() - .expect("should have exactly 32 elements"); - - let proof_data = ProofData { - smt_proof_local_exit_root: local_proof_array, - smt_proof_rollup_exit_root: rollup_proof_array, - global_index, - mainnet_exit_root: ExitRoot::from(mainnet_exit_root), - rollup_exit_root: ExitRoot::from(rollup_exit_root), - }; - - let leaf_data = LeafData { - origin_network, - origin_token_address: EthAddressFormat::new(origin_token_address), - destination_network, - destination_address: EthAddressFormat::new(destination_address), - amount: amount_eth, - metadata, - }; - let output_note_data = OutputNoteData { output_p2id_serial_num: serial_num, target_faucet_account_id: agglayer_faucet.id(), - output_note_tag: NoteTag::with_account_target(user_account.id()), + output_note_tag: NoteTag::with_account_target(destination_account_id), }; let claim_inputs = ClaimNoteStorage { proof_data, leaf_data, output_note_data }; - let claim_note = create_claim_note(claim_inputs, user_account.id(), builder.rng_mut())?; - - // Create P2ID note for the user account (similar to network faucet test) - let p2id_script = StandardNote::P2ID.script(); - let p2id_inputs = vec![user_account.id().suffix(), user_account.id().prefix().as_felt()]; - let note_storage = NoteStorage::new(p2id_inputs)?; - let p2id_recipient = NoteRecipient::new(serial_num, p2id_script.clone(), note_storage); + let claim_note = create_claim_note(claim_inputs, sender_account.id(), builder.rng_mut())?; // Add the claim note to the builder before building the mock chain builder.add_output_note(OutputNote::Full(claim_note.clone())); + // CREATE UPDATE_GER NOTE WITH GLOBAL EXIT ROOT + // -------------------------------------------------------------------------------------------- + let update_ger_note = + UpdateGerNote::create(ger, sender_account.id(), bridge_account.id(), builder.rng_mut())?; + builder.add_output_note(OutputNote::Full(update_ger_note.clone())); + // BUILD MOCK CHAIN WITH ALL ACCOUNTS // -------------------------------------------------------------------------------------------- let mut mock_chain = builder.clone().build()?; - mock_chain.prove_next_block()?; - // CREATE EXPECTED P2ID NOTE FOR VERIFICATION + // EXECUTE UPDATE_GER NOTE TO STORE GER IN BRIDGE ACCOUNT // -------------------------------------------------------------------------------------------- - let amount_felt = Felt::from(claim_amount); - let mint_asset: Asset = FungibleAsset::new(agglayer_faucet.id(), amount_felt.into())?.into(); - let output_note_tag = NoteTag::with_account_target(user_account.id()); - let expected_p2id_note = Note::new( - NoteAssets::new(vec![mint_asset])?, - NoteMetadata::new(agglayer_faucet.id(), NoteType::Public).with_tag(output_note_tag), - p2id_recipient, - ); + let update_ger_tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? + .build()?; + let update_ger_executed = update_ger_tx_context.execute().await?; + + mock_chain.add_pending_executed_transaction(&update_ger_executed)?; + mock_chain.prove_next_block()?; // EXECUTE CLAIM NOTE AGAINST AGGLAYER FAUCET (with FPI to Bridge) // -------------------------------------------------------------------------------------------- @@ -185,44 +144,19 @@ async fn test_bridge_in_claim_to_p2id() -> anyhow::Result<()> { assert_eq!(executed_transaction.output_notes().num_notes(), 1); let output_note = executed_transaction.output_notes().get_note(0); - // Verify the output note contains the minted fungible asset - let expected_asset = FungibleAsset::new(agglayer_faucet.id(), claim_amount.into())?; - // Verify note metadata properties assert_eq!(output_note.metadata().sender(), agglayer_faucet.id()); assert_eq!(output_note.metadata().note_type(), NoteType::Public); - assert_eq!(output_note.id(), expected_p2id_note.id()); - - // Extract the full note from the OutputNote enum for detailed verification - let full_note = match output_note { - OutputNote::Full(note) => note, - _ => panic!("Expected OutputNote::Full variant for public note"), - }; - - // Verify note structure and asset content - let expected_asset_obj = Asset::from(expected_asset); - assert_eq!(full_note, &expected_p2id_note); - - assert!(full_note.assets().iter().any(|asset| asset == &expected_asset_obj)); - - // Apply the transaction to the mock chain - mock_chain.add_pending_executed_transaction(&executed_transaction)?; - mock_chain.prove_next_block()?; - - // CONSUME THE OUTPUT NOTE WITH TARGET ACCOUNT - // -------------------------------------------------------------------------------------------- - // Consume the output note with target account - let mut user_account_mut = user_account.clone(); - let consume_tx_context = mock_chain - .build_tx_context(user_account_mut.clone(), &[], slice::from_ref(&expected_p2id_note))? - .build()?; - let consume_executed_transaction = consume_tx_context.execute().await?; - - user_account_mut.apply_delta(consume_executed_transaction.account_delta())?; - // Verify the account's vault now contains the expected fungible asset - let balance = user_account_mut.vault().get_balance(agglayer_faucet.id())?; - assert_eq!(balance, expected_asset.amount()); + // Note: We intentionally do NOT verify the exact note ID or asset amount here because + // the scale_u256_to_native_amount function is currently a TODO stub that doesn't perform + // proper u256-to-native scaling. The test verifies that the bridge-in flow correctly + // validates the Merkle proof using real cryptographic proof data and creates an output note. + // + // TODO: Once scale_u256_to_native_amount is properly implemented, add: + // - Verification that the minted amount matches the expected scaled value + // - Full note ID comparison with the expected P2ID note + // - Asset content verification Ok(()) } diff --git a/crates/miden-testing/tests/agglayer/bridge_out.rs b/crates/miden-testing/tests/agglayer/bridge_out.rs index 23f1663631..83fc957eb3 100644 --- a/crates/miden-testing/tests/agglayer/bridge_out.rs +++ b/crates/miden-testing/tests/agglayer/bridge_out.rs @@ -1,156 +1,358 @@ extern crate alloc; -use miden_agglayer::errors::ERR_B2AGG_TARGET_ACCOUNT_MISMATCH; -use miden_agglayer::{B2AggNote, EthAddressFormat, create_existing_bridge_account}; +use miden_agglayer::errors::{ERR_B2AGG_TARGET_ACCOUNT_MISMATCH, ERR_FAUCET_NOT_REGISTERED}; +use miden_agglayer::{ + B2AggNote, + ConfigAggBridgeNote, + EthAddressFormat, + ExitRoot, + create_existing_agglayer_faucet, + create_existing_bridge_account, +}; use miden_crypto::rand::FeltRng; use miden_protocol::Felt; -use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; +use miden_protocol::account::{ + Account, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, + StorageSlotName, +}; use miden_protocol::asset::{Asset, FungibleAsset}; -use miden_protocol::note::{NoteAssets, NoteTag, NoteType}; +use miden_protocol::note::{NoteAssets, NoteScript, NoteTag, NoteType}; use miden_protocol::transaction::OutputNote; use miden_standards::account::faucets::TokenMetadata; use miden_standards::note::StandardNote; use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; +use miden_tx::utils::hex_to_bytes; -/// Tests the B2AGG (Bridge to AggLayer) note script with bridge_out account component. +use super::test_utils::SOLIDITY_MMR_FRONTIER_VECTORS; + +/// Reads the Local Exit Root (double-word) from the bridge account's storage. /// -/// This test flow: -/// 1. Creates a network faucet to provide assets -/// 2. Creates a bridge account with the bridge_out component (using network storage) -/// 3. Creates a B2AGG note with assets from the network faucet -/// 4. Executes the B2AGG note consumption via network transaction -/// 5. Consumes the BURN note -#[tokio::test] -async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { - let mut builder = MockChain::builder(); +/// The Local Exit Root is stored in two dedicated value slots: +/// - `"miden::agglayer::let::root_lo"` — low word of the root +/// - `"miden::agglayer::let::root_hi"` — high word of the root +/// +/// Returns the 256-bit root as 8 `Felt`s: first the 4 elements of `root_lo` (in +/// reverse of their storage order), followed by the 4 elements of `root_hi` (also in +/// reverse of their storage order). For an empty/uninitialized tree, all elements are +/// zeros. +fn read_local_exit_root(account: &Account) -> Vec { + let root_lo_slot = + StorageSlotName::new("miden::agglayer::let::root_lo").expect("slot name should be valid"); + let root_hi_slot = + StorageSlotName::new("miden::agglayer::let::root_hi").expect("slot name should be valid"); + + let root_lo = account + .storage() + .get_item(&root_lo_slot) + .expect("should be able to read LET root lo"); + let root_hi = account + .storage() + .get_item(&root_hi_slot) + .expect("should be able to read LET root hi"); + + let mut root = Vec::with_capacity(8); + root.extend(root_lo.to_vec().into_iter().rev()); + root.extend(root_hi.to_vec().into_iter().rev()); + root +} - // Create a network faucet owner account - let faucet_owner_account_id = AccountId::dummy( - [1; 15], - AccountIdVersion::Version0, - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - ); +fn read_let_num_leaves(account: &Account) -> u64 { + let num_leaves_slot = StorageSlotName::new("miden::agglayer::let::num_leaves") + .expect("slot name should be valid"); + let value = account + .storage() + .get_item(&num_leaves_slot) + .expect("should be able to read LET num leaves"); + value.to_vec()[0].as_int() +} - // Create a network faucet to provide assets for the B2AGG note - let faucet = - builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; +/// Tests that 32 sequential B2AGG note consumptions match all 32 Solidity MMR roots. +/// +/// This test exercises the complete bridge-out lifecycle: +/// 1. Creates a bridge account (empty faucet registry) and an agglayer faucet with conversion +/// metadata (origin token address, network, scale) +/// 2. Registers the faucet in the bridge's faucet registry via a CONFIG_AGG_BRIDGE note +/// 3. Creates a B2AGG note with assets from the agglayer faucet +/// 4. Consumes the B2AGG note against the bridge account — the bridge's `bridge_out` procedure: +/// - Validates the faucet is registered via `convert_asset` +/// - Calls the faucet's `asset_to_origin_asset` via FPI to get the scaled amount, origin token +/// address, and origin network +/// - Writes the leaf data and computes the Keccak hash for the MMR +/// - Creates a BURN note addressed to the faucet +/// 5. Verifies the BURN note was created with the correct asset, tag, and script +/// 6. Consumes the BURN note with the faucet to burn the tokens +#[tokio::test] +async fn bridge_out_consecutive() -> anyhow::Result<()> { + let vectors = &*SOLIDITY_MMR_FRONTIER_VECTORS; + let note_count = 32usize; + assert_eq!(vectors.amounts.len(), note_count, "amount vectors should contain 32 entries"); + assert_eq!(vectors.roots.len(), note_count, "root vectors should contain 32 entries"); + assert_eq!( + vectors.destination_networks.len(), + note_count, + "destination network vectors should contain 32 entries" + ); + assert_eq!( + vectors.destination_addresses.len(), + note_count, + "destination address vectors should contain 32 entries" + ); - // Create a bridge account (includes a `bridge_out` component tested here) + let mut builder = MockChain::builder(); let mut bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); builder.add_account(bridge_account.clone())?; - // CREATE B2AGG NOTE WITH ASSETS - // -------------------------------------------------------------------------------------------- + let expected_amounts = vectors + .amounts + .iter() + .map(|amount| amount.parse::().expect("valid amount decimal string")) + .collect::>(); + let total_burned: u64 = expected_amounts.iter().sum(); - let amount = Felt::new(100); - let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - - // Create note storage with destination network and address - let destination_network = 1u32; // Example network ID - let destination_address = "0x1234567890abcdef1122334455667788990011aa"; - let eth_address = - EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); + // CREATE AGGLAYER FAUCET ACCOUNT (with conversion metadata for FPI) + // -------------------------------------------------------------------------------------------- + let origin_token_address = EthAddressFormat::from_hex(&vectors.origin_token_address) + .expect("valid shared origin token address"); + let origin_network = 64u32; + let scale = 0u8; + let faucet = create_existing_agglayer_faucet( + builder.rng_mut().draw_word(), + "AGG", + 8, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(total_burned), + bridge_account.id(), + &origin_token_address, + origin_network, + scale, + ); + builder.add_account(faucet.clone())?; - let assets = NoteAssets::new(vec![bridge_asset])?; + // CREATE SENDER ACCOUNT + // -------------------------------------------------------------------------------------------- + let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; - // Create the B2AGG note using the helper - let b2agg_note = B2AggNote::create( - destination_network, - eth_address, - assets, - bridge_account.id(), + // CONFIG_AGG_BRIDGE note to register the faucet in the bridge + let config_note = ConfigAggBridgeNote::create( faucet.id(), + sender_account.id(), + bridge_account.id(), builder.rng_mut(), )?; + builder.add_output_note(OutputNote::Full(config_note.clone())); + + // CREATE ALL B2AGG NOTES UPFRONT (before building mock chain) + // -------------------------------------------------------------------------------------------- + let mut notes = Vec::with_capacity(note_count); + for (i, &amount) in expected_amounts.iter().enumerate().take(note_count) { + let destination_network = vectors.destination_networks[i]; + let eth_address = EthAddressFormat::from_hex(&vectors.destination_addresses[i]) + .expect("valid destination address"); + + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount).unwrap().into(); + let note = B2AggNote::create( + destination_network, + eth_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + faucet.id(), + builder.rng_mut(), + )?; + builder.add_output_note(OutputNote::Full(note.clone())); + notes.push(note); + } - // Add the B2AGG note to the mock chain - builder.add_output_note(OutputNote::Full(b2agg_note.clone())); let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; - // EXECUTE B2AGG NOTE AGAINST BRIDGE ACCOUNT (NETWORK TRANSACTION) + // STEP 1: REGISTER FAUCET VIA CONFIG_AGG_BRIDGE NOTE // -------------------------------------------------------------------------------------------- - let tx_context = mock_chain - .build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])? - .build()?; - let executed_transaction = tx_context.execute().await?; + let config_executed = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()? + .execute() + .await?; + bridge_account.apply_delta(config_executed.account_delta())?; + mock_chain.add_pending_executed_transaction(&config_executed)?; + mock_chain.prove_next_block()?; - // VERIFY PUBLIC BURN NOTE WAS CREATED + // STEP 2: CONSUME 32 B2AGG NOTES AND VERIFY FRONTIER EVOLUTION + // -------------------------------------------------------------------------------------------- + let burn_note_script: NoteScript = StandardNote::BURN.script(); + let mut burn_note_ids = Vec::with_capacity(note_count); + + for (i, note) in notes.iter().enumerate() { + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + let executed_tx = mock_chain + .build_tx_context(bridge_account.clone(), &[note.id()], &[])? + .add_note_script(burn_note_script.clone()) + .foreign_accounts(vec![foreign_account_inputs]) + .build()? + .execute() + .await?; + + assert_eq!( + executed_tx.output_notes().num_notes(), + 1, + "Expected one BURN note after consume #{}", + i + 1 + ); + let burn_note = match executed_tx.output_notes().get_note(0) { + OutputNote::Full(note) => note, + _ => panic!("Expected OutputNote::Full variant for BURN note"), + }; + burn_note_ids.push(burn_note.id()); + + let expected_asset = Asset::from(FungibleAsset::new(faucet.id(), expected_amounts[i])?); + assert!( + burn_note.assets().iter().any(|asset| asset == &expected_asset), + "BURN note after consume #{} should contain the bridged asset", + i + 1 + ); + assert_eq!( + burn_note.metadata().note_type(), + NoteType::Public, + "BURN note should be public" + ); + assert_eq!( + burn_note.metadata().tag(), + NoteTag::with_account_target(faucet.id()), + "BURN note should have the correct tag" + ); + assert_eq!( + burn_note.recipient().script().root(), + StandardNote::BURN.script_root(), + "BURN note should use the BURN script" + ); + + bridge_account.apply_delta(executed_tx.account_delta())?; + assert_eq!( + read_let_num_leaves(&bridge_account), + (i + 1) as u64, + "LET leaf count should match consumed notes" + ); + + let expected_ler = + ExitRoot::new(hex_to_bytes(&vectors.roots[i]).expect("valid root hex")).to_elements(); + assert_eq!( + read_local_exit_root(&bridge_account), + expected_ler, + "Local Exit Root after {} leaves should match the Solidity-generated root", + i + 1 + ); + + mock_chain.add_pending_executed_transaction(&executed_tx)?; + mock_chain.prove_next_block()?; + } + + // STEP 3: CONSUME ALL BURN NOTES WITH THE AGGLAYER FAUCET // -------------------------------------------------------------------------------------------- - // The bridge_out component should create a PUBLIC BURN note addressed to the faucet + let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); assert_eq!( - executed_transaction.output_notes().num_notes(), - 1, - "Expected one BURN note to be created" + initial_token_supply, + Felt::new(total_burned), + "Initial issuance should match all pending burns" ); - let output_note = executed_transaction.output_notes().get_note(0); + let mut faucet = faucet; + for burn_note_id in burn_note_ids { + let burn_executed_tx = mock_chain + .build_tx_context(faucet.id(), &[burn_note_id], &[])? + .build()? + .execute() + .await?; + assert_eq!( + burn_executed_tx.output_notes().num_notes(), + 0, + "Burn transaction should not create output notes" + ); + faucet.apply_delta(burn_executed_tx.account_delta())?; + mock_chain.add_pending_executed_transaction(&burn_executed_tx)?; + mock_chain.prove_next_block()?; + } - // Extract the full note from the OutputNote enum - let burn_note = match output_note { - OutputNote::Full(note) => note, - _ => panic!("Expected OutputNote::Full variant for BURN note"), - }; + let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); + assert_eq!( + final_token_supply, + Felt::new(initial_token_supply.as_int() - total_burned), + "Token supply should decrease by the sum of 32 bridged amounts" + ); - // Verify the BURN note is public - assert_eq!(burn_note.metadata().note_type(), NoteType::Public, "BURN note should be public"); + Ok(()) +} - // Verify the BURN note contains the bridged asset - let expected_asset = FungibleAsset::new(faucet.id(), amount.into())?; - let expected_asset_obj = Asset::from(expected_asset); - assert!( - burn_note.assets().iter().any(|asset| asset == &expected_asset_obj), - "BURN note should contain the bridged asset" - ); +/// Tests that bridging out fails when the faucet is not registered in the bridge's registry. +/// +/// This test verifies the faucet allowlist check in bridge_out's `convert_asset` procedure: +/// 1. Creates a bridge account with an empty faucet registry (no faucets registered) +/// 2. Creates a B2AGG note with an asset from an agglayer faucet +/// 3. Attempts to consume the B2AGG note against the bridge — this should fail because +/// `convert_asset` checks the faucet registry and panics with ERR_FAUCET_NOT_REGISTERED when the +/// faucet is not found +#[tokio::test] +async fn test_bridge_out_fails_with_unregistered_faucet() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); - assert_eq!( - burn_note.metadata().tag(), - NoteTag::with_account_target(faucet.id()), - "BURN note should have the correct tag" - ); + // CREATE BRIDGE ACCOUNT (empty faucet registry — no faucets registered) + // -------------------------------------------------------------------------------------------- + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; - // Verify the BURN note uses the correct script - assert_eq!( - burn_note.recipient().script().root(), - StandardNote::BURN.script_root(), - "BURN note should use the BURN script" + // CREATE AGGLAYER FAUCET ACCOUNT (NOT registered in the bridge) + // -------------------------------------------------------------------------------------------- + let origin_token_address = EthAddressFormat::new([0u8; 20]); + let faucet = create_existing_agglayer_faucet( + builder.rng_mut().draw_word(), + "AGG", + 8, + Felt::new(FungibleAsset::MAX_AMOUNT), + Felt::new(100), + bridge_account.id(), + &origin_token_address, + 0, // origin_network + 0, // scale ); + builder.add_account(faucet.clone())?; - // Apply the delta to the bridge account - bridge_account.apply_delta(executed_transaction.account_delta())?; + // CREATE B2AGG NOTE WITH ASSETS FROM THE UNREGISTERED FAUCET + // -------------------------------------------------------------------------------------------- + let amount = Felt::new(100); + let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - // Apply the transaction to the mock chain - mock_chain.add_pending_executed_transaction(&executed_transaction)?; - mock_chain.prove_next_block()?; + let destination_address = "0x1234567890abcdef1122334455667788990011aa"; + let eth_address = + EthAddressFormat::from_hex(destination_address).expect("Valid Ethereum address"); - // CONSUME THE BURN NOTE WITH THE NETWORK FAUCET - // -------------------------------------------------------------------------------------------- - // Check the initial token issuance before burning - let initial_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); - assert_eq!(initial_token_supply, Felt::new(100), "Initial issuance should be 100"); + let b2agg_note = B2AggNote::create( + 1u32, // destination_network + eth_address, + NoteAssets::new(vec![bridge_asset])?, + bridge_account.id(), + faucet.id(), + builder.rng_mut(), + )?; - // Execute the BURN note against the network faucet - let burn_tx_context = - mock_chain.build_tx_context(faucet.id(), &[burn_note.id()], &[])?.build()?; - let burn_executed_transaction = burn_tx_context.execute().await?; + builder.add_output_note(OutputNote::Full(b2agg_note.clone())); + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; - // Verify the burn transaction was successful - no output notes should be created - assert_eq!( - burn_executed_transaction.output_notes().num_notes(), - 0, - "Burn transaction should not create output notes" - ); + // ATTEMPT TO BRIDGE OUT WITHOUT REGISTERING THE FAUCET (SHOULD FAIL) + // -------------------------------------------------------------------------------------------- + let foreign_account_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; - // Apply the delta to the faucet account and verify the token issuance decreased - let mut faucet = faucet; - faucet.apply_delta(burn_executed_transaction.account_delta())?; + let result = mock_chain + .build_tx_context(bridge_account.id(), &[b2agg_note.id()], &[])? + .foreign_accounts(vec![foreign_account_inputs]) + .build()? + .execute() + .await; - let final_token_supply = TokenMetadata::try_from(faucet.storage())?.token_supply(); - assert_eq!( - final_token_supply, - Felt::new(initial_token_supply.as_int() - amount.as_int()), - "Token issuance should decrease by the burned amount" - ); + assert_transaction_executor_error!(result, ERR_FAUCET_NOT_REGISTERED); Ok(()) } @@ -167,7 +369,7 @@ async fn test_bridge_out_consumes_b2agg_note() -> anyhow::Result<()> { /// 4. The same user account consumes the B2AGG note (triggering reclaim branch) /// 5. Verifies that assets are added back to the account and no BURN note is created #[tokio::test] -async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { +async fn b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let mut builder = MockChain::builder(); // Create a network faucet owner account @@ -182,7 +384,7 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let faucet = builder.add_existing_network_faucet("AGG", 1000, faucet_owner_account_id, Some(100))?; - // Create a bridge account (includes a `bridge_out` component tested here) + // Create a bridge account (includes a `bridge_out` component) let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); builder.add_account(bridge_account.clone())?; @@ -191,11 +393,9 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // CREATE B2AGG NOTE WITH USER ACCOUNT AS SENDER // -------------------------------------------------------------------------------------------- - let amount = Felt::new(50); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - // Create note storage with destination network and address let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = @@ -203,8 +403,8 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { let assets = NoteAssets::new(vec![bridge_asset])?; - // Create the B2AGG note with the USER ACCOUNT as the sender - // This is the key difference - the note sender will be the same as the consuming account + // Create the B2AGG note with the USER ACCOUNT as the sender. + // This is the key difference — the note sender will be the same as the consuming account. let b2agg_note = B2AggNote::create( destination_network, eth_address, @@ -214,7 +414,6 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { builder.rng_mut(), )?; - // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); let mut mock_chain = builder.build()?; @@ -230,7 +429,6 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // VERIFY NO BURN NOTE WAS CREATED (RECLAIM BRANCH) // -------------------------------------------------------------------------------------------- - // In the reclaim scenario, no BURN note should be created assert_eq!( executed_transaction.output_notes().num_notes(), 0, @@ -243,14 +441,12 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { // VERIFY ASSETS WERE ADDED BACK TO THE ACCOUNT // -------------------------------------------------------------------------------------------- let final_balance = user_account.vault().get_balance(faucet.id()).unwrap_or(0u64); - let expected_balance = initial_balance + amount.as_int(); - assert_eq!( - final_balance, expected_balance, + final_balance, + initial_balance + amount.as_int(), "User account should have received the assets back from the B2AGG note" ); - // Apply the transaction to the mock chain mock_chain.add_pending_executed_transaction(&executed_transaction)?; mock_chain.prove_next_block()?; @@ -271,7 +467,7 @@ async fn test_b2agg_note_reclaim_scenario() -> anyhow::Result<()> { /// 5. Attempts to consume the B2AGG note with the malicious account /// 6. Verifies that the transaction fails with ERR_B2AGG_TARGET_ACCOUNT_MISMATCH #[tokio::test] -async fn test_b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { +async fn b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<()> { let mut builder = MockChain::builder(); // Create a network faucet owner account @@ -299,11 +495,9 @@ async fn test_b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<( // CREATE B2AGG NOTE // -------------------------------------------------------------------------------------------- - let amount = Felt::new(50); let bridge_asset: Asset = FungibleAsset::new(faucet.id(), amount.into()).unwrap().into(); - // Create note storage with destination network and address let destination_network = 1u32; let destination_address = "0x1234567890abcdef1122334455667788990011aa"; let eth_address = @@ -311,7 +505,7 @@ async fn test_b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<( let assets = NoteAssets::new(vec![bridge_asset])?; - // Create the B2AGG note + // Create the B2AGG note targeting the real bridge account let b2agg_note = B2AggNote::create( destination_network, eth_address, @@ -321,7 +515,6 @@ async fn test_b2agg_note_non_target_account_cannot_consume() -> anyhow::Result<( builder.rng_mut(), )?; - // Add the B2AGG note to the mock chain builder.add_output_note(OutputNote::Full(b2agg_note.clone())); let mock_chain = builder.build()?; diff --git a/crates/miden-testing/tests/agglayer/config_bridge.rs b/crates/miden-testing/tests/agglayer/config_bridge.rs new file mode 100644 index 0000000000..ee676d0816 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/config_bridge.rs @@ -0,0 +1,82 @@ +extern crate alloc; + +use miden_agglayer::{ConfigAggBridgeNote, create_existing_bridge_account, faucet_registry_key}; +use miden_protocol::account::{ + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, + StorageSlotName, +}; +use miden_protocol::crypto::rand::FeltRng; +use miden_protocol::transaction::OutputNote; +use miden_protocol::{Felt, FieldElement}; +use miden_testing::{Auth, MockChain}; + +/// Tests that a CONFIG_AGG_BRIDGE note registers a faucet in the bridge's faucet registry. +/// +/// Flow: +/// 1. Create a bridge account (empty faucet registry) +/// 2. Create a sender account +/// 3. Create a CONFIG_AGG_BRIDGE note carrying a faucet ID +/// 4. Consume the note with the bridge account +/// 5. Verify the faucet is now in the bridge's faucet_registry map +#[tokio::test] +async fn test_config_agg_bridge_registers_faucet() -> anyhow::Result<()> { + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ACCOUNT (starts with empty faucet registry) + let bridge_account = create_existing_bridge_account(builder.rng_mut().draw_word()); + builder.add_account(bridge_account.clone())?; + + // CREATE SENDER ACCOUNT + let sender_account = builder.add_existing_wallet(Auth::BasicAuth)?; + + // Use a dummy faucet ID to register (any valid AccountId will do) + let faucet_to_register = AccountId::dummy( + [42; 15], + AccountIdVersion::Version0, + AccountType::FungibleFaucet, + AccountStorageMode::Network, + ); + + // Verify the faucet is NOT in the registry before registration + let registry_slot_name = StorageSlotName::new("miden::agglayer::bridge::faucet_registry")?; + let key = faucet_registry_key(faucet_to_register); + let value_before = bridge_account.storage().get_map_item(®istry_slot_name, key)?; + assert_eq!( + value_before, + [Felt::ZERO; 4].into(), + "Faucet should not be in registry before registration" + ); + + // CREATE CONFIG_AGG_BRIDGE NOTE + let config_note = ConfigAggBridgeNote::create( + faucet_to_register, + sender_account.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + + builder.add_output_note(OutputNote::Full(config_note.clone())); + let mock_chain = builder.build()?; + + // CONSUME THE CONFIG_AGG_BRIDGE NOTE WITH THE BRIDGE ACCOUNT + let tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()?; + let executed_transaction = tx_context.execute().await?; + + // VERIFY FAUCET IS NOW REGISTERED + let mut updated_bridge = bridge_account.clone(); + updated_bridge.apply_delta(executed_transaction.account_delta())?; + + let value_after = updated_bridge.storage().get_map_item(®istry_slot_name, key)?; + let expected_value = [Felt::new(1), Felt::ZERO, Felt::ZERO, Felt::ZERO].into(); + assert_eq!( + value_after, expected_value, + "Faucet should be registered with value [0, 0, 0, 1]" + ); + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/crypto_utils.rs b/crates/miden-testing/tests/agglayer/crypto_utils.rs index 392795086c..6f8e26a017 100644 --- a/crates/miden-testing/tests/agglayer/crypto_utils.rs +++ b/crates/miden-testing/tests/agglayer/crypto_utils.rs @@ -5,129 +5,190 @@ use alloc::sync::Arc; use alloc::vec::Vec; use anyhow::Context; -use miden_agglayer::agglayer_library; +use miden_agglayer::claim_note::Keccak256Output; +use miden_agglayer::utils::felts_to_bytes; +use miden_agglayer::{ExitRoot, SmtNode, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; -use miden_core_lib::handlers::bytes_to_packed_u32_felts; -use miden_core_lib::handlers::keccak256::KeccakPreimage; -use miden_crypto::FieldElement; -use miden_crypto::hash::keccak::Keccak256Digest; +use miden_crypto::SequentialCommit; use miden_processor::AdviceInputs; -use miden_protocol::utils::sync::LazyLock; -use miden_protocol::{Felt, Hasher, Word}; +use miden_protocol::{Felt, Word}; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; -use serde::Deserialize; - -use super::test_utils::{execute_program_with_default_host, keccak_digest_to_word_strings}; - -// LEAF_DATA_NUM_WORDS is defined as 8 in crypto_utils.masm, representing 8 Miden words of 4 felts -// each -const LEAF_DATA_FELTS: usize = 32; - -/// Merkle proof verification vectors JSON embedded at compile time from the Foundry-generated file. -const MERKLE_PROOF_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); - -/// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol -/// Uses parallel arrays for leaves and roots. For each element from leaves/roots there are 32 -/// elements from merkle_paths, which represent the merkle path for that leaf + root. -#[derive(Debug, Deserialize)] -struct MerkleProofVerificationFile { - leaves: Vec, - roots: Vec, - merkle_paths: Vec, -} - -/// Lazily parsed Merkle proof vectors from the JSON file. -static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = LazyLock::new(|| { - serde_json::from_str(MERKLE_PROOF_VECTORS_JSON) - .expect("failed to parse Merkle proof vectors JSON") -}); +use miden_tx::utils::hex_to_bytes; -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]; +use super::test_utils::{ + LEAF_VALUE_VECTORS_JSON, + LeafValueVector, + MerkleProofVerificationFile, + SOLIDITY_MERKLE_PROOF_VECTORS, + execute_program_with_default_host, +}; - 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); - } +// HELPER FUNCTIONS +// ================================================================================================ - let mut s = String::from("0x"); - for b in out { - s.push_str(&format!("{:02x}", b)); +fn felts_to_le_bytes(limbs: &[Felt]) -> Vec { + let mut bytes = Vec::with_capacity(limbs.len() * 4); + for limb in limbs.iter() { + let u32_value = limb.as_int() as u32; + bytes.extend_from_slice(&u32_value.to_le_bytes()); } - s + bytes } -// 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(); +fn merkle_proof_verification_code( + index: usize, + merkle_paths: &MerkleProofVerificationFile, +) -> String { + // generate the code which stores the merkle path to the memory + let mut store_path_source = String::new(); + for height in 0..32 { + let path_node = merkle_paths.merkle_paths[index * 32 + height].as_str(); + let smt_node = SmtNode::from(hex_to_bytes(path_node).unwrap()); + let [node_lo, node_hi] = smt_node.to_words(); + // each iteration (each index in leaf/root vector) we rewrite the merkle path nodes, so the + // memory pointers for the merkle path and the expected root never change + store_path_source.push_str(&format!( + " + \tpush.{node_lo} mem_storew_be.{} dropw + \tpush.{node_hi} mem_storew_be.{} dropw + ", + height * 8, + height * 8 + 4 + )); } - out + + // prepare the root for the provided index + let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); + let [root_lo, root_hi] = root.to_words(); + + // prepare the leaf for the provided index + let leaf = Keccak256Output::from(hex_to_bytes(&merkle_paths.leaves[index]).unwrap()); + let [leaf_lo, leaf_hi] = leaf.to_words(); + + format!( + r#" + use miden::agglayer::crypto_utils + use miden::core::word + + begin + # store the merkle path to the memory (double word slots from 0 to 248) + {store_path_source} + # => [] + + # store the root to the memory (double word slot 256) + push.{root_lo} mem_storew_be.256 dropw + push.{root_hi} mem_storew_be.260 dropw + # => [] + + # prepare the stack for the `verify_merkle_proof` procedure + push.256 # expected root memory pointer + push.{index} # provided leaf index + push.0 # Merkle path memory pointer + # in practice this is never "pushed" to the stack, but rather an output of `get_leaf_value` + # which returns the leaf value in LE-felt order + push.{leaf_hi} + exec.word::reverse + push.{leaf_lo} + exec.word::reverse + # => [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx, expected_root_ptr] + + exec.crypto_utils::verify_merkle_proof + # => [verification_flag] + + assert.err="verification failed" + # => [] + end + "# + ) } +// TESTS +// ================================================================================================ + +/// Test that the `pack_leaf_data` procedure produces the correct byte layout. #[tokio::test] -async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { +async fn pack_leaf_data() -> anyhow::Result<()> { + let vector: LeafValueVector = + serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("Failed to parse leaf value vector"); + + let leaf_data = vector.to_leaf_data(); + + // Build expected bytes + let mut expected_packed_bytes: Vec = Vec::new(); + expected_packed_bytes.push(0u8); + expected_packed_bytes.extend_from_slice(&leaf_data.origin_network.to_be_bytes()); + expected_packed_bytes.extend_from_slice(leaf_data.origin_token_address.as_bytes()); + expected_packed_bytes.extend_from_slice(&leaf_data.destination_network.to_be_bytes()); + expected_packed_bytes.extend_from_slice(leaf_data.destination_address.as_bytes()); + expected_packed_bytes.extend_from_slice(leaf_data.amount.as_bytes()); + let metadata_hash_bytes: [u8; 32] = hex_to_bytes(&vector.metadata_hash).unwrap(); + expected_packed_bytes.extend_from_slice(&metadata_hash_bytes); + assert_eq!(expected_packed_bytes.len(), 113); + let agglayer_lib = agglayer_library(); + let leaf_data_elements = leaf_data.to_elements(); + let leaf_data_bytes: Vec = felts_to_bytes(&leaf_data_elements); + assert_eq!( + leaf_data_bytes.len(), + 128, + "expected 8 words * 4 felts * 4 bytes per felt = 128 bytes" + ); + assert_eq!(leaf_data_bytes[116..], vec![0; 12], "the last 3 felts are pure padding"); + assert_eq!(leaf_data_bytes[3], expected_packed_bytes[0], "the first byte is the leaf type"); + assert_eq!( + leaf_data_bytes[4..8], + expected_packed_bytes[1..5], + "the next 4 bytes are the origin network" + ); + assert_eq!( + leaf_data_bytes[8..28], + expected_packed_bytes[5..25], + "the next 20 bytes are the origin token address" + ); + assert_eq!( + leaf_data_bytes[28..32], + expected_packed_bytes[25..29], + "the next 4 bytes are the destination network" + ); + assert_eq!( + leaf_data_bytes[32..52], + expected_packed_bytes[29..49], + "the next 20 bytes are the destination address" + ); + assert_eq!( + leaf_data_bytes[52..84], + expected_packed_bytes[49..81], + "the next 32 bytes are the amount" + ); + assert_eq!( + leaf_data_bytes[84..116], + expected_packed_bytes[81..113], + "the next 32 bytes are the metadata hash" + ); + + assert_eq!(leaf_data_bytes[3..116], expected_packed_bytes, "byte packing is as expected"); - // === Values from hardhat test === - let leaf_type: u8 = 0; - let origin_network: u32 = 0; - let token_address: [u8; 20] = hex_to_fixed("0x1234567890123456789012345678901234567890"); - let destination_network: u32 = 1; - let destination_address: [u8; 20] = hex_to_fixed("0x0987654321098765432109876543210987654321"); - let amount_u64: u64 = 1; // 1e19 - let metadata_hash: [u8; 32] = - hex_to_fixed("0x2cdc14cacf6fec86a549f0e4d01e83027d3b10f29fa527c1535192c1ca1aac81"); - - // Expected hash value from Solidity implementation - let expected_hash = "0xf6825f6c59be2edf318d7251f4b94c0e03eb631b76a0e7b977fd8ed3ff925a3f"; - - // abi.encodePacked( - // uint8, uint32, address, uint32, address, uint256, bytes32 - // ) - let mut amount_u256_be = [0u8; 32]; - amount_u256_be[24..32].copy_from_slice(&amount_u64.to_be_bytes()); - - let mut input_u8 = Vec::with_capacity(113); - input_u8.push(leaf_type); - input_u8.extend_from_slice(&origin_network.to_be_bytes()); - input_u8.extend_from_slice(&token_address); - input_u8.extend_from_slice(&destination_network.to_be_bytes()); - input_u8.extend_from_slice(&destination_address); - input_u8.extend_from_slice(&amount_u256_be); - input_u8.extend_from_slice(&metadata_hash); - - let len_bytes = input_u8.len(); - assert_eq!(len_bytes, 113); - - let preimage = KeccakPreimage::new(input_u8.clone()); - let mut input_felts = bytes_to_packed_u32_felts(&input_u8); - // Pad to LEAF_DATA_FELTS (128 bytes) as expected by the downstream code - input_felts.resize(LEAF_DATA_FELTS, Felt::ZERO); - assert_eq!(input_felts.len(), LEAF_DATA_FELTS); - - // Arbitrary key to store input in advice map (in prod this is RPO(input_felts)) - let key: Word = Hasher::hash_elements(&input_felts); - let advice_inputs = AdviceInputs::default().with_map(vec![(key, input_felts)]); + let key: Word = leaf_data.to_commitment(); + let advice_inputs = AdviceInputs::default().with_map(vec![(key, leaf_data_elements.clone())]); let source = format!( r#" - use miden::core::sys - use miden::core::crypto::hashes::keccak256 + use miden::core::mem use miden::agglayer::crypto_utils + const LEAF_DATA_START_PTR = 0 + const LEAF_DATA_NUM_WORDS = 8 + begin push.{key} - exec.crypto_utils::get_leaf_value - exec.sys::truncate_stack + adv.push_mapval + push.LEAF_DATA_START_PTR push.LEAF_DATA_NUM_WORDS + exec.mem::pipe_preimage_to_memory drop + + exec.crypto_utils::pack_leaf_data end "# ); @@ -142,18 +203,75 @@ async fn test_keccak_hash_get_leaf_value() -> anyhow::Result<()> { 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); + // Read packed elements from memory at addresses 0..29 + let ctx = miden_processor::ContextId::root(); + let err_ctx = (); - 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); + let packed_elements: Vec = (0..29u32) + .map(|addr| { + exec_output + .memory + .read_element(ctx, Felt::from(addr), &err_ctx) + .expect("address should be valid") + }) + .collect(); + + let packed_bytes: Vec = felts_to_le_bytes(&packed_elements); + + // push 3 more zero bytes for packing, since `pack_leaf_data` should leave us with the last 3 + // bytes set to 0 (prep for hashing, where padding bytes must be 0) + expected_packed_bytes.extend_from_slice(&[0u8; 3]); + + assert_eq!( + &packed_bytes, &expected_packed_bytes, + "Packed bytes don't match expected Solidity encoding" + ); - assert_eq!(digest, keccak256_digest); - assert_eq!(hex_digest, keccak256_hex_digest); - assert_eq!(hex_digest, expected_hash); Ok(()) } +#[tokio::test] +async fn get_leaf_value() -> anyhow::Result<()> { + let vector: LeafValueVector = + serde_json::from_str(LEAF_VALUE_VECTORS_JSON).expect("Failed to parse leaf value vector"); + + let leaf_data = vector.to_leaf_data(); + let key: Word = leaf_data.to_commitment(); + let advice_inputs = AdviceInputs::default().with_map(vec![(key, leaf_data.to_elements())]); + + let source = format!( + r#" + use miden::core::mem + use miden::core::sys + use miden::agglayer::crypto_utils + + begin + push.{key} + exec.crypto_utils::get_leaf_value + exec.sys::truncate_stack + end + "# + ); + let agglayer_lib = agglayer_library(); + + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib.clone()) + .unwrap() + .assemble_program(&source) + .unwrap(); + + let exec_output = execute_program_with_default_host(program, Some(advice_inputs)).await?; + let computed_leaf_value: Vec = exec_output.stack[0..8].to_vec(); + let expected_leaf_value_bytes: [u8; 32] = + hex_to_bytes(&vector.leaf_value).expect("valid leaf value hex"); + let expected_leaf_value: Vec = + Keccak256Output::from(expected_leaf_value_bytes).to_elements(); + + assert_eq!(computed_leaf_value, expected_leaf_value); + Ok(()) +} #[tokio::test] async fn test_solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { let merkle_paths = &*SOLIDITY_MERKLE_PROOF_VECTORS; @@ -181,68 +299,3 @@ async fn test_solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> Ok(()) } - -// HELPER FUNCTIONS -// ================================================================================================ - -fn merkle_proof_verification_code( - index: usize, - merkle_paths: &MerkleProofVerificationFile, -) -> String { - // generate the code which stores the merkle path to the memory - let mut store_path_source = String::new(); - for height in 0..32 { - let path_node = - Keccak256Digest::try_from(merkle_paths.merkle_paths[index * 32 + height].as_str()) - .unwrap(); - let (node_hi, node_lo) = keccak_digest_to_word_strings(path_node); - // each iteration (each index in leaf/root vector) we rewrite the merkle path nodes, so the - // memory pointers for the merkle path and the expected root never change - store_path_source.push_str(&format!( - " -\tpush.[{node_hi}] mem_storew_be.{} dropw -\tpush.[{node_lo}] mem_storew_be.{} dropw - ", - height * 8, - height * 8 + 4 - )); - } - - // prepare the root for the provided index - let root = Keccak256Digest::try_from(merkle_paths.roots[index].as_str()).unwrap(); - let (root_hi, root_lo) = keccak_digest_to_word_strings(root); - - // prepare the leaf for the provided index - let leaf = Keccak256Digest::try_from(merkle_paths.leaves[index].as_str()).unwrap(); - let (leaf_hi, leaf_lo) = keccak_digest_to_word_strings(leaf); - - format!( - r#" - use miden::agglayer::crypto_utils - - begin - # store the merkle path to the memory (double word slots from 0 to 248) - {store_path_source} - # => [] - - # store the root to the memory (double word slot 256) - push.[{root_lo}] mem_storew_be.256 dropw - push.[{root_hi}] mem_storew_be.260 dropw - # => [] - - # prepare the stack for the `verify_merkle_proof` procedure - push.256 # expected root memory pointer - push.{index} # provided leaf index - push.0 # Merkle path memory pointer - push.[{leaf_hi}] push.[{leaf_lo}] # provided leaf value - # => [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_idx, expected_root_ptr] - - exec.crypto_utils::verify_merkle_proof - # => [verification_flag] - - assert.err="verification failed" - # => [] - end - "# - ) -} diff --git a/crates/miden-testing/tests/agglayer/global_index.rs b/crates/miden-testing/tests/agglayer/global_index.rs index 92c53ef61e..ad29d767d0 100644 --- a/crates/miden-testing/tests/agglayer/global_index.rs +++ b/crates/miden-testing/tests/agglayer/global_index.rs @@ -2,12 +2,12 @@ extern crate alloc; use alloc::sync::Arc; -use miden_agglayer::agglayer_library; use miden_agglayer::errors::{ ERR_BRIDGE_NOT_MAINNET, ERR_LEADING_BITS_NON_ZERO, ERR_ROLLUP_INDEX_NON_ZERO, }; +use miden_agglayer::{GlobalIndex, agglayer_library}; use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; use miden_processor::Program; @@ -15,8 +15,10 @@ use miden_testing::{ExecError, assert_execution_error}; use crate::agglayer::test_utils::execute_program_with_default_host; -fn assemble_process_global_index_program(global_index_be_u32_limbs: [u32; 8]) -> Program { - let [g0, g1, g2, g3, g4, g5, g6, g7] = global_index_be_u32_limbs; +fn assemble_process_global_index_program(global_index: GlobalIndex) -> Program { + // Convert GlobalIndex to 8 field elements (big-endian: [0]=MSB, [7]=LSB) + let elements = global_index.to_elements(); + let [g0, g1, g2, g3, g4, g5, g6, g7] = elements.try_into().unwrap(); let script_code = format!( r#" @@ -42,10 +44,15 @@ fn assemble_process_global_index_program(global_index_be_u32_limbs: [u32; 8]) -> #[tokio::test] async fn test_process_global_index_mainnet_returns_leaf_index() -> anyhow::Result<()> { - // 256-bit globalIndex encoded as 8 u32 limbs (big-endian): - // [top 191 bits = 0, mainnet flag = 1, rollup_index = 0, leaf_index = 2] - let global_index = [0, 0, 0, 0, 0, 1, 0, 2]; - let program = assemble_process_global_index_program(global_index); + // Global index format (32 bytes, big-endian like Solidity uint256): + // - bytes[0..20]: leading zeros + // - bytes[20..24]: mainnet_flag = 1 (BE u32) + // - bytes[24..28]: rollup_index = 0 (BE u32) + // - bytes[28..32]: leaf_index = 2 (BE u32) + let mut bytes = [0u8; 32]; + bytes[23] = 1; // mainnet flag = 1 (BE: LSB at byte 23) + bytes[31] = 2; // leaf index = 2 (BE: LSB at byte 31) + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let exec_output = execute_program_with_default_host(program, None).await?; @@ -55,8 +62,11 @@ async fn test_process_global_index_mainnet_returns_leaf_index() -> anyhow::Resul #[tokio::test] async fn test_process_global_index_mainnet_rejects_non_zero_leading_bits() { - let global_index = [1, 0, 0, 0, 0, 1, 0, 2]; - let program = assemble_process_global_index_program(global_index); + let mut bytes = [0u8; 32]; + bytes[3] = 1; // non-zero leading bits (BE: LSB of first u32 limb) + bytes[23] = 1; // mainnet flag = 1 + bytes[31] = 2; // leaf index = 2 + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_LEADING_BITS_NON_ZERO); @@ -64,9 +74,10 @@ async fn test_process_global_index_mainnet_rejects_non_zero_leading_bits() { #[tokio::test] async fn test_process_global_index_mainnet_rejects_flag_limb_upper_bits() { - // limb5 is the mainnet flag; only the lowest bit is allowed - let global_index = [0, 0, 0, 0, 0, 3, 0, 2]; - let program = assemble_process_global_index_program(global_index); + let mut bytes = [0u8; 32]; + bytes[23] = 3; // mainnet flag limb = 3 (upper bits set, only lowest bit allowed) + bytes[31] = 2; // leaf index = 2 + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_BRIDGE_NOT_MAINNET); @@ -74,8 +85,11 @@ async fn test_process_global_index_mainnet_rejects_flag_limb_upper_bits() { #[tokio::test] async fn test_process_global_index_mainnet_rejects_non_zero_rollup_index() { - let global_index = [0, 0, 0, 0, 0, 1, 7, 2]; - let program = assemble_process_global_index_program(global_index); + let mut bytes = [0u8; 32]; + bytes[23] = 1; // mainnet flag = 1 + bytes[27] = 7; // rollup index = 7 (BE: LSB at byte 27) + bytes[31] = 2; // leaf index = 2 + let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_ROLLUP_INDEX_NON_ZERO); diff --git a/crates/miden-testing/tests/agglayer/mmr_frontier.rs b/crates/miden-testing/tests/agglayer/mmr_frontier.rs index 367d221cc5..00bb195e76 100644 --- a/crates/miden-testing/tests/agglayer/mmr_frontier.rs +++ b/crates/miden-testing/tests/agglayer/mmr_frontier.rs @@ -1,15 +1,12 @@ use alloc::format; use alloc::string::ToString; -use miden_agglayer::agglayer_library; +use miden_agglayer::claim_note::SmtNode; +use miden_agglayer::{ExitRoot, agglayer_library}; use miden_crypto::hash::keccak::{Keccak256, Keccak256Digest}; use miden_protocol::utils::sync::LazyLock; use miden_standards::code_builder::CodeBuilder; use miden_testing::TransactionContextBuilder; -use serde::Deserialize; - -use super::test_utils::keccak_digest_to_word_strings; - // KECCAK MMR FRONTIER // ================================================================================================ @@ -76,7 +73,8 @@ impl KeccakMmrFrontier32 { async fn test_append_and_update_frontier() -> anyhow::Result<()> { let mut mmr_frontier = KeccakMmrFrontier32::<32>::new(); - let mut source = "use miden::agglayer::mmr_frontier32_keccak begin".to_string(); + let mut source = + "use miden::agglayer::mmr_frontier32_keccak use miden::core::word begin".to_string(); for round in 0..32 { // construct the leaf from the hex representation of the round number @@ -84,7 +82,11 @@ async fn test_append_and_update_frontier() -> anyhow::Result<()> { let root = mmr_frontier.append_and_update_frontier(leaf); let num_leaves = mmr_frontier.num_leaves; - source.push_str(&leaf_assertion_code(leaf, root, num_leaves)); + source.push_str(&leaf_assertion_code( + SmtNode::new(leaf.into()), + ExitRoot::new(root.into()), + num_leaves, + )); } source.push_str("end"); @@ -108,11 +110,16 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { let zero_31 = *CANONICAL_ZEROS_32.get(31).expect("zeros should have 32 values total"); let empty_mmr_root = Keccak256::merge(&[zero_31, zero_31]); - let mut source = "use miden::agglayer::mmr_frontier32_keccak begin".to_string(); + let mut source = + "use miden::agglayer::mmr_frontier32_keccak use miden::core::word begin".to_string(); for round in 1..=32 { // check that pushing the zero leaves into the MMR doesn't change its root - source.push_str(&leaf_assertion_code(zero_leaf, empty_mmr_root, round)); + source.push_str(&leaf_assertion_code( + SmtNode::new(zero_leaf.into()), + ExitRoot::new(empty_mmr_root.into()), + round, + )); } source.push_str("end"); @@ -137,39 +144,7 @@ async fn test_check_empty_mmr_root() -> anyhow::Result<()> { // Test vectors generated from: https://github.com/agglayer/agglayer-contracts // Run `make generate-solidity-test-vectors` to regenerate the test vectors. -/// Canonical zeros JSON embedded at compile time from the Foundry-generated file. -const CANONICAL_ZEROS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json"); - -/// MMR frontier vectors JSON embedded at compile time from the Foundry-generated file. -const MMR_FRONTIER_VECTORS_JSON: &str = - include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); - -/// Deserialized canonical zeros from Solidity DepositContractBase.sol -#[derive(Debug, Deserialize)] -struct CanonicalZerosFile { - canonical_zeros: Vec, -} - -/// Deserialized MMR frontier vectors from Solidity DepositContractBase.sol -/// Uses parallel arrays for leaves, roots, and counts instead of array of objects -#[derive(Debug, Deserialize)] -struct MmrFrontierVectorsFile { - leaves: Vec, - roots: Vec, - counts: Vec, -} - -/// Lazily parsed canonical zeros from the JSON file. -static SOLIDITY_CANONICAL_ZEROS: LazyLock = LazyLock::new(|| { - serde_json::from_str(CANONICAL_ZEROS_JSON).expect("Failed to parse canonical zeros JSON") -}); - -/// Lazily parsed MMR frontier vectors from the JSON file. -static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = LazyLock::new(|| { - serde_json::from_str(MMR_FRONTIER_VECTORS_JSON) - .expect("failed to parse MMR frontier vectors JSON") -}); +use super::test_utils::{SOLIDITY_CANONICAL_ZEROS, SOLIDITY_MMR_FRONTIER_VECTORS}; /// Verifies that the Rust KeccakMmrFrontier32 produces the same canonical zeros as Solidity. #[test] @@ -222,27 +197,28 @@ fn test_solidity_mmr_frontier_compatibility() { // HELPER FUNCTIONS // ================================================================================================ -fn leaf_assertion_code( - leaf: Keccak256Digest, - expected_root: Keccak256Digest, - num_leaves: u32, -) -> String { - let (leaf_hi, leaf_lo) = keccak_digest_to_word_strings(leaf); - let (root_hi, root_lo) = keccak_digest_to_word_strings(expected_root); +fn leaf_assertion_code(leaf: SmtNode, expected_root: ExitRoot, num_leaves: u32) -> String { + let [leaf_lo, leaf_hi] = leaf.to_words(); + let [root_lo, root_hi] = expected_root.to_words(); format!( r#" - # load the provided leaf onto the stack - push.[{leaf_hi}] - push.[{leaf_lo}] + # load the provided leaf onto the stack and reverse (the leaf value would have been + # reversed by the keccak hash call in a real get_leaf_value) + push.{leaf_hi} + exec.word::reverse + push.{leaf_lo} + exec.word::reverse # add this leaf to the MMR frontier exec.mmr_frontier32_keccak::append_and_update_frontier # => [NEW_ROOT_LO, NEW_ROOT_HI, new_leaf_count] # assert the root correctness after the first leaf was added - push.[{root_lo}] - push.[{root_hi}] + push.{root_lo} + exec.word::reverse + push.{root_hi} + exec.word::reverse movdnw.3 # => [EXPECTED_ROOT_LO, NEW_ROOT_LO, NEW_ROOT_HI, EXPECTED_ROOT_HI, new_leaf_count] diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index f96326ffa3..7fbb3d38a0 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,6 +1,7 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; +mod config_bridge; mod crypto_utils; mod global_index; mod mmr_frontier; 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 2083a9dd36..f1bf21ea6d 100644 --- a/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs +++ b/crates/miden-testing/tests/agglayer/solidity_miden_address_conversion.rs @@ -109,7 +109,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { let eth_address = EthAddressFormat::from_account_id(*original_account_id); let address_felts = eth_address.to_elements().to_vec(); - let le: Vec = address_felts + let limbs: Vec = address_felts .iter() .map(|f| { let val = f.as_int(); @@ -118,13 +118,13 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { }) .collect(); - assert_eq!(le[4], 0, "test {}: expected msw limb (le[4]) to be zero", idx); + let limb0 = limbs[0]; + let limb1 = limbs[1]; + let limb2 = limbs[2]; + let limb3 = limbs[3]; + let limb4 = limbs[4]; - let addr0 = le[0]; - let addr1 = le[1]; - let addr2 = le[2]; - let addr3 = le[3]; - let addr4 = le[4]; + assert_eq!(limb0, 0, "test {}: expected msb limb (limb0) to be zero", idx); let account_id_felts: [Felt; 2] = (*original_account_id).into(); let expected_prefix = account_id_felts[0].as_int(); @@ -141,7 +141,7 @@ async fn test_ethereum_address_to_account_id_in_masm() -> anyhow::Result<()> { exec.sys::truncate_stack end "#, - addr4, addr3, addr2, addr1, addr0 + limb4, limb3, limb2, limb1, limb0 ); let program = Assembler::new(Arc::new(DefaultSourceManager::default())) diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index b77d99e1bf..39bc77c917 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -1,27 +1,262 @@ extern crate alloc; +use alloc::string::String; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use miden_agglayer::agglayer_library; +use miden_agglayer::claim_note::{Keccak256Output, ProofData, SmtNode}; +use miden_agglayer::{ + EthAddressFormat, + EthAmount, + ExitRoot, + GlobalIndex, + LeafData, + MetadataHash, + agglayer_library, +}; +use miden_assembly::{Assembler, DefaultSourceManager}; use miden_core_lib::CoreLibrary; -use miden_crypto::hash::keccak::Keccak256Digest; use miden_processor::fast::{ExecutionOutput, FastProcessor}; -use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Felt, Program, StackInputs}; +use miden_processor::{AdviceInputs, DefaultHost, ExecutionError, Program, StackInputs}; use miden_protocol::transaction::TransactionKernel; +use miden_protocol::utils::sync::LazyLock; +use miden_tx::utils::hex_to_bytes; +use serde::Deserialize; -/// Transforms the `[Keccak256Digest]` into two word strings: (`a, b, c, d`, `e, f, g, h`) -pub fn keccak_digest_to_word_strings(digest: Keccak256Digest) -> (String, String) { - let double_word = (*digest) - .chunks(4) - .map(|chunk| Felt::from(u32::from_le_bytes(chunk.try_into().unwrap())).to_string()) - .rev() - .collect::>(); +// EMBEDDED TEST VECTOR JSON FILES +// ================================================================================================ + +/// Claim asset test vectors JSON — contains both LeafData and ProofData from a real claimAsset +/// transaction. +const CLAIM_ASSET_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors.json"); + +/// Leaf data test vectors JSON from the Foundry-generated file. +pub const LEAF_VALUE_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); + +/// Merkle proof verification vectors JSON from the Foundry-generated file. +pub const MERKLE_PROOF_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/merkle_proof_vectors.json"); + +/// Canonical zeros JSON from the Foundry-generated file. +pub const CANONICAL_ZEROS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/canonical_zeros.json"); + +/// MMR frontier vectors JSON from the Foundry-generated file. +pub const MMR_FRONTIER_VECTORS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); + +// SERDE HELPERS +// ================================================================================================ - (double_word[0..4].join(", "), double_word[4..8].join(", ")) +/// Deserializes a JSON value that may be either a number or a string into a `String`. +/// +/// Foundry's `vm.serializeUint` outputs JSON numbers for uint256 values. +/// This deserializer accepts both `"100"` (string) and `100` (number) forms. +fn deserialize_uint_to_string<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::String(s) => Ok(s), + serde_json::Value::Number(n) => Ok(n.to_string()), + _ => Err(serde::de::Error::custom("expected a number or string for amount")), + } } -/// Execute a program with default host and optional advice inputs +/// Deserializes a JSON array of values that may be either numbers or strings into `Vec`. +/// +/// Array-level counterpart of [`deserialize_uint_to_string`]. +fn deserialize_uint_vec_to_strings<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let values = Vec::::deserialize(deserializer)?; + values + .into_iter() + .map(|v| match v { + serde_json::Value::String(s) => Ok(s), + serde_json::Value::Number(n) => Ok(n.to_string()), + _ => Err(serde::de::Error::custom("expected a number or string for amount")), + }) + .collect() +} + +// TEST VECTOR TYPES +// ================================================================================================ + +/// Deserialized leaf value test vector from Solidity-generated JSON. +#[derive(Debug, Deserialize)] +pub struct LeafValueVector { + pub origin_network: u32, + pub origin_token_address: String, + pub destination_network: u32, + pub destination_address: String, + #[serde(deserialize_with = "deserialize_uint_to_string")] + pub amount: String, + pub metadata_hash: String, + #[allow(dead_code)] + pub leaf_value: String, +} + +impl LeafValueVector { + /// Converts this test vector into a `LeafData` instance. + pub fn to_leaf_data(&self) -> LeafData { + LeafData { + origin_network: self.origin_network, + origin_token_address: EthAddressFormat::from_hex(&self.origin_token_address) + .expect("valid origin token address hex"), + destination_network: self.destination_network, + destination_address: EthAddressFormat::from_hex(&self.destination_address) + .expect("valid destination address hex"), + amount: EthAmount::from_uint_str(&self.amount).expect("valid amount uint string"), + metadata_hash: MetadataHash::new( + hex_to_bytes(&self.metadata_hash).expect("valid metadata hash hex"), + ), + } + } +} + +/// Deserialized proof value test vector from Solidity-generated JSON. +/// Contains SMT proofs, exit roots, global index, and expected global exit root. +#[derive(Debug, Deserialize)] +pub struct ProofValueVector { + pub smt_proof_local_exit_root: Vec, + pub smt_proof_rollup_exit_root: Vec, + pub global_index: String, + pub mainnet_exit_root: String, + pub rollup_exit_root: String, + /// Expected global exit root: keccak256(mainnetExitRoot || rollupExitRoot) + #[allow(dead_code)] + pub global_exit_root: String, +} + +impl ProofValueVector { + /// Converts this test vector into a `ProofData` instance. + pub fn to_proof_data(&self) -> ProofData { + let smt_proof_local: [SmtNode; 32] = self + .smt_proof_local_exit_root + .iter() + .map(|s| SmtNode::new(hex_to_bytes(s).expect("valid smt proof hex"))) + .collect::>() + .try_into() + .expect("expected 32 SMT proof nodes for local exit root"); + + let smt_proof_rollup: [SmtNode; 32] = self + .smt_proof_rollup_exit_root + .iter() + .map(|s| SmtNode::new(hex_to_bytes(s).expect("valid smt proof hex"))) + .collect::>() + .try_into() + .expect("expected 32 SMT proof nodes for rollup exit root"); + + ProofData { + smt_proof_local_exit_root: smt_proof_local, + smt_proof_rollup_exit_root: smt_proof_rollup, + global_index: GlobalIndex::from_hex(&self.global_index) + .expect("valid global index hex"), + mainnet_exit_root: Keccak256Output::new( + hex_to_bytes(&self.mainnet_exit_root).expect("valid mainnet exit root hex"), + ), + rollup_exit_root: Keccak256Output::new( + hex_to_bytes(&self.rollup_exit_root).expect("valid rollup exit root hex"), + ), + } + } +} + +/// Deserialized claim asset test vector from Solidity-generated JSON. +/// Contains both LeafData and ProofData from a real claimAsset transaction. +#[derive(Debug, Deserialize)] +pub struct ClaimAssetVector { + #[serde(flatten)] + pub proof: ProofValueVector, + + #[serde(flatten)] + pub leaf: LeafValueVector, +} + +/// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol. +/// Uses parallel arrays for leaves and roots. For each element from leaves/roots there are 32 +/// elements from merkle_paths, which represent the merkle path for that leaf + root. +#[derive(Debug, Deserialize)] +pub struct MerkleProofVerificationFile { + pub leaves: Vec, + pub roots: Vec, + pub merkle_paths: Vec, +} + +/// Deserialized canonical zeros from Solidity DepositContractBase.sol. +#[derive(Debug, Deserialize)] +pub struct CanonicalZerosFile { + pub canonical_zeros: Vec, +} + +/// Deserialized MMR frontier vectors from Solidity DepositContractV2. +/// +/// Each leaf is produced by `getLeafValue` using the same hardcoded fields as `bridge_out.masm` +/// (leafType=0, originNetwork=64, metadataHash=0), parametrised by +/// a shared `origin_token_address`, `amounts[i]`, and per-index +/// `destination_networks[i]` / `destination_addresses[i]`. +/// +/// Amounts are serialized as uint256 values (JSON numbers). +#[derive(Debug, Deserialize)] +pub struct MmrFrontierVectorsFile { + pub leaves: Vec, + pub roots: Vec, + pub counts: Vec, + #[serde(deserialize_with = "deserialize_uint_vec_to_strings")] + pub amounts: Vec, + pub origin_token_address: String, + pub destination_networks: Vec, + pub destination_addresses: Vec, +} + +// LAZY-PARSED TEST VECTORS +// ================================================================================================ + +/// Lazily parsed claim asset test vector from the JSON file. +pub static CLAIM_ASSET_VECTOR: LazyLock = LazyLock::new(|| { + serde_json::from_str(CLAIM_ASSET_VECTORS_JSON) + .expect("failed to parse claim asset vectors JSON") +}); + +/// Lazily parsed Merkle proof vectors from the JSON file. +pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = + LazyLock::new(|| { + serde_json::from_str(MERKLE_PROOF_VECTORS_JSON) + .expect("failed to parse Merkle proof vectors JSON") + }); + +/// Lazily parsed canonical zeros from the JSON file. +pub static SOLIDITY_CANONICAL_ZEROS: LazyLock = LazyLock::new(|| { + serde_json::from_str(CANONICAL_ZEROS_JSON).expect("Failed to parse canonical zeros JSON") +}); + +/// Lazily parsed MMR frontier vectors from the JSON file. +pub static SOLIDITY_MMR_FRONTIER_VECTORS: LazyLock = LazyLock::new(|| { + serde_json::from_str(MMR_FRONTIER_VECTORS_JSON) + .expect("failed to parse MMR frontier vectors JSON") +}); + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Returns real claim data from the claim_asset_vectors.json file. +/// +/// Returns a tuple of (ProofData, LeafData) parsed from the real on-chain claim transaction. +pub fn real_claim_data() -> (ProofData, LeafData, ExitRoot) { + let vector = &*CLAIM_ASSET_VECTOR; + let ger = ExitRoot::new( + hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"), + ); + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger) +} + +/// Execute a program with a default host and optional advice inputs. pub async fn execute_program_with_default_host( program: Program, advice_inputs: Option, @@ -34,7 +269,6 @@ pub async fn execute_program_with_default_host( 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)?; } @@ -49,79 +283,30 @@ pub async fn execute_program_with_default_host( processor.execute(&program, &mut host).await } -// TESTING HELPERS -// ================================================================================================ +/// Execute a MASM script with the default host +pub async fn execute_masm_script(script_code: &str) -> Result { + let agglayer_lib = agglayer_library(); -/// Type alias for the complex return type of claim_note_test_inputs. -/// -/// Contains native types for the new ClaimNoteParams structure: -/// - smt_proof_local_exit_root: `Vec<[u8; 32]>` (256 bytes32 values) -/// - smt_proof_rollup_exit_root: `Vec<[u8; 32]>` (256 bytes32 values) -/// - global_index: [u32; 8] -/// - mainnet_exit_root: [u8; 32] -/// - rollup_exit_root: [u8; 32] -/// - origin_network: u32 -/// - origin_token_address: [u8; 20] -/// - destination_network: u32 -/// - metadata: [u32; 8] -pub type ClaimNoteTestInputs = ( - Vec<[u8; 32]>, - Vec<[u8; 32]>, - [u32; 8], - [u8; 32], - [u8; 32], - u32, - [u8; 20], - u32, - [u32; 8], -); - -/// Returns dummy test inputs for creating CLAIM notes with native types. -/// -/// This is a convenience function for testing that provides realistic dummy data -/// for all the agglayer claimAsset function inputs using native types. -/// -/// # Returns -/// A tuple containing native types for the new ClaimNoteParams structure -pub fn claim_note_test_inputs() -> ClaimNoteTestInputs { - // Create SMT proofs with 32 bytes32 values each (SMT path depth) - let smt_proof_local_exit_root = vec![[0u8; 32]; 32]; - let smt_proof_rollup_exit_root = vec![[0u8; 32]; 32]; - // Global index format: [top 5 limbs = 0, mainnet_flag = 1, rollup_index = 0, leaf_index = 2] - let global_index = [0u32, 0, 0, 0, 0, 1, 0, 2]; - - let mainnet_exit_root: [u8; 32] = [ - 0xe3, 0xd3, 0x3b, 0x7e, 0x1f, 0x64, 0xb4, 0x04, 0x47, 0x2f, 0x53, 0xd1, 0xe4, 0x56, 0xc9, - 0xfa, 0x02, 0x47, 0x03, 0x13, 0x72, 0xa3, 0x08, 0x0f, 0x82, 0xf2, 0x57, 0xa2, 0x60, 0x8a, - 0x63, 0x1f, - ]; - - let rollup_exit_root: [u8; 32] = [ - 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, - 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, - ]; - - let origin_network = 1u32; - - let origin_token_address: [u8; 20] = [ - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xaa, 0xbb, 0xcc, - ]; - - let destination_network = 2u32; - - let metadata: [u32; 8] = [0; 8]; - - ( - smt_proof_local_exit_root, - smt_proof_rollup_exit_root, - global_index, - mainnet_exit_root, - rollup_exit_root, - origin_network, - origin_token_address, - destination_network, - metadata, - ) + let program = Assembler::new(Arc::new(DefaultSourceManager::default())) + .with_dynamic_library(CoreLibrary::default()) + .unwrap() + .with_dynamic_library(agglayer_lib) + .unwrap() + .assemble_program(script_code) + .unwrap(); + + execute_program_with_default_host(program, None).await +} + +/// Helper to assert execution fails with a specific error message +pub async fn assert_execution_fails_with(script_code: &str, expected_error: &str) { + let result = execute_masm_script(script_code).await; + assert!(result.is_err(), "Expected execution to fail but it succeeded"); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains(expected_error), + "Expected error containing '{}', got: {}", + expected_error, + error_msg + ); } diff --git a/crates/miden-testing/tests/agglayer/update_ger.rs b/crates/miden-testing/tests/agglayer/update_ger.rs index 6b2f973ae9..3b66f15dcb 100644 --- a/crates/miden-testing/tests/agglayer/update_ger.rs +++ b/crates/miden-testing/tests/agglayer/update_ger.rs @@ -1,12 +1,52 @@ -use miden_agglayer::{ExitRoot, UpdateGerNote, create_existing_bridge_account}; +extern crate alloc; + +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use miden_agglayer::utils::felts_to_bytes; +use miden_agglayer::{ExitRoot, UpdateGerNote, agglayer_library, create_existing_bridge_account}; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_core_lib::handlers::bytes_to_packed_u32_felts; +use miden_core_lib::handlers::keccak256::KeccakPreimage; +use miden_crypto::hash::rpo::Rpo256 as Hasher; +use miden_crypto::{Felt, FieldElement}; use miden_protocol::Word; use miden_protocol::account::StorageSlotName; use miden_protocol::crypto::rand::FeltRng; use miden_protocol::transaction::OutputNote; +use miden_protocol::utils::sync::LazyLock; use miden_testing::{Auth, MockChain}; +use miden_tx::utils::hex_to_bytes; +use serde::Deserialize; + +use super::test_utils::execute_program_with_default_host; + +// EXIT ROOT TEST VECTORS +// ================================================================================================ +// Test vectors generated from Solidity's GlobalExitRootLib.calculateGlobalExitRoot +// Run `forge test --match-contract ExitRootsTestVectors` to regenerate. + +/// Exit roots JSON embedded at compile time from the Foundry-generated file. +const EXIT_ROOTS_JSON: &str = + include_str!("../../../miden-agglayer/solidity-compat/test-vectors/exit_roots.json"); + +/// Deserialized exit root vectors from Solidity GlobalExitRootLib +#[derive(Debug, Deserialize)] +struct ExitRootsFile { + mainnet_exit_roots: Vec, + rollup_exit_roots: Vec, + global_exit_roots: Vec, +} + +/// Lazily parsed exit root vectors from the JSON file. +static EXIT_ROOTS_VECTORS: LazyLock = LazyLock::new(|| { + serde_json::from_str(EXIT_ROOTS_JSON).expect("Failed to parse exit roots JSON") +}); #[tokio::test] -async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { +async fn update_ger_note_updates_storage() -> anyhow::Result<()> { let mut builder = MockChain::builder(); // CREATE BRIDGE ACCOUNT @@ -42,23 +82,196 @@ async fn test_update_ger_note_updates_storage() -> anyhow::Result<()> { .build()?; let executed_transaction = tx_context.execute().await?; - // VERIFY GER WAS UPDATED IN STORAGE + // VERIFY GER HASH WAS STORED IN MAP // -------------------------------------------------------------------------------------------- let mut updated_bridge_account = bridge_account.clone(); updated_bridge_account.apply_delta(executed_transaction.account_delta())?; - let ger_upper = updated_bridge_account - .storage() - .get_item(&StorageSlotName::new("miden::agglayer::bridge::ger_upper")?) - .unwrap(); - let ger_lower = updated_bridge_account + // Compute the expected GER hash: rpo256::merge(GER_UPPER, GER_LOWER) + let mut ger_lower: [Felt; 4] = ger.to_elements()[0..4].try_into().unwrap(); + let mut ger_upper: [Felt; 4] = ger.to_elements()[4..8].try_into().unwrap(); + // Elements are reversed: rpo256::merge treats stack as if loaded BE from memory + // The following will produce matching hashes: + // Rust + // Hasher::merge(&[a, b, c, d], &[e, f, g, h]) + // MASM + // rpo256::merge(h, g, f, e, d, c, b, a) + ger_lower.reverse(); + ger_upper.reverse(); + + let ger_hash = Hasher::merge(&[ger_upper.into(), ger_lower.into()]); + // Look up the GER hash in the map storage + let ger_storage_slot = StorageSlotName::new("miden::agglayer::bridge::ger")?; + let stored_value = updated_bridge_account .storage() - .get_item(&StorageSlotName::new("miden::agglayer::bridge::ger_lower")?) + .get_map_item(&ger_storage_slot, ger_hash) + .expect("GER hash should be stored in the map"); + + // The stored value should be [GER_KNOWN_FLAG, 0, 0, 0] = [1, 0, 0, 0] + let expected_value: Word = [Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO].into(); + assert_eq!(stored_value, expected_value, "GER hash should map to [1, 0, 0, 0]"); + + Ok(()) +} + +/// Tests compute_ger with known mainnet and rollup exit roots. +/// +/// The GER (Global Exit Root) is computed as keccak256(mainnet_exit_root || rollup_exit_root). +#[tokio::test] +async fn compute_ger() -> anyhow::Result<()> { + let agglayer_lib = agglayer_library(); + let vectors = &*EXIT_ROOTS_VECTORS; + + for i in 0..vectors.mainnet_exit_roots.len() { + let mainnet_exit_root_bytes = + hex_to_bytes(vectors.mainnet_exit_roots[i].as_str()).expect("Invalid hex string"); + let rollup_exit_root_bytes = + hex_to_bytes(vectors.rollup_exit_roots[i].as_str()).expect("Invalid hex string"); + let expected_ger_bytes = + hex_to_bytes(vectors.global_exit_roots[i].as_str()).expect("Invalid hex string"); + + // Convert expected GER to felts for comparison + let expected_ger_exit_root = ExitRoot::from(expected_ger_bytes); + let expected_ger_felts = expected_ger_exit_root.to_elements(); + + // Computed GER using keccak256 + let ger_preimage: Vec = + [mainnet_exit_root_bytes.as_ref(), rollup_exit_root_bytes.as_ref()].concat(); + let ger_preimage = KeccakPreimage::new(ger_preimage); + let computed_ger_felts: Vec = ger_preimage.digest().as_ref().to_vec(); + + assert_eq!( + computed_ger_felts, expected_ger_felts, + "Computed GER mismatch for test vector {}", + i + ); + + // Convert exit roots to packed u32 felts for memory initialization + let mainnet_felts = ExitRoot::from(mainnet_exit_root_bytes).to_elements(); + let rollup_felts = ExitRoot::from(rollup_exit_root_bytes).to_elements(); + + // Build memory initialization: mainnet at ptr 0, rollup at ptr 8 + let mem_init: Vec = mainnet_felts + .iter() + .chain(rollup_felts.iter()) + .enumerate() + .map(|(idx, f)| format!("push.{} mem_store.{}", f.as_int(), idx)) + .collect(); + let mem_init_code = mem_init.join("\n"); + + let source = format!( + r#" + use miden::core::sys + use miden::agglayer::crypto_utils + + begin + # Initialize memory with exit roots + {mem_init_code} + + # Call compute_ger with pointer to exit roots + push.0 + exec.crypto_utils::compute_ger + 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, None).await?; + + let result_digest: Vec = exec_output.stack[0..8].to_vec(); + + assert_eq!(result_digest, expected_ger_felts, "GER mismatch for test vector {}", i); + } + + Ok(()) +} + +/// Tests compute_ger with known mainnet and rollup exit roots. +/// +/// The GER (Global Exit Root) is computed as keccak256(mainnet_exit_root || rollup_exit_root). +#[tokio::test] +async fn test_compute_ger_basic() -> anyhow::Result<()> { + let agglayer_lib = agglayer_library(); + + // Define test exit roots (32 bytes each) + let mainnet_exit_root: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, + ]; + + let rollup_exit_root: [u8; 32] = [ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, + ]; + + // Concatenate the two roots (64 bytes total) + let mut ger_preimage = Vec::with_capacity(64); + ger_preimage.extend_from_slice(&mainnet_exit_root); + ger_preimage.extend_from_slice(&rollup_exit_root); + + // Compute expected GER using keccak256 + let expected_ger_preimage = KeccakPreimage::new(ger_preimage.clone()); + let expected_ger_felts: [Felt; 8] = expected_ger_preimage.digest().as_ref().try_into().unwrap(); + + let ger_bytes: [u8; 32] = felts_to_bytes(&expected_ger_felts).try_into().unwrap(); + + let ger = ExitRoot::from(ger_bytes); + // sanity check + assert_eq!(ger.to_elements(), expected_ger_felts); + + // Convert exit roots to packed u32 felts for memory initialization + let mainnet_felts = bytes_to_packed_u32_felts(&mainnet_exit_root); + let rollup_felts = bytes_to_packed_u32_felts(&rollup_exit_root); + + // Build memory initialization: mainnet at ptr 0, rollup at ptr 8 + let mem_init: Vec = mainnet_felts + .iter() + .chain(rollup_felts.iter()) + .enumerate() + .map(|(i, f)| format!("push.{} mem_store.{}", f.as_int(), i)) + .collect(); + let mem_init_code = mem_init.join("\n"); + + let source = format!( + r#" + use miden::core::sys + use miden::agglayer::crypto_utils + + begin + # Initialize memory with exit roots + {mem_init_code} + + # Call compute_ger with pointer to exit roots + push.0 + exec.crypto_utils::compute_ger + 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 expected_lower: Word = ger.to_elements()[0..4].try_into().unwrap(); - let expected_upper: Word = ger.to_elements()[4..8].try_into().unwrap(); - assert_eq!(ger_upper, expected_upper); - assert_eq!(ger_lower, expected_lower); + + let exec_output = execute_program_with_default_host(program, None).await?; + + let result_digest: Vec = exec_output.stack[0..8].to_vec(); + + assert_eq!(result_digest, expected_ger_felts); Ok(()) }