diff --git a/.gitmodules b/.gitmodules index 0333b7b402..ee3e6490f2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,11 @@ path = crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git branch = release-v4.9 +[submodule "crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts"] + path = crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts.git + branch = release-v5.0 +[submodule "crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable5"] + path = crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable5 + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git + branch = release-v5.0 diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index d56f85e31e..3f86fee62b 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -14,6 +14,8 @@ use miden::protocol::output_note::ATTACHMENT_KIND_NONE use miden::protocol::tx use miden::standards::note_tag use miden::standards::attachments::network_account_target +use miden::protocol::native_account +use miden::protocol::active_account use miden::standards::note::execution_hint::ALWAYS # TYPE ALIASES @@ -35,6 +37,14 @@ const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" # CONSTANTS # ================================================================================================= +# Storage slots +# ------------------------------------------------------------------------------------------------- + +# Storage slot constants for the CGI (claimed global index) chain hash. +# It is stored in two separate value slots. +const CGI_CHAIN_HASH_LO_SLOT_NAME = word("miden::agglayer::bridge::cgi_chain_hash_lo") +const CGI_CHAIN_HASH_HI_SLOT_NAME = word("miden::agglayer::bridge::cgi_chain_hash_hi") + # Data sizes # ------------------------------------------------------------------------------------------------- @@ -163,6 +173,7 @@ const CLAIM_DEST_ID_SUFFIX_LOCAL = 1 #! - the Merkle proof for the provided leaf-index tuple against the computed GER is invalid. #! #! Invocation: call +@locals(8) # leaf value pub proc verify_leaf_bridge # get the leaf value. We have all the necessary leaf data in the advice map exec.get_leaf_value @@ -171,9 +182,23 @@ pub proc verify_leaf_bridge movupw.3 dropw # => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(4)] + # save the leaf value to the local memory to reuse it during the computation of the CGI chain + # hash + loc_storew_le.0 swapw + loc_storew_le.4 swapw + # => [LEAF_VALUE[8], PROOF_DATA_KEY, pad(4)] + # delegate proof verification exec.verify_leaf # => [pad(16)] + + # load the pointer to the leaf value back to the stack + locaddr.0 + # => [leaf_value_ptr, pad(16)] + + # update the CGI chain hash + exec.update_cgi_chain_hash + # => [pad(16)] end #! Assert the global index is valid for a mainnet deposit. @@ -473,8 +498,7 @@ proc verify_leaf # => [LEAF_VALUE[8]] # 3. load global index from memory - padw mem_loadw_le.GLOBAL_INDEX_PTR - padw push.GLOBAL_INDEX_PTR add.4 mem_loadw_le swapw + push.GLOBAL_INDEX_PTR exec.utils::mem_load_double_word # => [GLOBAL_INDEX[8], LEAF_VALUE[8]] # to see if we're dealing with a deposit from mainnet or from a rollup, process the global index @@ -821,3 +845,73 @@ proc calculate_root( padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ROOT_LO, ROOT_HI] end + +#! Updates the claimed global index (CGI) chain hash by recomputing it and storing into the +#! corresponding storage slot. +#! +#! The resulting hash is computed as a sequential hash of leaf value, global index, and previous +#! value of the CGI chain hash: +#! NEW_CGI_CHAIN_HASH[8] = Keccak256(OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX[8], LEAF_VALUE[8])) +#! +#! Inputs: [leaf_value_ptr] +#! Outputs: [] +#! +#! Invocation: exec +proc update_cgi_chain_hash + # load the required CGI chain data values onto the stack + exec.load_cgi_chain_hash_data + # => [GLOBAL_INDEX[8], LEAF_VALUE[8], OLD_CGI_CHAIN_HASH[8]] + + # compute the new CGI chain hash + exec.keccak256::merge swapdw + # => [OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX, LEAF_VALUE)] + + exec.keccak256::merge + # => [NEW_CGI_CHAIN_HASH[8]] + + # store the new CGI chain hash + exec.store_cgi_chain_hash + # => [] +end + +#! Loads the old CGI chain hash, the leaf value, and the global index onto the stack as a +#! preparation for the new CGI chain hash computation. +#! +#! Inputs: [leaf_value_ptr] +#! Outputs: [GLOBAL_INDEX[8], LEAF_VALUE[8], OLD_CGI_CHAIN_HASH[8]] +#! +#! Invocation: exec +proc load_cgi_chain_hash_data + # load the old CGI chain hash onto the stack + push.CGI_CHAIN_HASH_HI_SLOT_NAME[0..2] + exec.active_account::get_item + # => [OLD_CGI_CHAIN_HASH_HI, leaf_value_ptr] + + push.CGI_CHAIN_HASH_LO_SLOT_NAME[0..2] + exec.active_account::get_item + # => [OLD_CGI_CHAIN_HASH[8], leaf_value_ptr] + + # load the leaf value onto the stack + movup.8 exec.utils::mem_load_double_word + # => [LEAF_VALUE[8], OLD_CGI_CHAIN_HASH[8]] + + # load the global index onto the stack + push.GLOBAL_INDEX_PTR exec.utils::mem_load_double_word + # => [GLOBAL_INDEX[8], LEAF_VALUE[8], OLD_CGI_CHAIN_HASH[8]] +end + +#! Stores the computed global index (CGI) chain hash into the corresponding storage slots. +#! +#! Inputs: [NEW_CGI_CHAIN_HASH_LO, NEW_CGI_CHAIN_HASH_HI] +#! Outputs: [] +#! +#! Invocation: exec +proc store_cgi_chain_hash + push.CGI_CHAIN_HASH_LO_SLOT_NAME[0..2] + exec.native_account::set_item dropw + # => [NEW_CGI_CHAIN_HASH_HI] + + push.CGI_CHAIN_HASH_HI_SLOT_NAME[0..2] + exec.native_account::set_item dropw + # => [] +end \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/foundry.lock b/crates/miden-agglayer/solidity-compat/foundry.lock index 196c826d70..f8ac0886bd 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.lock +++ b/crates/miden-agglayer/solidity-compat/foundry.lock @@ -8,10 +8,22 @@ "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" } }, + "lib/openzeppelin-contracts": { + "branch": { + "name": "release-v5.0", + "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" + } + }, "lib/openzeppelin-contracts-upgradeable": { "branch": { "name": "release-v4.9", "rev": "2d081f24cac1a867f6f73d512f2022e1fa987854" } + }, + "lib/openzeppelin-contracts-upgradeable5": { + "branch": { + "name": "release-v5.0", + "rev": "723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1" + } } } \ 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 e841d3e689..36d2c9934e 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.toml +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -3,13 +3,15 @@ libs = ["lib"] optimizer = true optimizer_runs = 200 out = "out" -solc = "0.8.20" +solc = "0.8.28" src = "src" via_ir = true remappings = [ "@agglayer/=lib/agglayer-contracts/contracts/", "@openzeppelin/contracts-upgradeable4/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts-upgradeable5/=lib/openzeppelin-contracts-upgradeable5/contracts/", + "@openzeppelin/contracts5/=lib/openzeppelin-contracts/contracts/", ] # Emit extra output for test vector generation diff --git a/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts new file mode 160000 index 0000000000..dbb6104ce8 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable5 b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable5 new file mode 160000 index 0000000000..723f8cab09 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable5 @@ -0,0 +1 @@ +Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 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_local_tx.json index 3bf850e8e0..f21309609a 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_local_tx.json @@ -1,5 +1,6 @@ { - "amount": 100000000000000000000, + "amount": "100000000000000000000", + "claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d", "deposit_count": 1, "description": "L1 bridgeAsset transaction test vectors with valid Merkle proofs", "destination_address": "0x00000000AA0000000000bb000000cc000000Dd00", 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 index b0819ea63d..e4423417fb 100644 --- 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 @@ -1,5 +1,6 @@ { "amount": 100000000000000, + "claimed_global_index_hash_chain": "0xd2bb2f0231ee9ea0c88e89049bea6dbcf7dd96a1015ca9e66ab38ef3c8dc928e", "destination_address": "0x00000000b0E79c68cafC54802726C6F102Cca300", "destination_network": 20, "global_exit_root": "0xe1cbfbde30bd598ee9aa2ac913b60d53e3297e51ed138bf86c500dd7d2391e7d", diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol index 64517d8580..22bc2eeb83 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsLocalTx.t.sol @@ -2,21 +2,33 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "@agglayer/v2/lib/DepositContractV2.sol"; +import "@agglayer/v2/sovereignChains/BridgeL2SovereignChain.sol"; import "@agglayer/lib/GlobalExitRootLib.sol"; +import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.sol"; import "./DepositContractTestHelpers.sol"; +contract MockGlobalExitRootManagerLocal is IBasePolygonZkEVMGlobalExitRoot { + mapping(bytes32 => uint256) public override globalExitRootMap; + + function updateExitRoot(bytes32) external override {} + + function setGlobalExitRoot(bytes32 globalExitRoot) external { + globalExitRootMap[globalExitRoot] = block.number; + } +} + /** * @title ClaimAssetTestVectorsLocalTx * @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 * * The output can be used to verify Miden's ability to process L1 bridge transactions. */ -contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContractTestHelpers { +contract ClaimAssetTestVectorsLocalTx is Test, BridgeL2SovereignChain, DepositContractTestHelpers { /** * @notice Generates bridge asset test vectors with VALID Merkle proofs. * Simulates a user calling bridgeAsset() to bridge tokens from L1 to Miden. @@ -95,6 +107,36 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContrac // extracts it via uint32(globalIndex) in _verifyLeaf() uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex); + // ====== COMPUTE CLAIMED GLOBAL INDEX HASH CHAIN ====== + // Use the actual BridgeL2SovereignChain to compute the authoritative value + + // Set up the global exit root manager + MockGlobalExitRootManagerLocal gerManager = new MockGlobalExitRootManagerLocal(); + 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( + smtProofLocal, + smtProofRollup, + globalIndex, + mainnetExitRoot, + rollupExitRoot, + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + // Read the updated claimedGlobalIndexHashChain + bytes32 claimedHashChain = claimedGlobalIndexHashChain; + // ====== SERIALIZE SMT PROOFS ====== _serializeProofs(obj, smtProofLocal, smtProofRollup); @@ -115,6 +157,7 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContrac { vm.serializeUint(obj, "deposit_count", depositCountValue); vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); + vm.serializeBytes32(obj, "claimed_global_index_hash_chain", claimedHashChain); vm.serializeBytes32(obj, "local_exit_root", localExitRoot); vm.serializeBytes32(obj, "mainnet_exit_root", mainnetExitRoot); vm.serializeBytes32(obj, "rollup_exit_root", rollupExitRoot); @@ -134,6 +177,39 @@ contract ClaimAssetTestVectorsLocalTx is Test, DepositContractV2, DepositContrac } } + /** + * @notice Harness function to call _verifyLeafBridge externally + */ + function verifyLeafBridgeHarness( + bytes32[32] calldata smtProofLocalExitRoot, + bytes32[32] calldata smtProofRollupExitRoot, + uint256 globalIndex, + bytes32 mainnetExitRoot, + bytes32 rollupExitRoot, + uint8 leafType, + uint32 originNetwork, + address originTokenAddress, + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + bytes32 metadataHash + ) external { + _verifyLeafBridge( + smtProofLocalExitRoot, + smtProofRollupExitRoot, + globalIndex, + mainnetExitRoot, + rollupExitRoot, + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + } + /** * @notice Helper function to serialize SMT proofs (avoids stack too deep) * @param obj The JSON object key diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol index 0f4e56bb5b..a4b503a9c4 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRealTx.t.sol @@ -2,13 +2,25 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import "@agglayer/v2/lib/DepositContractV2.sol"; +import "@agglayer/v2/sovereignChains/BridgeL2SovereignChain.sol"; import "@agglayer/lib/GlobalExitRootLib.sol"; +import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.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. * @@ -16,7 +28,7 @@ import "@agglayer/lib/GlobalExitRootLib.sol"; * * The output can be compared against the Rust ClaimNoteStorage implementation. */ -contract ClaimAssetTestVectorsRealTx is Test, DepositContractV2 { +contract ClaimAssetTestVectorsRealTx is Test, BridgeL2SovereignChain { /** * @notice Generates claim asset test vectors from real Katana transaction and saves to JSON. * Uses real transaction data from Katana explorer: @@ -28,10 +40,17 @@ contract ClaimAssetTestVectorsRealTx is Test, DepositContractV2 { 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) - bytes32[32] memory smtProofLocalExitRoot = [ + smtProofLocalExitRoot = [ bytes32(0x0000000000000000000000000000000000000000000000000000000000000000), bytes32(0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5), bytes32(0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30), @@ -66,24 +85,28 @@ contract ClaimAssetTestVectorsRealTx is Test, DepositContractV2 { bytes32(0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9) ]; - // forge-std JSON serialization supports `bytes32[]` but not `bytes32[32]`. - bytes32[] memory smtProofLocalExitRootDyn = new bytes32[](32); + // SMT proof for rollup exit root (32 nodes - all zeros for this rollup claim). for (uint256 i = 0; i < 32; i++) { - smtProofLocalExitRootDyn[i] = smtProofLocalExitRoot[i]; + smtProofRollupExitRoot[i] = bytes32(0); } - // 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; + globalIndex = 18446744073709788808; // Exit roots - bytes32 mainnetExitRoot = 0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c; - bytes32 rollupExitRoot = 0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858; + mainnetExitRoot = 0x31d3268d3a0145d65482b336935fa07dab0822f7dccd865f361d2bf122c4905c; + rollupExitRoot = 0x8452a95fd710163c5fa8ca2b2fe720d8781f0222bb9e82c2a442ec986c374858; // Compute global exit root: keccak256(mainnetExitRoot || rollupExitRoot) - bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(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); @@ -120,6 +143,36 @@ contract ClaimAssetTestVectorsRealTx is Test, DepositContractV2 { 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); @@ -127,11 +180,45 @@ contract ClaimAssetTestVectorsRealTx is Test, DepositContractV2 { 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); + 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); } } + + /** + * @notice Harness function to call _verifyLeafBridge externally + */ + function verifyLeafBridgeHarness( + bytes32[32] calldata smtProofLocalExitRoot, + bytes32[32] calldata smtProofRollupExitRoot, + uint256 globalIndex, + bytes32 mainnetExitRoot, + bytes32 rollupExitRoot, + uint8 leafType, + uint32 originNetwork, + address originTokenAddress, + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + bytes32 metadataHash + ) external { + _verifyLeafBridge( + smtProofLocalExitRoot, + smtProofRollupExitRoot, + globalIndex, + mainnetExitRoot, + rollupExitRoot, + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + } } diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index bc16fcf274..51326c91a5 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -11,6 +11,7 @@ use miden_utils_sync::LazyLock; use thiserror::Error; use super::agglayer_bridge_component_library; +use crate::claim_note::Keccak256Output; pub use crate::{ B2AggNote, ClaimNoteStorage, @@ -72,18 +73,26 @@ static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::agglayer::bridge::ger_manager") .expect("GER manager storage slot name should be valid") }); +static CGI_CHAIN_HASH_LO_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::cgi_chain_hash_lo") + .expect("CGI chain hash lo storage slot name should be valid") +}); +static CGI_CHAIN_HASH_HI_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::agglayer::bridge::cgi_chain_hash_hi") + .expect("CGI chain hash hi storage slot name should be valid") +}); /// An [`AccountComponent`] implementing the AggLayer Bridge. /// /// It reexports the procedures from `miden::agglayer::bridge`. When linking against this /// component, the `agglayer` library must be available to the assembler. /// The procedures of this component are: -/// - `assert_sender_is_bridge_admin`, which validates CONFIG note senders. -/// - `assert_sender_is_ger_manager`, which validates UPDATE_GER note senders. /// - `register_faucet`, which registers a faucet in the bridge. /// - `update_ger`, which injects a new GER into the storage map. /// - `verify_leaf_bridge`, which verifies a deposit leaf against one of the stored GERs. /// - `bridge_out`, which bridges an asset out of Miden to the destination network. +/// - `claim`, which validates a claim against the AggLayer bridge and creates a MINT note for the +/// AggFaucet. /// /// ## Storage Layout /// @@ -96,6 +105,8 @@ static GER_MANAGER_SLOT_NAME: LazyLock = LazyLock::new(|| { /// - [`Self::token_registry_slot_name`]: Stores the token address → faucet ID map. /// - [`Self::bridge_admin_slot_name`]: Stores the bridge admin account ID. /// - [`Self::ger_manager_slot_name`]: Stores the GER manager account ID. +/// - [`Self::cgi_lo_slot_name`]: Stores the lower 128 bits of the CGI chain hash. +/// - [`Self::cgi_hi_slot_name`]: Stores the upper 128 bits of the CGI chain hash. /// /// The bridge starts with an empty faucet registry; faucets are registered at runtime via /// CONFIG_AGG_BRIDGE notes. @@ -167,6 +178,16 @@ impl AggLayerBridge { &GER_MANAGER_SLOT_NAME } + /// Storage slot name for the lower 128 bits of the CGI chain hash. + pub fn cgi_lo_slot_name() -> &'static StorageSlotName { + &CGI_CHAIN_HASH_LO_SLOT_NAME + } + + /// Storage slot name for the lower 128 bits of the CGI chain hash. + pub fn cgi_hi_slot_name() -> &'static StorageSlotName { + &CGI_CHAIN_HASH_HI_SLOT_NAME + } + /// Returns a boolean indicating whether the provided GER is present in storage of the provided /// bridge account. /// @@ -256,6 +277,41 @@ impl AggLayerBridge { value.to_vec()[0].as_int() } + /// Returns the claimed global index (CGI) chain hash from the corresponding storage slot. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided account is not an [`AggLayerBridge`] account. + pub fn cgi_chain_hash( + bridge_account: &Account, + ) -> Result { + // check that the provided account is a bridge account + Self::assert_bridge_account(bridge_account)?; + + let cgi_chain_hash_lo = bridge_account + .storage() + .get_item(AggLayerBridge::cgi_lo_slot_name()) + .expect("failed to get CGI hash chain lo slot"); + let cgi_chain_hash_hi = bridge_account + .storage() + .get_item(AggLayerBridge::cgi_hi_slot_name()) + .expect("failed to get CGI hash chain hi slot"); + + let cgi_chain_hash_bytes = cgi_chain_hash_lo + .iter() + .rev() + .chain(cgi_chain_hash_hi.iter().rev()) + .flat_map(|felt| (felt.as_int() as u32).to_le_bytes()) + .collect::>(); + + Ok(Keccak256Output::new( + cgi_chain_hash_bytes + .try_into() + .expect("keccak hash should consist of exactly 32 bytes"), + )) + } + // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- @@ -360,6 +416,8 @@ impl From for AccountComponent { StorageSlot::with_empty_map(TOKEN_REGISTRY_SLOT_NAME.clone()), StorageSlot::with_value(BRIDGE_ADMIN_SLOT_NAME.clone(), bridge_admin_word), StorageSlot::with_value(GER_MANAGER_SLOT_NAME.clone(), ger_manager_word), + StorageSlot::with_value(CGI_CHAIN_HASH_LO_SLOT_NAME.clone(), Word::empty()), + StorageSlot::with_value(CGI_CHAIN_HASH_HI_SLOT_NAME.clone(), Word::empty()), ]; bridge_component(bridge_storage_slots) } diff --git a/crates/miden-agglayer/src/eth_types/global_index.rs b/crates/miden-agglayer/src/eth_types/global_index.rs index 4c4688edc0..66d66dd2aa 100644 --- a/crates/miden-agglayer/src/eth_types/global_index.rs +++ b/crates/miden-agglayer/src/eth_types/global_index.rs @@ -1,8 +1,9 @@ use alloc::vec::Vec; +#[cfg(any(test, feature = "testing"))] use miden_core_lib::handlers::bytes_to_packed_u32_felts; -use miden_protocol::Felt; use miden_protocol::utils::{HexParseError, hex_to_bytes}; +use miden_protocol::{Felt, Word}; // ================================================================================================ // GLOBAL INDEX ERROR @@ -99,6 +100,18 @@ impl GlobalIndex { pub const fn as_bytes(&self) -> &[u8; 32] { &self.0 } + + /// Converts the [`GlobalIndex`] 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)] + } } #[cfg(test)] diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index a6a4ddae25..8b7dcdffd2 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -6,6 +6,7 @@ use alloc::string::String; use anyhow::Context; use miden_agglayer::claim_note::Keccak256Output; use miden_agglayer::{ + AggLayerBridge, ClaimNoteStorage, ConfigAggBridgeNote, ExitRoot, @@ -41,60 +42,9 @@ use super::test_utils::{ SOLIDITY_MERKLE_PROOF_VECTORS, }; -// HELPER FUNCTIONS +// TESTS // ================================================================================================ -fn merkle_proof_verification_code( - index: usize, - merkle_paths: &MerkleProofVerificationFile, -) -> String { - 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(); - 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 - )); - } - - let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); - let [root_lo, root_hi] = root.to_words(); - - 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::bridge::bridge_in - use miden::core::word - - begin - {store_path_source} - - push.{root_lo} mem_storew_be.256 dropw - push.{root_hi} mem_storew_be.260 dropw - - push.256 - push.{index} - push.0 - push.{leaf_hi} - exec.word::reverse - push.{leaf_lo} - exec.word::reverse - - exec.bridge_in::verify_merkle_proof - assert.err="verification failed" - end - "# - ) -} - /// Tests the bridge-in flow with the new 2-transaction architecture: /// /// TX0: CONFIG_AGG_BRIDGE → bridge (registers faucet + token address in registries) @@ -137,7 +87,7 @@ 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) = data_source.get_data(); + let (proof_data, leaf_data, ger, cgi_chain_hash) = data_source.get_data(); // CREATE AGGLAYER FAUCET ACCOUNT (with agglayer_faucet component) // Use the origin token address and network from the claim data. @@ -278,6 +228,16 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a let claim_executed = claim_tx_context.execute().await?; + // VERIFY CGI CHAIN HASH WAS SUCCESSFULLY UPDATED + // -------------------------------------------------------------------------------------------- + + let mut updated_bridge_account = bridge_account.clone(); + updated_bridge_account.apply_delta(claim_executed.account_delta())?; + + let actual_cgi_chain_hash = AggLayerBridge::cgi_chain_hash(&updated_bridge_account)?; + + assert_eq!(cgi_chain_hash, actual_cgi_chain_hash); + // VERIFY MINT NOTE WAS CREATED BY THE BRIDGE // -------------------------------------------------------------------------------------------- assert_eq!(claim_executed.output_notes().num_notes(), 1); @@ -404,3 +364,57 @@ async fn solidity_verify_merkle_proof_compatibility() -> anyhow::Result<()> { } Ok(()) } + +// HELPER FUNCTIONS +// ================================================================================================ + +fn merkle_proof_verification_code( + index: usize, + merkle_paths: &MerkleProofVerificationFile, +) -> String { + 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(); + 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 + )); + } + + let root = ExitRoot::from(hex_to_bytes(&merkle_paths.roots[index]).unwrap()); + let [root_lo, root_hi] = root.to_words(); + + 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::bridge::bridge_in + use miden::core::word + + begin + {store_path_source} + + push.{root_lo} mem_storew_be.256 dropw + push.{root_hi} mem_storew_be.260 dropw + + push.256 + push.{index} + push.0 + push.{leaf_hi} + exec.word::reverse + push.{leaf_lo} + exec.word::reverse + + exec.bridge_in::verify_merkle_proof + assert.err="verification failed" + end + "# + ) +} diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 50fb537243..6adc78910b 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -138,6 +138,7 @@ pub struct ProofValueVector { /// Expected global exit root: keccak256(mainnetExitRoot || rollupExitRoot) #[allow(dead_code)] pub global_exit_root: String, + pub claimed_global_index_hash_chain: String, } impl ProofValueVector { @@ -268,7 +269,7 @@ pub enum ClaimDataSource { impl ClaimDataSource { /// Returns the `(ProofData, LeafData, ExitRoot)` tuple for this data source. - pub fn get_data(self) -> (ProofData, LeafData, ExitRoot) { + pub fn get_data(self) -> (ProofData, LeafData, ExitRoot, Keccak256Output) { let vector = match self { ClaimDataSource::Real => &*CLAIM_ASSET_VECTOR, ClaimDataSource::Simulated => &*CLAIM_ASSET_VECTOR_LOCAL, @@ -276,7 +277,12 @@ impl ClaimDataSource { 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) + let cgi_chain_hash = Keccak256Output::new( + hex_to_bytes(&vector.proof.claimed_global_index_hash_chain) + .expect("invalid CGI chain hash"), + ); + + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger, cgi_chain_hash) } }