diff --git a/crates/miden-agglayer/SPEC.md b/crates/miden-agglayer/SPEC.md index 40a8ecbf60..5876aea0ae 100644 --- a/crates/miden-agglayer/SPEC.md +++ b/crates/miden-agglayer/SPEC.md @@ -79,8 +79,8 @@ The `CLAIM` note is consumed by the bridge account: The faucet consumes the `MINT` note, mints the specified amount, and creates a [`P2ID`](#46-p2id-generated) note that delivers the minted assets to the recipient's Miden account. -TODO: Destination network from the leaf data is not validated against Miden's own network -ID ([#2698](https://github.com/0xMiden/protocol/issues/2698)). +Inside `bridge_in::claim`, immediately after proof and leaf data are piped into memory, the bridge asserts the leaf's `destination_network` equals the global MASM constant `MIDEN_NETWORK_ID` in `asm/agglayer/common/constants.masm` (after `swap_u32_bytes` on the LE-packed memory limb). The same value is exposed to Rust as `AggLayerBridge::MIDEN_NETWORK_ID`, matching Solidity test vectors. +This mirrors Solidity `claimAsset` destination-network checks. TODO: The leaf type field is not validated to be `LEAF_TYPE_ASSET` (0) ([#2699](https://github.com/0xMiden/protocol/issues/2699)). @@ -223,12 +223,14 @@ Asserts the note sender matches the GER manager stored in | **Inputs** | `[PROOF_DATA_KEY, LEAF_DATA_KEY, faucet_mint_amount, pad(7)]` on the operand stack; proof data and leaf data in the advice map keyed by `PROOF_DATA_KEY` and `LEAF_DATA_KEY` respectively | | **Outputs** | `[pad(16)]` | | **Context** | Consuming a `CLAIM` note on the bridge account | -| **Panics** | GER not known; global index invalid; Merkle proof verification failed; origin token address not in token registry; claim already spent; amount conversion mismatch | +| **Panics** | Leaf `destination_network` does not match `agglayer::common::constants::MIDEN_NETWORK_ID`; invalid leaf type; GER not known; global index invalid; Merkle proof verification failed; origin token address not in token registry; claim already spent; amount conversion mismatch | Validates a bridge-in claim and creates a MINT note targeting the faucet: 1. Pipes proof data and leaf data from the advice map into memory, verifying preimage - integrity. + integrity, then asserts the leaf's `destination_network` matches the global + `MIDEN_NETWORK_ID` constant (`asm/agglayer/common/constants.masm`) after `swap_u32_bytes` on + the LE-packed limb (same convention as other AggLayer bridge-in u32 felts in memory). 2. Extracts the destination account ID from the leaf data's destination address (via `eth_address::to_account_id`). 3. Validates the Merkle proof via `verify_leaf_bridge`: computes the leaf @@ -267,7 +269,7 @@ Validates a bridge-in claim and creates a MINT note targeting the faucet: | `agglayer::bridge::ger_manager_account_id` | Value | -- | `[0, 0, mgr_suffix, mgr_prefix]` | GER manager account ID for UPDATE_GER note authorization | Initial state: all map slots empty, all value slots `[0, 0, 0, 0]` except -`admin_account_id` and `ger_manager_account_id` which are set at account creation time. +`admin_account_id` and `ger_manager_account_id` (set at account creation time). ### 3.2 Faucet Account Component @@ -495,9 +497,9 @@ The storage is divided into three logical regions: proof data (felts 0-535), lea advice map as two keyed entries (`PROOF_DATA_KEY`, `LEAF_DATA_KEY`). 4. The `miden_claim_amount` is read from memory. 5. `bridge_in::claim` is called with `[PROOF_DATA_KEY, LEAF_DATA_KEY, miden_claim_amount]` - on the stack. The bridge validates the proof, checks the claim nullifier, looks up the - faucet via the token registry, verifies the amount conversion, then builds a MINT - output note targeting the faucet. + on the stack. The bridge asserts the leaf's `destination_network` matches the global + `MIDEN_NETWORK_ID` MASM constant, validates the proof, checks the claim nullifier, looks up the faucet via the token + registry, verifies the amount conversion, then builds a MINT output note targeting the faucet. #### Permissions diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 0364f4a3f0..a8f36cd5b7 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -1,5 +1,6 @@ use agglayer::bridge::bridge_config use agglayer::bridge::leaf_utils +use agglayer::common::constants::MIDEN_NETWORK_ID use agglayer::common::utils use agglayer::common::asset_conversion use agglayer::common::eth_address @@ -34,6 +35,7 @@ const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: prov const ERR_CLAIM_ALREADY_SPENT = "claim note has already been spent" const ERR_SOURCE_BRIDGE_NETWORK_OVERFLOW = "source bridge network overflowed u32" const ERR_INVALID_LEAF_TYPE = "invalid leaf type: only asset claims (leafType=0) are supported" +const ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH = "claim leaf destination network does not match Miden AggLayer network ID" # CONSTANTS # ================================================================================================= @@ -112,26 +114,31 @@ const CLAIM_LEAF_DATA_KEY_MEM_ADDR = 704 const CLAIM_LEAF_INDEX_MEM_ADDR = 900 const CLAIM_SOURCE_BRIDGE_NETWORK_MEM_ADDR = 901 -# Memory addresses for leaf data fields (derived from leaf data layout at CLAIM_LEAF_DATA_START_PTR=536) -const LEAF_TYPE_ADDRESS = 536 -const ORIGIN_TOKEN_ADDRESS_0 = 538 -const ORIGIN_TOKEN_ADDRESS_1 = 539 -const ORIGIN_TOKEN_ADDRESS_2 = 540 -const ORIGIN_TOKEN_ADDRESS_3 = 541 -const ORIGIN_TOKEN_ADDRESS_4 = 542 -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 -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 +# Memory addresses for leaf data fields (felts relative to CLAIM_LEAF_DATA_START_PTR): +const LEAF_TYPE_ADDRESS = CLAIM_LEAF_DATA_START_PTR + +const ORIGIN_TOKEN_ADDRESS_0 = CLAIM_LEAF_DATA_START_PTR + 2 +const ORIGIN_TOKEN_ADDRESS_1 = CLAIM_LEAF_DATA_START_PTR + 3 +const ORIGIN_TOKEN_ADDRESS_2 = CLAIM_LEAF_DATA_START_PTR + 4 +const ORIGIN_TOKEN_ADDRESS_3 = CLAIM_LEAF_DATA_START_PTR + 5 +const ORIGIN_TOKEN_ADDRESS_4 = CLAIM_LEAF_DATA_START_PTR + 6 + +const DESTINATION_NETWORK_ID_MEM_ADDR = CLAIM_LEAF_DATA_START_PTR + 7 + +const DESTINATION_ADDRESS_0 = CLAIM_LEAF_DATA_START_PTR + 8 +const DESTINATION_ADDRESS_1 = CLAIM_LEAF_DATA_START_PTR + 9 +const DESTINATION_ADDRESS_2 = CLAIM_LEAF_DATA_START_PTR + 10 +const DESTINATION_ADDRESS_3 = CLAIM_LEAF_DATA_START_PTR + 11 +const DESTINATION_ADDRESS_4 = CLAIM_LEAF_DATA_START_PTR + 12 + +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_0 = CLAIM_LEAF_DATA_START_PTR + 13 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_1 = CLAIM_LEAF_DATA_START_PTR + 14 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_2 = CLAIM_LEAF_DATA_START_PTR + 15 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_3 = CLAIM_LEAF_DATA_START_PTR + 16 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_4 = CLAIM_LEAF_DATA_START_PTR + 17 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_5 = CLAIM_LEAF_DATA_START_PTR + 18 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_6 = CLAIM_LEAF_DATA_START_PTR + 19 +const OUTPUT_NOTE_ASSET_AMOUNT_MEM_ADDR_7 = CLAIM_LEAF_DATA_START_PTR + 20 # Memory addresses for MINT note output construction const MINT_NOTE_STORAGE_MEM_ADDR_0 = 800 @@ -193,6 +200,7 @@ const CLAIM_DEST_ID_SUFFIX_LOCAL = 1 #! #! Panics if: #! - the leaf type is not 0 (not an asset claim). +#! - the leaf destination network does not match the global `MIDEN_NETWORK_ID` constant. #! - the Merkle proof validation fails. #! - the origin token address is not registered in the bridge's token registry. #! @@ -207,6 +215,10 @@ pub proc claim exec.claim_batch_pipe_double_words # => [pad(16)] + # check that the destination network stored in the leaf data matches the Miden network ID + exec.assert_claim_leaf_destination_network + # => [pad(16)] + exec.load_destination_address exec.eth_address::to_account_id loc_store.CLAIM_DEST_ID_SUFFIX_LOCAL loc_store.CLAIM_DEST_ID_PREFIX_LOCAL @@ -529,7 +541,7 @@ pub proc get_leaf_value(leaf_data_key: word) -> DoubleWord exec.mem::pipe_preimage_to_memory drop # => [] - # compute the leaf value for elements in memory starting at LEAF_DATA_START_PTR + # compute the leaf value from elements in memory starting at LEAF_DATA_START_PTR push.LEAF_DATA_START_PTR exec.leaf_utils::compute_leaf_value # => [LEAF_VALUE[8]] @@ -1131,3 +1143,31 @@ proc store_cgi_chain_hash exec.native_account::set_item dropw # => [] end + +#! Asserts the claim leaf's `destination_network` matches the global `MIDEN_NETWORK_ID`. +#! +#! `claim_batch_pipe_double_words` stores leaf felts as LE-packed u32 limbs. `swap_u32_bytes` +#! converts the loaded limb to the canonical u32 value so it can be compared to `MIDEN_NETWORK_ID` +#! from `agglayer::common::constants`. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the leaf destination network does not match the Miden AggLayer network ID constant. +#! +#! Invocation: exec +proc assert_claim_leaf_destination_network + # load the destination network ID onto the stack + mem_load.DESTINATION_NETWORK_ID_MEM_ADDR + # => [destination_network_id_le] + + # change the endianness to BE to compare it with the Miden network ID + exec.utils::swap_u32_bytes + # => [destination_network_id_be] + + # assert that the destination network ID matches the Miden network ID + push.MIDEN_NETWORK_ID + assert_eq.err=ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH + # => [] +end diff --git a/crates/miden-agglayer/asm/agglayer/common/constants.masm b/crates/miden-agglayer/asm/agglayer/common/constants.masm new file mode 100644 index 0000000000..f618136b35 --- /dev/null +++ b/crates/miden-agglayer/asm/agglayer/common/constants.masm @@ -0,0 +1,10 @@ +# AggLayer-wide numeric constants shared by bridge-in and other modules. +# +# Each `const NAME = ` line is parsed at crate build time into +# `pub const NAME: u32 = value` in `agglayer_constants.rs` (see `build.rs`). + +# NETWORK IDS +# ================================================================================================= + +# AggLayer-assigned network ID for this Miden chain. +const MIDEN_NETWORK_ID = 77 diff --git a/crates/miden-agglayer/build.rs b/crates/miden-agglayer/build.rs index af7d0c2768..e2f6d29db7 100644 --- a/crates/miden-agglayer/build.rs +++ b/crates/miden-agglayer/build.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::env; use std::fmt::Write; use std::path::Path; @@ -17,6 +18,7 @@ use miden_protocol::account::{ use miden_protocol::transaction::TransactionKernel; use miden_standards::account::auth::NoAuth; use miden_standards::account::mint_policies::OwnerControlled; +use regex::Regex; // CONSTANTS // ================================================================================================ @@ -26,6 +28,7 @@ const ASM_DIR: &str = "asm"; const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts"; const ASM_AGGLAYER_DIR: &str = "agglayer"; const ASM_AGGLAYER_BRIDGE_DIR: &str = "agglayer/bridge"; +const ASM_AGGLAYER_CONSTANTS_MASM: &str = "agglayer/common/constants.masm"; const ASM_COMPONENTS_DIR: &str = "components"; const AGGLAYER_ERRORS_RS_FILE: &str = "agglayer_errors.rs"; @@ -80,7 +83,12 @@ fn main() -> Result<()> { // generate agglayer specific constants let constants_out_path = Path::new(&build_dir).join(AGGLAYER_GLOBAL_CONSTANTS_FILE_NAME); - generate_agglayer_constants(constants_out_path, component_libraries)?; + let agglayer_constants_masm_path = crate_path.join(ASM_DIR).join(ASM_AGGLAYER_CONSTANTS_MASM); + generate_agglayer_constants( + constants_out_path, + component_libraries, + &agglayer_constants_masm_path, + )?; generate_error_constants(&source_dir, &build_dir)?; @@ -207,14 +215,77 @@ fn compile_account_components( // GENERATE AGGLAYER CONSTANTS // ================================================================================================ +/// Parses every decimal `u32` constant from `asm/agglayer/common/constants.masm`. +/// +/// Recognized lines (whitespace-flexible, one definition per line, `#` comments ignored by the +/// regex): +/// +/// ```text +/// const SOME_NAME = 123 +/// ``` +/// +/// Each match is emitted to `agglayer_constants.rs` as `pub const SOME_NAME: u32`. +/// Duplicate `const` names in the same file are a build error. Non-decimal values (e.g. `word(...)` +/// or array literals) are not parsed here; add support in this function when needed. +fn parse_numeric_constants_from_constants_masm(masm_path: &Path) -> Result> { + // Read the full `constants.masm` text; parsing is line-based so we need the whole file. + let contents = fs::read_to_string(masm_path) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read {}", masm_path.display()))?; + + // One line per match: optional leading space, `const`, identifier (no leading digit), `=`, + // decimal digits only. `(?m)^` makes `^` match after newlines so we skip comment-only lines. + let re = Regex::new(r"(?m)^\s*const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\d+)\s*$") + .expect("constants.masm parse regex should compile"); + + // `out` preserves declaration order; `seen` rejects duplicate const names in the same file. + let mut out = Vec::new(); + let mut seen = HashSet::new(); + + for caps in re.captures_iter(&contents) { + let name = caps.get(1).expect("group 1").as_str(); + + // Require each identifier at most once so generated Rust names are unique. + if !seen.insert(name.to_string()) { + return Err(Report::msg(format!( + "duplicate `const {name}` in {}", + masm_path.display() + ))); + } + + // Right-hand side must fit `u32` (same range we emit in Rust). + let raw = caps.get(2).expect("group 2").as_str(); + let value = raw.parse::().map_err(|_| { + Report::msg(format!( + "`const {name}` value `{raw}` is not a valid u32 in {}", + masm_path.display() + )) + })?; + + out.push((name.to_string(), value)); + } + + // Empty match set is almost certainly a misconfigured or mistyped `constants.masm`. + if out.is_empty() { + return Err(Report::msg(format!( + "{} does not contain any constants to parse", + masm_path.display() + ))); + } + + Ok(out) +} + /// Generates a Rust file containing AggLayer specific constants. /// -/// At the moment, this file contains the following constants: +/// This file contains: +/// - All the constants listed in the `constants.masm` file. /// - AggLayer Bridge code commitment. /// - AggLayer Faucet code commitment. fn generate_agglayer_constants( target_file: impl AsRef, component_libraries: Vec<(String, Library)>, + constants_masm_path: &Path, ) -> Result<()> { let mut file_contents = String::new(); @@ -232,6 +303,11 @@ fn generate_agglayer_constants( ) .unwrap(); + let masm_constants = parse_numeric_constants_from_constants_masm(constants_masm_path)?; + for (name, value) in &masm_constants { + writeln!(file_contents, "pub const {name}: u32 = {value};\n").unwrap(); + } + // Create a dummy metadata to be able to create components. We only interested in the resulting // code commitment, so it doesn't matter what does this metadata holds. let dummy_metadata = AccountComponentMetadata::new("dummy", AccountType::all()); diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_mainnet_tx.json similarity index 92% rename from crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json rename to crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_mainnet_tx.json index f21309609a..aa7f9e3149 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_mainnet_tx.json @@ -1,16 +1,16 @@ { "amount": "100000000000000000000", - "claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d", + "claimed_global_index_hash_chain": "0xaaa935b34029c1f61586245a57893b461fd7b25fde618c4fd64accb92a8b42f1", "deposit_count": 1, "description": "L1 bridgeAsset transaction test vectors with valid Merkle proofs", "destination_address": "0x00000000AA0000000000bb000000cc000000Dd00", - "destination_network": 20, - "global_exit_root": "0xc84f1e3744c151b345a8899034b3677c0fdbaf45aa3aaf18a3f97dbcf70836cb", + "destination_network": 77, + "global_exit_root": "0x1e79b6d6b557b9404242f0118ca5bbef4b2017209abf978628b456e684985b10", "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", "leaf_type": 0, - "leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d", - "local_exit_root": "0xc9e095ea4cfe19b7e9a6d1aff6c55914ccc8df34954f9f6a2ad8e42d2632a0ab", - "mainnet_exit_root": "0xc9e095ea4cfe19b7e9a6d1aff6c55914ccc8df34954f9f6a2ad8e42d2632a0ab", + "leaf_value": "0x0d16f063db3b2ea0c1ff3f41538b644820f4eb718aafc57006823bf0ccbabc81", + "local_exit_root": "0xe6fc31195b37bad51b5e013b283bb9e6eba87719f2d8df0c9524d25a1765ce44", + "mainnet_exit_root": "0xe6fc31195b37bad51b5e013b283bb9e6eba87719f2d8df0c9524d25a1765ce44", "metadata": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a5465737420546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045445535400000000000000000000000000000000000000000000000000000000", "metadata_hash": "0x4d0d9fb7f9ab2f012da088dc1c228173723db7e09147fe4fea2657849d580161", "origin_network": 0, diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_real_tx.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_real_tx.json deleted file mode 100644 index e4423417fb..0000000000 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_real_tx.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "amount": 100000000000000, - "claimed_global_index_hash_chain": "0xd2bb2f0231ee9ea0c88e89049bea6dbcf7dd96a1015ca9e66ab38ef3c8dc928e", - "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/claim_asset_vectors_rollup_tx.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json index aafa61f593..f43ec295b8 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json @@ -1,21 +1,21 @@ { "amount": "100000000000000000000", - "claimed_global_index_hash_chain": "0x68ace2f015593d5f6de5338c9eca6e748764574491b9f0eed941a2b49db1a7a3", + "claimed_global_index_hash_chain": "0x472d40a02f9f29c5c85c76382909597184aa2232cbbc0475faf38246c3b2bf62", "deposit_count": 3, "description": "Rollup deposit test vectors with valid two-level Merkle proofs (non-zero indices)", "destination_address": "0x00000000AA0000000000bb000000cc000000Dd00", - "destination_network": 20, - "global_exit_root": "0x677d4ecba0ff4871f33163e70ea39a13fe97f2fa9b4dbad110e398830a324159", + "destination_network": 77, + "global_exit_root": "0xad65472b61e1cc42672b0dcd555f492d9e5936c64f4afe94896d6c3124f7b83a", "global_index": "0x0000000000000000000000000000000000000000000000000000000500000002", "leaf_type": 0, - "leaf_value": "0x4a6a047a2b89dd9c557395833c5e34c4f72e6f9aae70779e856f14a6a2827585", - "local_exit_root": "0x985cff7ee35794b30fba700b64546b4ec240d2d78aaf356d56e83d907009367f", + "leaf_value": "0x0b573bc7d7512b93b3efb564bff56ed142df7866fca929198427978343935a10", + "local_exit_root": "0x87bcee2155771e2a37752949c3463544acf7254da5ca2c30e526f1eec7606f30", "mainnet_exit_root": "0x4d63440b08ffffe5a049aae4161d54821a09973965a1a1728534a0f117b6d6c9", "metadata": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a5465737420546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045445535400000000000000000000000000000000000000000000000000000000", "metadata_hash": "0x4d0d9fb7f9ab2f012da088dc1c228173723db7e09147fe4fea2657849d580161", "origin_network": 3, "origin_token_address": "0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF", - "rollup_exit_root": "0x91105681934ca0791f4e760fb1f702050d81e4b7c866d42f540710999c90ea97", + "rollup_exit_root": "0x824f001ffe45a6b15dc0b3124142676ecf5b896ab08af6e76e87bf94b4804e38", "smt_proof_local_exit_root": [ "0x0000000000000000000000000000000000000000000000000000000000000000", "0xa8367b4263332f7e5453faa770f07ef4ce3e74fc411e0a788a98b38b91fd3b3e", diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsMainnetTx.t.sol similarity index 93% rename from crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol rename to crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsMainnetTx.t.sol index 94abe9ae54..fc234ac87c 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsMainnetTx.t.sol @@ -6,7 +6,7 @@ import "@agglayer/lib/GlobalExitRootLib.sol"; import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.sol"; import "./DepositContractTestHelpers.sol"; -contract MockGlobalExitRootManagerLocal is IBasePolygonZkEVMGlobalExitRoot { +contract MockGlobalExitRootManagerMainnet is IBasePolygonZkEVMGlobalExitRoot { mapping(bytes32 => uint256) public override globalExitRootMap; function updateExitRoot(bytes32) external override {} @@ -17,24 +17,24 @@ contract MockGlobalExitRootManagerLocal is IBasePolygonZkEVMGlobalExitRoot { } /** - * @title ClaimAssetTestVectorsLocalTx + * @title ClaimAssetTestVectorsMainnetTx * @notice Test contract that generates test vectors for an L1 bridgeAsset transaction. * This simulates calling bridgeAsset() on the PolygonZkEVMBridgeV2 contract * and captures all relevant data including VALID Merkle proofs. * Uses BridgeL2SovereignChain to get the authoritative claimedGlobalIndexHashChain. * - * Run with: forge test -vv --match-contract ClaimAssetTestVectorsLocalTx + * Run with: forge test -vv --match-contract ClaimAssetTestVectorsMainnetTx * * The output can be used to verify Miden's ability to process L1 bridge transactions. */ -contract ClaimAssetTestVectorsLocalTx is Test, DepositContractTestHelpers { +contract ClaimAssetTestVectorsMainnetTx is Test, DepositContractTestHelpers { /** * @notice Generates bridge asset test vectors with VALID Merkle proofs. * Simulates a user calling bridgeAsset() to bridge tokens from L1 to Miden. * * Output file: test-vectors/bridge_asset_vectors.json */ - function test_generateClaimAssetVectorsLocalTx() public { + function test_generateClaimAssetVectorsMainnetTx() public { string memory obj = "root"; // ====== BRIDGE TRANSACTION PARAMETERS ====== @@ -42,7 +42,7 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractTestHelpers { uint8 leafType = 0; uint32 originNetwork = 0; address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; - uint32 destinationNetwork = 20; + uint32 destinationNetwork = 77; address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; uint256 amount = 100000000000000000000; @@ -110,7 +110,7 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractTestHelpers { // Use the actual BridgeL2SovereignChain to compute the authoritative value // Set up the global exit root manager - MockGlobalExitRootManagerLocal gerManager = new MockGlobalExitRootManagerLocal(); + MockGlobalExitRootManagerMainnet gerManager = new MockGlobalExitRootManagerMainnet(); gerManager.setGlobalExitRoot(globalExitRoot); globalExitRootManager = IBasePolygonZkEVMGlobalExitRoot(address(gerManager)); @@ -166,10 +166,10 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractTestHelpers { obj, "description", "L1 bridgeAsset transaction test vectors with valid Merkle proofs" ); - string memory outputPath = "test-vectors/claim_asset_vectors_local_tx.json"; + string memory outputPath = "test-vectors/claim_asset_vectors_mainnet_tx.json"; vm.writeJson(json, outputPath); - console.log("Generated claim asset local tx test vectors with valid Merkle proofs"); + console.log("Generated claim asset mainnet tx test vectors with valid Merkle proofs"); console.log("Output file:", outputPath); console.log("Leaf index:", leafIndex); console.log("Deposit count:", depositCountValue); diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol deleted file mode 100644 index 8674e6cb06..0000000000 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "forge-std/Test.sol"; -import "@agglayer/lib/GlobalExitRootLib.sol"; -import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.sol"; -import "./DepositContractTestHelpers.sol"; - -contract MockGlobalExitRootManagerReal is IBasePolygonZkEVMGlobalExitRoot { - mapping(bytes32 => uint256) public override globalExitRootMap; - - function updateExitRoot(bytes32) external override {} - - function setGlobalExitRoot(bytes32 globalExitRoot) external { - globalExitRootMap[globalExitRoot] = block.number; - } -} - -/** - * @title ClaimAssetTestVectorsRealTx - * @notice Test contract that generates comprehensive test vectors for verifying - * compatibility between Solidity's claimAsset and Miden's implementation. - * Uses BridgeL2SovereignChain to get the authoritative claimedGlobalIndexHashChain. - * - * Generates vectors for both LeafData and ProofData from a real transaction. - * - * Run with: forge test -vv --match-contract ClaimAssetTestVectorsRealTx - * - * The output can be compared against the Rust ClaimNoteStorage implementation. - */ -contract ClaimAssetTestVectorsRealTx is Test, DepositContractTestHelpers { - /** - * @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 ====== - bytes32[32] memory smtProofLocalExitRoot; - bytes32[32] memory smtProofRollupExitRoot; - uint256 globalIndex; - bytes32 mainnetExitRoot; - bytes32 rollupExitRoot; - bytes32 globalExitRoot; - - // Scoped block keeps stack usage under Solidity limits. - { - // SMT proof for local exit root (32 nodes) - 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) - ]; - - // SMT proof for rollup exit root (32 nodes - all zeros for this rollup claim). - for (uint256 i = 0; i < 32; i++) { - smtProofRollupExitRoot[i] = bytes32(0); - } - - // Global index (uint256) - encodes rollup_id and deposit_count. - globalIndex = 18446744073709788808; - - // Exit roots - mainnetExitRoot = 0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c; - rollupExitRoot = 0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858; - - // Compute global exit root: keccak256(mainnetExitRoot || rollupExitRoot) - globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoot, rollupExitRoot); - - // forge-std JSON serialization supports `bytes32[]` but not `bytes32[32]`. - bytes32[] memory smtProofLocalExitRootDyn = new bytes32[](32); - bytes32[] memory smtProofRollupExitRootDyn = new bytes32[](32); - for (uint256 i = 0; i < 32; i++) { - smtProofLocalExitRootDyn[i] = smtProofLocalExitRoot[i]; - smtProofRollupExitRootDyn[i] = smtProofRollupExitRoot[i]; - } - - 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 - ); - - // ====== COMPUTE CLAIMED GLOBAL INDEX HASH CHAIN ====== - // Use the actual BridgeL2SovereignChain to compute the authoritative value - - // Set up the global exit root manager - MockGlobalExitRootManagerReal gerManager = new MockGlobalExitRootManagerReal(); - gerManager.setGlobalExitRoot(globalExitRoot); - globalExitRootManager = IBasePolygonZkEVMGlobalExitRoot(address(gerManager)); - - // Use a non-zero network ID to match sovereign-chain requirements - networkID = 10; - - // Call _verifyLeafBridge to update claimedGlobalIndexHashChain - this.verifyLeafBridgeHarness( - smtProofLocalExitRoot, - smtProofRollupExitRoot, - globalIndex, - mainnetExitRoot, - rollupExitRoot, - leafType, - originNetwork, - originTokenAddress, - destinationNetwork, - destinationAddress, - amount, - metadataHash - ); - - // Read the updated claimedGlobalIndexHashChain - bytes32 claimedHashChain = claimedGlobalIndexHashChain; - - 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); - vm.serializeBytes32(obj, "leaf_value", leafValue); - string memory json = vm.serializeBytes32(obj, "claimed_global_index_hash_chain", claimedHashChain); - - // Save to file - string memory outputPath = "test-vectors/claim_asset_vectors_real_tx.json"; - vm.writeJson(json, outputPath); - } - } -} diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol index edc867f29d..a27917a848 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol @@ -66,7 +66,7 @@ contract ClaimAssetTestVectorsRollupTx is Test, DepositContractV2, DepositContra uint8 leafType = 0; uint32 originNetwork = 3; // rollup network ID address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; - uint32 destinationNetwork = 20; + uint32 destinationNetwork = 77; // Destination address with zero MSB (embeds a Miden AccountId) address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; uint256 amount = 100000000000000000000; diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 2ec155232b..996c863992 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -136,6 +136,10 @@ static LET_NUM_LEAVES_SLOT_NAME: LazyLock = LazyLock::new(|| { /// /// The bridge starts with an empty faucet registry; faucets are registered at runtime via /// CONFIG_AGG_BRIDGE notes. +/// +/// Claim validation compares the leaf's `destination_network` to the global MASM constant +/// `agglayer::common::constants::MIDEN_NETWORK_ID`. Rust exposes the same value as +/// [`Self::MIDEN_NETWORK_ID`] from generated `agglayer_constants.rs` file. #[derive(Debug, Clone)] pub struct AggLayerBridge { bridge_admin_id: AccountId, @@ -146,6 +150,11 @@ impl AggLayerBridge { // CONSTANTS // -------------------------------------------------------------------------------------------- + /// AggLayer-assigned network ID for this Miden chain. + /// + /// Matches `const MIDEN_NETWORK_ID` in `asm/agglayer/common/constants.masm`. + pub const MIDEN_NETWORK_ID: u32 = MIDEN_NETWORK_ID; + const REGISTERED_GER_MAP_VALUE: Word = Word::new([ONE, ZERO, ZERO, ZERO]); // CONSTRUCTORS diff --git a/crates/miden-agglayer/src/eth_types/metadata_hash.rs b/crates/miden-agglayer/src/eth_types/metadata_hash.rs index d0f02d83fa..3a1df866a0 100644 --- a/crates/miden-agglayer/src/eth_types/metadata_hash.rs +++ b/crates/miden-agglayer/src/eth_types/metadata_hash.rs @@ -92,7 +92,7 @@ mod tests { use super::*; - /// Partial deserialization of claim_asset_vectors_local_tx.json + /// Partial deserialization of claim_asset_vectors_mainnet_tx.json #[derive(Deserialize)] struct ClaimAssetVectors { metadata: std::string::String, @@ -101,7 +101,7 @@ mod tests { fn load_test_vectors() -> ClaimAssetVectors { let path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("solidity-compat/test-vectors/claim_asset_vectors_local_tx.json"); + .join("solidity-compat/test-vectors/claim_asset_vectors_mainnet_tx.json"); let data = std::fs::read_to_string(path).expect("failed to read test vectors"); serde_json::from_str(&data).expect("failed to parse test vectors") } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index e1b7526844..8d7123d90c 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -4,8 +4,12 @@ use alloc::slice; use alloc::string::String; use anyhow::Context; -use miden_agglayer::errors::ERR_CLAIM_ALREADY_SPENT; +use miden_agglayer::errors::{ + ERR_CLAIM_ALREADY_SPENT, + ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH, +}; use miden_agglayer::{ + AggLayerBridge, ClaimNoteStorage, ConfigAggBridgeNote, EthEmbeddedAccountId, @@ -33,7 +37,13 @@ use miden_standards::note::P2idNote; use miden_standards::testing::account_component::IncrNonceAuthComponent; use miden_standards::testing::mock_account::MockAccountExt; use miden_testing::utils::create_p2id_note_exact; -use miden_testing::{AccountState, Auth, MockChain, TransactionContextBuilder}; +use miden_testing::{ + AccountState, + Auth, + MockChain, + TransactionContextBuilder, + assert_transaction_executor_error, +}; use miden_tx::utils::hex_to_bytes; use rand::Rng; @@ -100,24 +110,19 @@ fn merkle_proof_verification_code( /// TX1: UPDATE_GER → bridge (stores GER) /// TX2: CLAIM → bridge (validates proof, creates MINT note) /// TX3: MINT → aggfaucet (mints asset, creates P2ID note) -/// TX4: P2ID → destination (simulated case only) +/// TX4: P2ID → destination (runs for both `Mainnet` and `Rollup` simulated cases) /// /// Parameterized over two claim data sources: -/// - [`ClaimDataSource::Real`]: uses real [`ProofData`] and [`LeafData`] from -/// `claim_asset_vectors_real_tx.json`, captured from an actual on-chain `claimAsset` transaction. -/// - [`ClaimDataSource::Simulated`]: uses locally generated [`ProofData`] and [`LeafData`] from -/// `claim_asset_vectors_local_tx.json`, produced by simulating a `bridgeAsset()` call. -/// -/// Note: Modifying anything in the real test vectors would invalidate the Merkle proof, -/// as the proof was computed for the original leaf data including the original destination. +/// - [`ClaimDataSource::Mainnet`]: uses locally generated [`ProofData`] and [`LeafData`] from +/// `claim_asset_vectors_mainnet_tx.json`, produced by simulating a `bridgeAsset()` call. +/// - [`ClaimDataSource::Rollup`]: uses locally generated [`ProofData`] and [`LeafData`] from +/// `claim_asset_vectors_rollup_tx.json`, produced by simulating the rollup exit tree from +/// PolygonRollupManager. #[rstest::rstest] -#[case::real(ClaimDataSource::Real)] -#[case::simulated(ClaimDataSource::Simulated)] +#[case::mainnet(ClaimDataSource::Mainnet)] #[case::rollup(ClaimDataSource::Rollup)] #[tokio::test] async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { - use miden_agglayer::AggLayerBridge; - let mut builder = MockChain::builder(); // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) @@ -142,6 +147,11 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a // GET CLAIM DATA FROM JSON (source depends on the test case) // -------------------------------------------------------------------------------------------- let (proof_data, leaf_data, ger, cgi_chain_hash) = data_source.get_data(); + assert_eq!( + leaf_data.destination_network, + AggLayerBridge::MIDEN_NETWORK_ID, + "test vectors must target Miden as destination (MIDEN_NETWORK_ID)" + ); // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) // Use the origin token address and network from the claim data. @@ -176,10 +186,11 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a .expect("destination address is not an embedded Miden AccountId") .into_account_id(); - // For the simulated/rollup case, create the destination account so we can consume the P2ID note + // For mainnet and rollup fixtures, create the destination account so we can consume the P2ID + // note. let destination_account = if matches!( data_source, - ClaimDataSource::Simulated | ClaimDataSource::Rollup + ClaimDataSource::Mainnet | ClaimDataSource::Rollup ) { let dest = Account::mock(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, IncrNonceAuthComponent); @@ -370,9 +381,9 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a assert_eq!(RawOutputNote::Full(expected_output_p2id_note.clone()), *output_note); - // TX4: CONSUME THE P2ID NOTE WITH THE DESTINATION ACCOUNT (simulated case only) + // TX4: CONSUME THE P2ID NOTE WITH THE DESTINATION ACCOUNT (mainnet and rollup fixtures) // -------------------------------------------------------------------------------------------- - // For the simulated case, we control the destination account and can verify the full + // The harness owns the destination account from the JSON vectors, so we can verify the full // end-to-end flow including P2ID consumption and balance updates. if let Some(destination_account) = destination_account { // Add the faucet transaction to the chain and prove the next block so the P2ID note is @@ -404,6 +415,138 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a Ok(()) } +/// CLAIM must reject a leaf whose `destination_network` does not match the global Miden +/// AggLayer network ID (`MIDEN_NETWORK_ID` in `constants.masm`), even when the rest of the proof +/// data is unchanged. +#[tokio::test] +async fn test_claim_rejects_wrong_destination_network() -> anyhow::Result<()> { + let data_source = ClaimDataSource::Mainnet; + let mut builder = MockChain::builder(); + + // CREATE BRIDGE ADMIN ACCOUNT (sends CONFIG_AGG_BRIDGE notes) + // -------------------------------------------------------------------------------------------- + let bridge_admin = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE GER MANAGER ACCOUNT (sends the UPDATE_GER note) + // -------------------------------------------------------------------------------------------- + let ger_manager = builder.add_existing_wallet(Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + })?; + + // CREATE BRIDGE ACCOUNT + // -------------------------------------------------------------------------------------------- + let bridge_seed = builder.rng_mut().draw_word(); + let bridge_account = + create_existing_bridge_account(bridge_seed, bridge_admin.id(), ger_manager.id()); + builder.add_account(bridge_account.clone())?; + + // GET CLAIM DATA FROM JSON + // -------------------------------------------------------------------------------------------- + let (proof_data, mut leaf_data, ger, _cgi) = data_source.get_data(); + + // Override destination_network so it no longer matches the bridge's MIDEN_NETWORK_ID. + // Proof data is unchanged; the bridge should fail before Merkle verification. + // -------------------------------------------------------------------------------------------- + leaf_data.destination_network = AggLayerBridge::MIDEN_NETWORK_ID.saturating_add(1); + + // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) + // Use the origin token address and network from the claim data. + // -------------------------------------------------------------------------------------------- + let token_symbol = "AGG"; + let decimals = 8u8; + let max_supply = Felt::new(FungibleAsset::MAX_AMOUNT); + let agglayer_faucet_seed = builder.rng_mut().draw_word(); + let origin_token_address = leaf_data.origin_token_address; + let origin_network = leaf_data.origin_network; + let scale = 10u8; + + 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, + leaf_data.metadata_hash, + ); + builder.add_account(agglayer_faucet.clone())?; + + // Calculate the scaled-down Miden amount + // -------------------------------------------------------------------------------------------- + let miden_claim_amount = leaf_data + .amount + .scale_to_token_amount(scale as u32) + .expect("amount should scale successfully"); + + // CREATE CLAIM NOTE (targets the bridge) + // -------------------------------------------------------------------------------------------- + let claim_note = create_claim_note( + ClaimNoteStorage { + proof_data, + leaf_data, + miden_claim_amount, + }, + bridge_account.id(), + bridge_admin.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(claim_note.clone())); + + // CREATE CONFIG_AGG_BRIDGE NOTE (registers faucet + token address in bridge) + // -------------------------------------------------------------------------------------------- + let config_note = ConfigAggBridgeNote::create( + agglayer_faucet.id(), + &origin_token_address, + bridge_admin.id(), + bridge_account.id(), + builder.rng_mut(), + )?; + builder.add_output_note(RawOutputNote::Full(config_note.clone())); + + // CREATE UPDATE_GER NOTE WITH GLOBAL EXIT ROOT + // -------------------------------------------------------------------------------------------- + let update_ger_note = + UpdateGerNote::create(ger, ger_manager.id(), bridge_account.id(), builder.rng_mut())?; + builder.add_output_note(RawOutputNote::Full(update_ger_note.clone())); + + // BUILD MOCK CHAIN WITH ALL ACCOUNTS + // -------------------------------------------------------------------------------------------- + let mut mock_chain = builder.clone().build()?; + + // TX0: EXECUTE CONFIG_AGG_BRIDGE NOTE TO REGISTER FAUCET IN BRIDGE + // -------------------------------------------------------------------------------------------- + let config_tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[config_note.id()], &[])? + .build()?; + mock_chain.add_pending_executed_transaction(&config_tx_context.execute().await?)?; + mock_chain.prove_next_block()?; + + // TX1: EXECUTE UPDATE_GER NOTE TO STORE GER IN BRIDGE ACCOUNT + // -------------------------------------------------------------------------------------------- + let update_ger_tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[update_ger_note.id()], &[])? + .build()?; + mock_chain.add_pending_executed_transaction(&update_ger_tx_context.execute().await?)?; + mock_chain.prove_next_block()?; + + // TX2: EXECUTE CLAIM NOTE AGAINST BRIDGE (must fail: wrong destination_network) + // -------------------------------------------------------------------------------------------- + let faucet_foreign_inputs = mock_chain.get_foreign_account_inputs(agglayer_faucet.id())?; + let claim_tx_context = mock_chain + .build_tx_context(bridge_account.id(), &[], &[claim_note])? + .foreign_accounts(vec![faucet_foreign_inputs]) + .build()?; + let result = claim_tx_context.execute().await; + assert_transaction_executor_error!(result, ERR_CLAIM_LEAF_DESTINATION_NETWORK_MISMATCH); + + Ok(()) +} + /// Tests that consuming a CLAIM note with the same PROOF_DATA_KEY twice fails. /// /// This test verifies the nullifier tracking mechanism: @@ -414,7 +557,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a /// been spent" #[tokio::test] async fn test_duplicate_claim_note_rejected() -> anyhow::Result<()> { - let data_source = ClaimDataSource::Simulated; + let data_source = ClaimDataSource::Mainnet; let mut builder = MockChain::builder(); // CREATE BRIDGE ADMIN ACCOUNT diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 69506f7a23..a00ab17867 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -34,15 +34,10 @@ use serde::Deserialize; // 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_real_tx.json" -); - -/// Bridge asset test vectors JSON — contains test data for an L1 bridgeAsset transaction. -const BRIDGE_ASSET_VECTORS_JSON: &str = include_str!( - "../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json" +/// Mainnet-side claim asset test vectors JSON — contains test data for an L1 `bridgeAsset` leaf and +/// proof. +const MAINNET_CLAIM_ASSET_VECTORS_JSON: &str = include_str!( + "../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_mainnet_tx.json" ); /// Rollup deposit test vectors JSON — contains test data for a rollup deposit with two-level @@ -244,16 +239,10 @@ pub struct MTFVectorsFile { // 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 bridge asset test vector from the JSON file (locally simulated L1 transaction). -pub static CLAIM_ASSET_VECTOR_LOCAL: LazyLock = LazyLock::new(|| { - serde_json::from_str(BRIDGE_ASSET_VECTORS_JSON) - .expect("failed to parse bridge asset vectors JSON") +/// Lazily parsed mainnet-side bridge asset test vector (`claim_asset_vectors_mainnet_tx.json`). +pub static CLAIM_ASSET_VECTOR_MAINNET: LazyLock = LazyLock::new(|| { + serde_json::from_str(MAINNET_CLAIM_ASSET_VECTORS_JSON) + .expect("failed to parse mainnet claim asset vectors JSON") }); /// Lazily parsed rollup deposit test vector from the JSON file. @@ -282,14 +271,12 @@ pub static SOLIDITY_MTF_VECTORS: LazyLock = LazyLock::new(|| { // HELPER FUNCTIONS // ================================================================================================ -/// Identifies the source of claim data used in bridge-in tests. +/// Identifies the source of simulated claim data used in bridge-in tests. #[derive(Debug, Clone, Copy)] pub enum ClaimDataSource { - /// Real on-chain claimAsset data from claim_asset_vectors_real_tx.json. - Real, - /// Locally simulated bridgeAsset data from claim_asset_vectors_local_tx.json. - Simulated, - /// Rollup deposit data from claim_asset_vectors_rollup_tx.json. + /// Mainnet `bridgeAsset` data from `claim_asset_vectors_mainnet_tx.json`. + Mainnet, + /// Rollup deposit data from `claim_asset_vectors_rollup_tx.json`. Rollup, } @@ -297,8 +284,7 @@ impl ClaimDataSource { /// Returns the `(ProofData, LeafData, ExitRoot, CgiChainHash)` tuple for this data source. pub fn get_data(self) -> (ProofData, LeafData, ExitRoot, CgiChainHash) { let vector = match self { - ClaimDataSource::Real => &*CLAIM_ASSET_VECTOR, - ClaimDataSource::Simulated => &*CLAIM_ASSET_VECTOR_LOCAL, + ClaimDataSource::Mainnet => &*CLAIM_ASSET_VECTOR_MAINNET, ClaimDataSource::Rollup => &*CLAIM_ASSET_VECTOR_ROLLUP, }; let ger = ExitRoot::new(