From a0d43bfa0edea0bacc616d951f7a45b62cea1976 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Tue, 24 Feb 2026 22:41:44 +0300 Subject: [PATCH 01/11] feat: add CGI chain hash computation --- .../asm/agglayer/bridge/bridge_in.masm | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 7a14f2a788..10cb5d0c3f 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -165,6 +165,7 @@ const OUTPUT_NOTE_TYPE_PUBLIC = 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 @@ -173,9 +174,36 @@ 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)] + + # compute the claimed global index hash chain + # + # this hash is computed as: + # NEW_CGI_CHAIN_HASH[8] = Keccak256(OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX[8], LEAF_VALUE[8])) + + # load the leaf value back to the stack from local memory + loc_loadw_le.4 swapw loc_loadw_le.0 + # => [LEAF_VALUE[8], pad(8)] + + # compute the claimed global index chain hash + exec.compute_cgi_hash_chain + # => [NEW_CGI_CHAIN_HASH[8], pad(8)] + + # store the computed CGI chain hash + # + # TODO: once the CLAIM note will be consumed against the bridge contract (and not FPIed against + # it), store the computed value. Notice that this value is used during the computation of the + # next CGI chain hash, so the `compute_cgi_hash_chain` procedure should be updated. + dropw dropw + # => [pad(16)] end #! Assert the global index is valid for a mainnet deposit. @@ -426,8 +454,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 @@ -747,3 +774,32 @@ proc calculate_root( padw loc_loadw_le.CUR_HASH_HI_LOCAL padw loc_loadw_le.CUR_HASH_LO_LOCAL # => [ROOT_LO, ROOT_HI] end + +#! Computes the claimed global index (CGI) chain hash. +#! +#! 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[8]] +#! Outputs: [NEW_CGI_CHAIN_HASH[8]] +#! +#! Invocation: exec +proc compute_cgi_hash_chain + # load the global index onto the stack + push.GLOBAL_INDEX_PTR exec.utils::mem_load_double_word + # => [GLOBAL_INDEX[8], LEAF_VALUE[8]] + + exec.keccak256::merge + # => [Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] + + # load the old CGI chain hash + # + # TODO: load the CGI chain hash value here once it will be stored + padw padw + # => [OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] + + # compute the new CGI chain hash + exec.keccak256::merge + # => [NEW_CGI_CHAIN_HASH[8], pad(8)] +end From 1e5f5aa670dfad1fc916684e54195a5e2107eb8e Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Wed, 25 Feb 2026 01:40:04 +0300 Subject: [PATCH 02/11] test: [WiP-1] first iteration, not building --- Makefile | 1 + .../claimed_global_index_hash_chain.json | 6 + .../ClaimedGlobalIndexHashChainVectors.t.sol | 136 ++++++++++++++++++ .../tests/agglayer/cgi_hash_chain.rs | 51 +++++++ crates/miden-testing/tests/agglayer/mod.rs | 1 + .../tests/agglayer/test_utils.rs | 20 +++ 6 files changed, 215 insertions(+) create mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json create mode 100644 crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol create mode 100644 crates/miden-testing/tests/agglayer/cgi_hash_chain.rs diff --git a/Makefile b/Makefile index 019bbd2bf9..c663f40a79 100644 --- a/Makefile +++ b/Makefile @@ -151,6 +151,7 @@ generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Fo 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 + cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateClaimedGlobalIndexHashChainVectors # --- benchmarking -------------------------------------------------------------------------------- diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json new file mode 100644 index 0000000000..c63b22cdb4 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json @@ -0,0 +1,6 @@ +{ + "description": "Claimed global index hash chain vector from BridgeL2SovereignChain", + "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", + "leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d", + "claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d" +} diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol new file mode 100644 index 0000000000..162861de05 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/sovereignChains/BridgeL2SovereignChain.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; +import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.sol"; + +contract MockGlobalExitRootManager is IBasePolygonZkEVMGlobalExitRoot { + mapping(bytes32 => uint256) public override globalExitRootMap; + + function updateExitRoot(bytes32) external override {} + + function setGlobalExitRoot(bytes32 globalExitRoot) external { + globalExitRootMap[globalExitRoot] = block.number; + } +} + +/** + * @title ClaimedGlobalIndexHashChainVectors + * @notice Generates a test vector for claimedGlobalIndexHashChain using _verifyLeafBridge. + * + * Run with: forge test -vv --match-test test_generateClaimedGlobalIndexHashChainVectors + */ +contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { + function test_generateClaimedGlobalIndexHashChainVectors() public { + string memory obj = "root"; + + // ====== BRIDGE TRANSACTION PARAMETERS ====== + uint8 leafType = 0; + uint32 originNetwork = 0; + address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; + uint32 destinationNetwork = 20; + address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; + uint256 amount = 100000000000000000000; + + bytes memory metadata = abi.encode("Test Token", "TEST", uint8(18)); + bytes32 metadataHash = keccak256(metadata); + + // ====== COMPUTE LEAF VALUE AND ADD TO TREE ====== + bytes32 leafValue = getLeafValue( + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + _addLeaf(leafValue); + uint256 leafIndex = depositCount - 1; + bytes32 localExitRoot = getRoot(); + + // ====== GENERATE MERKLE PROOF ====== + bytes32[32] memory canonicalZeros = _computeCanonicalZeros(); + bytes32[32] memory smtProofLocalExitRoot = + _generateLocalProof(leafIndex, canonicalZeros); + bytes32[32] memory smtProofRollupExitRoot; + + // ====== COMPUTE EXIT ROOTS ====== + bytes32 mainnetExitRoot = localExitRoot; + bytes32 rollupExitRoot = keccak256(abi.encodePacked("rollup_exit_root_simulated")); + bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot( + mainnetExitRoot, + rollupExitRoot + ); + + // ====== SET GLOBAL EXIT ROOT MANAGER ====== + MockGlobalExitRootManager gerManager = new MockGlobalExitRootManager(); + gerManager.setGlobalExitRoot(globalExitRoot); + globalExitRootManager = IBasePolygonZkEVMGlobalExitRoot(address(gerManager)); + + // Use a non-zero network ID to match sovereign-chain requirements + networkID = 10; + + // ====== COMPUTE GLOBAL INDEX ====== + uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex); + + // ====== COMPUTE CLAIMED GLOBAL INDEX HASH CHAIN ====== + _verifyLeafBridge( + smtProofLocalExitRoot, + smtProofRollupExitRoot, + globalIndex, + mainnetExitRoot, + rollupExitRoot, + leafType, + originNetwork, + originTokenAddress, + destinationNetwork, + destinationAddress, + amount, + metadataHash + ); + + bytes32 claimedHashChain = claimedGlobalIndexHashChain; + + // ====== SERIALIZE OUTPUT ====== + vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); + vm.serializeBytes32(obj, "leaf_value", leafValue); + vm.serializeBytes32(obj, "claimed_global_index_hash_chain", claimedHashChain); + string memory json = vm.serializeString( + obj, + "description", + "Claimed global index hash chain vector from BridgeL2SovereignChain" + ); + + vm.writeJson(json, "test-vectors/claimed_global_index_hash_chain.json"); + } + + // ============================================================================================ + // Helpers (copied from DepositContractTestHelpers) + // ============================================================================================ + + function _computeCanonicalZeros() internal pure returns (bytes32[32] memory canonicalZeros) { + bytes32 current = bytes32(0); + for (uint256 i = 0; i < 32; i++) { + canonicalZeros[i] = current; + current = keccak256(abi.encodePacked(current, current)); + } + } + + function _generateLocalProof(uint256 leafIndex, bytes32[32] memory canonicalZeros) + internal + view + returns (bytes32[32] memory smtProof) + { + for (uint256 i = 0; i < 32; i++) { + if ((leafIndex >> i) & 1 == 1) { + smtProof[i] = _branch[i]; + } else { + smtProof[i] = canonicalZeros[i]; + } + } + } +} diff --git a/crates/miden-testing/tests/agglayer/cgi_hash_chain.rs b/crates/miden-testing/tests/agglayer/cgi_hash_chain.rs new file mode 100644 index 0000000000..6e8ed53c84 --- /dev/null +++ b/crates/miden-testing/tests/agglayer/cgi_hash_chain.rs @@ -0,0 +1,51 @@ +extern crate alloc; + +use alloc::sync::Arc; + +use miden_agglayer::claim_note::Keccak256Output; +use miden_agglayer::{GlobalIndex, agglayer_library}; +use miden_assembly::{Assembler, DefaultSourceManager}; +use miden_core_lib::CoreLibrary; +use miden_protocol::Felt; +use miden_tx::utils::hex_to_bytes; + +use super::test_utils::{ + CGIChainHashTestData, + CLAIMED_GLOBAL_INDEX_HASH_CHAIN, + execute_program_with_default_host, +}; + +#[tokio::test] +#[ignore = "CGI chain hash is not stored anywhere yet"] +async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> { + let vector: &CGIChainHashTestData = &*CLAIMED_GLOBAL_INDEX_HASH_CHAIN; + + let global_index = GlobalIndex::from_hex(&vector.global_index).expect("valid global index hex"); + + let leaf_bytes: [u8; 32] = hex_to_bytes(&vector.leaf) + .expect("valid leaf value hex") + .try_into() + .expect("leaf value must be 32 bytes"); + let [leaf_lo, leaf_hi] = Keccak256Output::from(leaf_bytes).to_words(); + + let expected_hash_bytes: [u8; 32] = hex_to_bytes(&vector.cgi_chain_hash) + .expect("valid claimed hash hex") + .try_into() + .expect("claimed hash must be 32 bytes"); + let [expected_hash_lo, expected_hash_hi] = Keccak256Output::from(expected_hash_bytes).to_words(); + + let source = format!( + r#" + use miden::core::sys + use miden::agglayer::bridge::bridge_in + + begin + + end + "# + ); + + + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index a497f74230..bb01ebf5fe 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 cgi_hash_chain; mod config_bridge; mod global_index; mod leaf_utils; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 50fb537243..c0aaa05345 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -54,6 +54,11 @@ pub const CANONICAL_ZEROS_JSON: &str = pub const MMR_FRONTIER_VECTORS_JSON: &str = include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); +/// Claimed global index hash chain JSON from the Foundry-generated file. +pub const CLAIMED_GLOBAL_INDEX_HASH_CHAIN_JSON: &str = include_str!( + "../../../miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json" +); + // SERDE HELPERS // ================================================================================================ @@ -185,6 +190,14 @@ pub struct ClaimAssetVector { pub leaf: LeafValueVector, } +/// Deserialized claimed global index hash chain data from Solidity-generated JSON. +#[derive(Debug, Deserialize)] +pub struct CGIChainHashTestData { + pub global_index: String, + pub leaf: String, + pub cgi_chain_hash: String, +} + /// 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. @@ -236,6 +249,13 @@ pub static CLAIM_ASSET_VECTOR_LOCAL: LazyLock = LazyLock::new( .expect("failed to parse bridge asset vectors JSON") }); +/// Lazily parsed claimed global index hash chain data from the JSON file. +pub static CLAIMED_GLOBAL_INDEX_HASH_CHAIN: LazyLock = + LazyLock::new(|| { + serde_json::from_str(CLAIMED_GLOBAL_INDEX_HASH_CHAIN_JSON) + .expect("failed to parse claimed global index hash chain vector JSON") + }); + /// Lazily parsed Merkle proof vectors from the JSON file. pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = LazyLock::new(|| { From 8c3dcdfeed9f3ff2938e35cecbb0c3a175ffe697 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Wed, 25 Feb 2026 03:06:12 +0300 Subject: [PATCH 03/11] test: [WiP-2] building state --- .gitmodules | 8 +++++ .../solidity-compat/foundry.toml | 5 ++- .../lib/openzeppelin-contracts | 1 + .../lib/openzeppelin-contracts-upgradeable5 | 1 + .../claim_asset_vectors_local_tx.json | 2 +- .../claimed_global_index_hash_chain.json | 6 ++-- .../ClaimedGlobalIndexHashChainVectors.t.sol | 32 ++++++++++++++++++- 7 files changed, 49 insertions(+), 6 deletions(-) create mode 160000 crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts create mode 160000 crates/miden-agglayer/solidity-compat/lib/openzeppelin-contracts-upgradeable5 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/solidity-compat/foundry.toml b/crates/miden-agglayer/solidity-compat/foundry.toml index e841d3e689..1d3c882842 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.toml +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -3,13 +3,16 @@ 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/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts5/=lib/openzeppelin-contracts/contracts/", "@openzeppelin/contracts-upgradeable4/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts-upgradeable5/=lib/openzeppelin-contracts-upgradeable5/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..cbea883d75 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,5 @@ { - "amount": 100000000000000000000, + "amount": "100000000000000000000", "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/claimed_global_index_hash_chain.json b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json index c63b22cdb4..29eac7a1d5 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json @@ -1,6 +1,6 @@ { + "claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d", "description": "Claimed global index hash chain vector from BridgeL2SovereignChain", "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", - "leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d", - "claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d" -} + "leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d" +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol index 162861de05..965825a3e8 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol @@ -78,7 +78,7 @@ contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex); // ====== COMPUTE CLAIMED GLOBAL INDEX HASH CHAIN ====== - _verifyLeafBridge( + this.verifyLeafBridgeHarness( smtProofLocalExitRoot, smtProofRollupExitRoot, globalIndex, @@ -108,6 +108,36 @@ contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { vm.writeJson(json, "test-vectors/claimed_global_index_hash_chain.json"); } + 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 + ); + } + // ============================================================================================ // Helpers (copied from DepositContractTestHelpers) // ============================================================================================ From 5f707decb47a035091d5da21f1a2b20f67fadea9 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Wed, 25 Feb 2026 20:04:16 +0300 Subject: [PATCH 04/11] test: updated script, failing --- .../solidity-compat/foundry.lock | 12 ++ .../solidity-compat/foundry.toml | 3 +- .../claimed_global_index_hash_chain.json | 6 +- .../ClaimedGlobalIndexHashChainVectors.t.sol | 41 +----- .../src/eth_types/global_index.rs | 15 ++- .../tests/agglayer/cgi_chain_hash.rs | 123 ++++++++++++++++++ .../tests/agglayer/cgi_hash_chain.rs | 51 -------- crates/miden-testing/tests/agglayer/mod.rs | 2 +- .../tests/agglayer/test_utils.rs | 10 +- 9 files changed, 166 insertions(+), 97 deletions(-) create mode 100644 crates/miden-testing/tests/agglayer/cgi_chain_hash.rs delete mode 100644 crates/miden-testing/tests/agglayer/cgi_hash_chain.rs 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 1d3c882842..36d2c9934e 100644 --- a/crates/miden-agglayer/solidity-compat/foundry.toml +++ b/crates/miden-agglayer/solidity-compat/foundry.toml @@ -9,10 +9,9 @@ via_ir = true remappings = [ "@agglayer/=lib/agglayer-contracts/contracts/", - "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", - "@openzeppelin/contracts5/=lib/openzeppelin-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/test-vectors/claimed_global_index_hash_chain.json b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json index 29eac7a1d5..0a4612faf8 100644 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json @@ -1,6 +1,6 @@ { - "claimed_global_index_hash_chain": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d", - "description": "Claimed global index hash chain vector from BridgeL2SovereignChain", + "cgi_chain_hash": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d", "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", - "leaf_value": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d" + "leaf": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d", + "old_cgi_chain_hash": "0x0000000000000000000000000000000000000000000000000000000000000000" } \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol index 965825a3e8..a8c7cffcb9 100644 --- a/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol +++ b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "@agglayer/v2/sovereignChains/BridgeL2SovereignChain.sol"; import "@agglayer/lib/GlobalExitRootLib.sol"; import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.sol"; +import "./DepositContractTestHelpers.sol"; contract MockGlobalExitRootManager is IBasePolygonZkEVMGlobalExitRoot { mapping(bytes32 => uint256) public override globalExitRootMap; @@ -22,7 +23,7 @@ contract MockGlobalExitRootManager is IBasePolygonZkEVMGlobalExitRoot { * * Run with: forge test -vv --match-test test_generateClaimedGlobalIndexHashChainVectors */ -contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { +contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain, DepositContractTestHelpers { function test_generateClaimedGlobalIndexHashChainVectors() public { string memory obj = "root"; @@ -78,6 +79,8 @@ contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex); // ====== COMPUTE CLAIMED GLOBAL INDEX HASH CHAIN ====== + bytes32 oldClaimedHashChain = claimedGlobalIndexHashChain; + this.verifyLeafBridgeHarness( smtProofLocalExitRoot, smtProofRollupExitRoot, @@ -97,13 +100,9 @@ contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { // ====== SERIALIZE OUTPUT ====== vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); - vm.serializeBytes32(obj, "leaf_value", leafValue); - vm.serializeBytes32(obj, "claimed_global_index_hash_chain", claimedHashChain); - string memory json = vm.serializeString( - obj, - "description", - "Claimed global index hash chain vector from BridgeL2SovereignChain" - ); + vm.serializeBytes32(obj, "leaf", leafValue); + vm.serializeBytes32(obj, "cgi_chain_hash", claimedHashChain); + string memory json = vm.serializeBytes32(obj, "old_cgi_chain_hash", oldClaimedHashChain); vm.writeJson(json, "test-vectors/claimed_global_index_hash_chain.json"); } @@ -137,30 +136,4 @@ contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain { metadataHash ); } - - // ============================================================================================ - // Helpers (copied from DepositContractTestHelpers) - // ============================================================================================ - - function _computeCanonicalZeros() internal pure returns (bytes32[32] memory canonicalZeros) { - bytes32 current = bytes32(0); - for (uint256 i = 0; i < 32; i++) { - canonicalZeros[i] = current; - current = keccak256(abi.encodePacked(current, current)); - } - } - - function _generateLocalProof(uint256 leafIndex, bytes32[32] memory canonicalZeros) - internal - view - returns (bytes32[32] memory smtProof) - { - for (uint256 i = 0; i < 32; i++) { - if ((leafIndex >> i) & 1 == 1) { - smtProof[i] = _branch[i]; - } else { - smtProof[i] = canonicalZeros[i]; - } - } - } } 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/cgi_chain_hash.rs b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs new file mode 100644 index 0000000000..5e889a663b --- /dev/null +++ b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs @@ -0,0 +1,123 @@ +extern crate alloc; + +use miden_agglayer::claim_note::Keccak256Output; +use miden_agglayer::{GlobalIndex, agglayer_library}; +use miden_standards::code_builder::CodeBuilder; +use miden_testing::TransactionContextBuilder; +use miden_tx::utils::hex_to_bytes; + +use super::test_utils::{CGIChainHashTestData, CLAIMED_GLOBAL_INDEX_HASH_CHAIN}; + +#[tokio::test] +#[ignore = "CGI chain hash is not stored anywhere yet"] +async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> { + let cgi_chain_hash_data: &CGIChainHashTestData = &CLAIMED_GLOBAL_INDEX_HASH_CHAIN; + + let [global_index_lo, global_index_hi] = + GlobalIndex::from_hex(&cgi_chain_hash_data.global_index) + .expect("valid global index hex") + .to_words(); + + let leaf_bytes: [u8; 32] = + hex_to_bytes(&cgi_chain_hash_data.leaf).expect("leaf value must be 32 bytes"); + let [leaf_lo, leaf_hi] = Keccak256Output::from(leaf_bytes).to_words(); + + let expected_cgi_hash_bytes: [u8; 32] = + hex_to_bytes(&cgi_chain_hash_data.cgi_chain_hash).expect("claimed hash must be 32 bytes"); + let [expected_cgi_hash_lo, expected_cgi_hash_hi] = + Keccak256Output::from(expected_cgi_hash_bytes).to_words(); + + let old_cgi_hash_bytes: [u8; 32] = hex_to_bytes(&cgi_chain_hash_data.old_cgi_chain_hash) + .expect("claimed hash must be 32 bytes"); + let [old_cgi_hash_lo, old_cgi_hash_hi] = Keccak256Output::from(old_cgi_hash_bytes).to_words(); + + let source = format!( + r#" + use miden::core::crypto::hashes::keccak256 + use miden::core::sys + + use miden::agglayer::common::utils + + const GLOBAL_INDEX_PTR = 0 + const OLD_CGI_CHAIN_HASH_PTR = 8 + + # This is a copy of the `compute_cgi_hash_chain` procedure, designed only for testing + # purposes. Keep in sync with the original procedure. + # + # This procedure expects the global index and the old CGI chain hash to be stored in memory + # at addresses 0 and 8 respectively. + # + # Inputs: [LEAF_VALUE[8]] + # Outputs: [NEW_CGI_CHAIN_HASH[8]] + proc compute_cgi_hash_chain_copy + # load the global index onto the stack + push.GLOBAL_INDEX_PTR exec.utils::mem_load_double_word + # => [GLOBAL_INDEX[8], LEAF_VALUE[8]] + + exec.keccak256::merge + # => [Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] + + # load the old CGI chain hash + push.OLD_CGI_CHAIN_HASH_PTR exec.utils::mem_load_double_word + # => [OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] + + # compute the new CGI chain hash + exec.keccak256::merge + # => [NEW_CGI_CHAIN_HASH[8], pad(8)] + end + + begin + # push the expected hash onto the stack + push.{expected_cgi_hash_hi} push.{expected_cgi_hash_lo} + # => [EXPECTED_CGI_HASH[8]] + + # push the leaf value onto the stack + push.{leaf_hi} push.{leaf_lo} + # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] + + # push the global index onto the stack and save it into the memory + push.GLOBAL_INDEX_PTR push.{global_index_hi} push.{global_index_lo} + exec.utils::mem_store_double_word dropw dropw drop + # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] + + # push the old CGI chain hash onto the stack and save it into the memory + push.OLD_CGI_CHAIN_HASH_PTR push.{old_cgi_hash_hi} push.{old_cgi_hash_lo} + exec.utils::mem_store_double_word dropw dropw drop + # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] + + # compute the CGI chain hash + exec.compute_cgi_hash_chain_copy + # => [NEW_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] + + # assert that the hashes are identical + # => [NEW_CGI_CHAIN_HASH_LO, NEW_CGI_CHAIN_HASH_HI, EXPECTED_CGI_HASH_LO, EXPECTED_CGI_HASH_HI] + + debug.stack + + swapw.3 + # => [EXPECTED_CGI_HASH_HI, NEW_CGI_CHAIN_HASH_HI, EXPECTED_CGI_HASH_LO, NEW_CGI_CHAIN_HASH_LO] + + assert_eqw.err="CGI chain hash (HI) is incorrect" + # => [EXPECTED_CGI_HASH_LO, NEW_CGI_CHAIN_HASH_LO] + + assert_eqw.err="CGI chain hash (LO) is incorrect" + # => [] + + exec.sys::truncate_stack + # => [] + end + "# + ); + + let tx_script = CodeBuilder::new() + .with_statically_linked_library(&agglayer_library())? + .compile_tx_script(source)?; + + TransactionContextBuilder::with_existing_mock_account() + .tx_script(tx_script.clone()) + .build()? + .execute() + .await?; + + Ok(()) +} diff --git a/crates/miden-testing/tests/agglayer/cgi_hash_chain.rs b/crates/miden-testing/tests/agglayer/cgi_hash_chain.rs deleted file mode 100644 index 6e8ed53c84..0000000000 --- a/crates/miden-testing/tests/agglayer/cgi_hash_chain.rs +++ /dev/null @@ -1,51 +0,0 @@ -extern crate alloc; - -use alloc::sync::Arc; - -use miden_agglayer::claim_note::Keccak256Output; -use miden_agglayer::{GlobalIndex, agglayer_library}; -use miden_assembly::{Assembler, DefaultSourceManager}; -use miden_core_lib::CoreLibrary; -use miden_protocol::Felt; -use miden_tx::utils::hex_to_bytes; - -use super::test_utils::{ - CGIChainHashTestData, - CLAIMED_GLOBAL_INDEX_HASH_CHAIN, - execute_program_with_default_host, -}; - -#[tokio::test] -#[ignore = "CGI chain hash is not stored anywhere yet"] -async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> { - let vector: &CGIChainHashTestData = &*CLAIMED_GLOBAL_INDEX_HASH_CHAIN; - - let global_index = GlobalIndex::from_hex(&vector.global_index).expect("valid global index hex"); - - let leaf_bytes: [u8; 32] = hex_to_bytes(&vector.leaf) - .expect("valid leaf value hex") - .try_into() - .expect("leaf value must be 32 bytes"); - let [leaf_lo, leaf_hi] = Keccak256Output::from(leaf_bytes).to_words(); - - let expected_hash_bytes: [u8; 32] = hex_to_bytes(&vector.cgi_chain_hash) - .expect("valid claimed hash hex") - .try_into() - .expect("claimed hash must be 32 bytes"); - let [expected_hash_lo, expected_hash_hi] = Keccak256Output::from(expected_hash_bytes).to_words(); - - let source = format!( - r#" - use miden::core::sys - use miden::agglayer::bridge::bridge_in - - begin - - end - "# - ); - - - - Ok(()) -} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index bb01ebf5fe..0f61653b57 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,7 +1,7 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; -mod cgi_hash_chain; +mod cgi_chain_hash; mod config_bridge; mod global_index; mod leaf_utils; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index c0aaa05345..82d44557c3 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -196,6 +196,7 @@ pub struct CGIChainHashTestData { pub global_index: String, pub leaf: String, pub cgi_chain_hash: String, + pub old_cgi_chain_hash: String, } /// Deserialized Merkle proof vectors from Solidity DepositContractBase.sol. @@ -250,11 +251,10 @@ pub static CLAIM_ASSET_VECTOR_LOCAL: LazyLock = LazyLock::new( }); /// Lazily parsed claimed global index hash chain data from the JSON file. -pub static CLAIMED_GLOBAL_INDEX_HASH_CHAIN: LazyLock = - LazyLock::new(|| { - serde_json::from_str(CLAIMED_GLOBAL_INDEX_HASH_CHAIN_JSON) - .expect("failed to parse claimed global index hash chain vector JSON") - }); +pub static CLAIMED_GLOBAL_INDEX_HASH_CHAIN: LazyLock = LazyLock::new(|| { + serde_json::from_str(CLAIMED_GLOBAL_INDEX_HASH_CHAIN_JSON) + .expect("failed to parse claimed global index hash chain vector JSON") +}); /// Lazily parsed Merkle proof vectors from the JSON file. pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = From 558257d9eb614640a06c1261ae1fd1e010722ab7 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Wed, 25 Feb 2026 20:13:16 +0300 Subject: [PATCH 05/11] test: update script (passing) --- .../tests/agglayer/cgi_chain_hash.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs index 5e889a663b..e767b72211 100644 --- a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs +++ b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs @@ -8,8 +8,9 @@ use miden_tx::utils::hex_to_bytes; use super::test_utils::{CGIChainHashTestData, CLAIMED_GLOBAL_INDEX_HASH_CHAIN}; +/// Checks the correctness of the claimed global index chain hash computation, used during the CLAIM +/// note execution. #[tokio::test] -#[ignore = "CGI chain hash is not stored anywhere yet"] async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> { let cgi_chain_hash_data: &CGIChainHashTestData = &CLAIMED_GLOBAL_INDEX_HASH_CHAIN; @@ -34,6 +35,7 @@ async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> let source = format!( r#" use miden::core::crypto::hashes::keccak256 + use miden::core::word use miden::core::sys use miden::agglayer::common::utils @@ -68,20 +70,26 @@ async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> begin # push the expected hash onto the stack - push.{expected_cgi_hash_hi} push.{expected_cgi_hash_lo} + push.{expected_cgi_hash_hi} exec.word::reverse + push.{expected_cgi_hash_lo} exec.word::reverse # => [EXPECTED_CGI_HASH[8]] # push the leaf value onto the stack - push.{leaf_hi} push.{leaf_lo} + push.{leaf_hi} exec.word::reverse + push.{leaf_lo} exec.word::reverse # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] # push the global index onto the stack and save it into the memory - push.GLOBAL_INDEX_PTR push.{global_index_hi} push.{global_index_lo} + push.GLOBAL_INDEX_PTR + push.{global_index_hi} exec.word::reverse + push.{global_index_lo} exec.word::reverse exec.utils::mem_store_double_word dropw dropw drop # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] # push the old CGI chain hash onto the stack and save it into the memory - push.OLD_CGI_CHAIN_HASH_PTR push.{old_cgi_hash_hi} push.{old_cgi_hash_lo} + push.OLD_CGI_CHAIN_HASH_PTR + push.{old_cgi_hash_hi} exec.word::reverse + push.{old_cgi_hash_lo} exec.word::reverse exec.utils::mem_store_double_word dropw dropw drop # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] @@ -92,8 +100,6 @@ async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> # assert that the hashes are identical # => [NEW_CGI_CHAIN_HASH_LO, NEW_CGI_CHAIN_HASH_HI, EXPECTED_CGI_HASH_LO, EXPECTED_CGI_HASH_HI] - debug.stack - swapw.3 # => [EXPECTED_CGI_HASH_HI, NEW_CGI_CHAIN_HASH_HI, EXPECTED_CGI_HASH_LO, NEW_CGI_CHAIN_HASH_LO] From ef477d503ac5a9be1b622eaa3f5eb07a90d43b62 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Wed, 4 Mar 2026 15:48:09 +0300 Subject: [PATCH 06/11] refactor: create ctorage slots for CGI chain hash, use actiual procedure in test --- .../asm/agglayer/bridge/bridge_in.masm | 68 +++++++---- crates/miden-agglayer/src/lib.rs | 10 ++ .../miden-testing/tests/agglayer/bridge_in.rs | 107 +++++++++--------- .../tests/agglayer/cgi_chain_hash.rs | 52 +++------ 4 files changed, 126 insertions(+), 111 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 10cb5d0c3f..79c7b6b108 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -11,6 +11,8 @@ use miden::protocol::note use miden::protocol::output_note use miden::standards::note_tag use miden::standards::attachments::network_account_target +use miden::protocol::native_account +use miden::protocol::active_account # TYPE ALIASES # ================================================================================================= @@ -31,6 +33,11 @@ const ERR_INVALID_CLAIM_PROOF = "invalid claim proof" # CONSTANTS # ================================================================================================= +# 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::let::root_lo") +const CGI_CHAIN_HASH_HI_SLOT_NAME = word("miden::agglayer::let::root_hi") + # Memory pointers for proof data layout const PROOF_DATA_PTR = 0 const SMT_PROOF_LOCAL_EXIT_ROOT_PTR = 0 # local SMT proof is first @@ -189,20 +196,25 @@ pub proc verify_leaf_bridge # this hash is computed as: # NEW_CGI_CHAIN_HASH[8] = Keccak256(OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX[8], LEAF_VALUE[8])) - # load the leaf value back to the stack from local memory - loc_loadw_le.4 swapw loc_loadw_le.0 - # => [LEAF_VALUE[8], pad(8)] + # load the old CGI chain hash + push.CGI_CHAIN_HASH_HI_SLOT_NAME[0..2] + exec.active_account::get_item + # => [OLD_CGI_CHAIN_HASH_HI, pad(16)] + + push.CGI_CHAIN_HASH_LO_SLOT_NAME[0..2] + exec.active_account::get_item + # => [OLD_CGI_CHAIN_HASH[8], pad(16)] + + # load the pointer to the leaf value onto the stack + locaddr.0 + # => [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8], pad(16)] # compute the claimed global index chain hash exec.compute_cgi_hash_chain - # => [NEW_CGI_CHAIN_HASH[8], pad(8)] + # => [NEW_CGI_CHAIN_HASH[8], pad(16)] # store the computed CGI chain hash - # - # TODO: once the CLAIM note will be consumed against the bridge contract (and not FPIed against - # it), store the computed value. Notice that this value is used during the computation of the - # next CGI chain hash, so the `compute_cgi_hash_chain` procedure should be updated. - dropw dropw + exec.store_cgi_hash_chain # => [pad(16)] end @@ -781,25 +793,39 @@ end #! 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[8]] +#! Inputs: [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8]] #! Outputs: [NEW_CGI_CHAIN_HASH[8]] #! #! Invocation: exec -proc compute_cgi_hash_chain +pub proc compute_cgi_hash_chain + # load the leaf value onto the stack + 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]] - - exec.keccak256::merge - # => [Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] + # => [GLOBAL_INDEX[8], LEAF_VALUE[8], OLD_CGI_CHAIN_HASH[8]] - # load the old CGI chain hash - # - # TODO: load the CGI chain hash value here once it will be stored - padw padw - # => [OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] + exec.keccak256::merge swapdw + # => [OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX, LEAF_VALUE)] # compute the new CGI chain hash exec.keccak256::merge - # => [NEW_CGI_CHAIN_HASH[8], pad(8)] + # => [NEW_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_hash_chain + 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/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 8918fc0eb0..3789fd5886 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -143,6 +143,14 @@ 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. /// @@ -253,6 +261,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-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index c3ca8a28e7..94aaf59d78 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -39,60 +39,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) @@ -403,3 +352,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/cgi_chain_hash.rs b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs index e767b72211..c00f67ac85 100644 --- a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs +++ b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs @@ -38,35 +38,11 @@ async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> use miden::core::word use miden::core::sys + use miden::agglayer::bridge::bridge_in use miden::agglayer::common::utils - const GLOBAL_INDEX_PTR = 0 - const OLD_CGI_CHAIN_HASH_PTR = 8 - - # This is a copy of the `compute_cgi_hash_chain` procedure, designed only for testing - # purposes. Keep in sync with the original procedure. - # - # This procedure expects the global index and the old CGI chain hash to be stored in memory - # at addresses 0 and 8 respectively. - # - # Inputs: [LEAF_VALUE[8]] - # Outputs: [NEW_CGI_CHAIN_HASH[8]] - proc compute_cgi_hash_chain_copy - # load the global index onto the stack - push.GLOBAL_INDEX_PTR exec.utils::mem_load_double_word - # => [GLOBAL_INDEX[8], LEAF_VALUE[8]] - - exec.keccak256::merge - # => [Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] - - # load the old CGI chain hash - push.OLD_CGI_CHAIN_HASH_PTR exec.utils::mem_load_double_word - # => [OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX, LEAF_VALUE), pad(8)] - - # compute the new CGI chain hash - exec.keccak256::merge - # => [NEW_CGI_CHAIN_HASH[8], pad(8)] - end + const LEAF_VALUE_PTR = 0 + const GLOBAL_INDEX_PTR = 512 begin # push the expected hash onto the stack @@ -74,27 +50,27 @@ async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> push.{expected_cgi_hash_lo} exec.word::reverse # => [EXPECTED_CGI_HASH[8]] - # push the leaf value onto the stack + # push the old CGI chain hash onto the stack + push.{old_cgi_hash_hi} exec.word::reverse + push.{old_cgi_hash_lo} exec.word::reverse + # => [OLD_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] + + # push the leaf value onto the stack and save it into the memory + push.LEAF_VALUE_PTR push.{leaf_hi} exec.word::reverse push.{leaf_lo} exec.word::reverse - # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] + exec.utils::mem_store_double_word dropw dropw + # => [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] # push the global index onto the stack and save it into the memory push.GLOBAL_INDEX_PTR push.{global_index_hi} exec.word::reverse push.{global_index_lo} exec.word::reverse exec.utils::mem_store_double_word dropw dropw drop - # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] - - # push the old CGI chain hash onto the stack and save it into the memory - push.OLD_CGI_CHAIN_HASH_PTR - push.{old_cgi_hash_hi} exec.word::reverse - push.{old_cgi_hash_lo} exec.word::reverse - exec.utils::mem_store_double_word dropw dropw drop - # => [LEAF_VALUE[8], EXPECTED_CGI_HASH[8]] + # => [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] # compute the CGI chain hash - exec.compute_cgi_hash_chain_copy + exec.bridge_in::compute_cgi_hash_chain # => [NEW_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] # assert that the hashes are identical From 38e6ea1ab001beb2db549e557127ed941e5f08ce Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Fri, 6 Mar 2026 15:40:55 +0300 Subject: [PATCH 07/11] refactor: incapsulate CGI hash computation --- .../asm/agglayer/bridge/bridge_in.masm | 77 +++++++++++-------- .../tests/agglayer/cgi_chain_hash.rs | 1 + 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 06a6c02c26..3fbd402bc4 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -194,30 +194,12 @@ pub proc verify_leaf_bridge exec.verify_leaf # => [pad(16)] - # compute the claimed global index hash chain - # - # this hash is computed as: - # NEW_CGI_CHAIN_HASH[8] = Keccak256(OLD_CGI_CHAIN_HASH[8], Keccak256(GLOBAL_INDEX[8], LEAF_VALUE[8])) - - # load the old CGI chain hash - push.CGI_CHAIN_HASH_HI_SLOT_NAME[0..2] - exec.active_account::get_item - # => [OLD_CGI_CHAIN_HASH_HI, pad(16)] - - push.CGI_CHAIN_HASH_LO_SLOT_NAME[0..2] - exec.active_account::get_item - # => [OLD_CGI_CHAIN_HASH[8], pad(16)] - - # load the pointer to the leaf value onto the stack + # load the pointer to the leaf value back to the stack locaddr.0 - # => [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8], pad(16)] + # => [leaf_value_ptr, pad(16)] - # compute the claimed global index chain hash - exec.compute_cgi_hash_chain - # => [NEW_CGI_CHAIN_HASH[8], pad(16)] - - # store the computed CGI chain hash - exec.store_cgi_hash_chain + # update the CGI chain hash + exec.update_cgi_chain_hash # => [pad(16)] end @@ -811,31 +793,58 @@ proc calculate_root( # => [ROOT_LO, ROOT_HI] end -#! Computes the claimed global index (CGI) chain hash. +#! 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, OLD_CGI_CHAIN_HASH[8]] -#! Outputs: [NEW_CGI_CHAIN_HASH[8]] +#! Inputs: [leaf_value_ptr] +#! Outputs: [] #! #! Invocation: exec -pub proc compute_cgi_hash_chain - # load the leaf value onto the stack - 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 +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)] - # compute the new CGI chain hash 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. @@ -844,7 +853,7 @@ end #! Outputs: [] #! #! Invocation: exec -proc store_cgi_hash_chain +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] diff --git a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs index c00f67ac85..6de4cc9ec2 100644 --- a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs +++ b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs @@ -11,6 +11,7 @@ use super::test_utils::{CGIChainHashTestData, CLAIMED_GLOBAL_INDEX_HASH_CHAIN}; /// Checks the correctness of the claimed global index chain hash computation, used during the CLAIM /// note execution. #[tokio::test] +#[ignore] async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> { let cgi_chain_hash_data: &CGIChainHashTestData = &CLAIMED_GLOBAL_INDEX_HASH_CHAIN; From 9892099bb7caabac311252b1e1355fff7064ab76 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Thu, 12 Mar 2026 01:24:22 +0300 Subject: [PATCH 08/11] test: update solidity contracts to generate CGI chain hash. Test is failing --- Makefile | 1 - .../claim_asset_vectors_local_tx.json | 1 + .../claim_asset_vectors_real_tx.json | 1 + .../claimed_global_index_hash_chain.json | 6 - .../test/ClaimAssetTestVectorsLocalTx.t.sol | 80 +++++++++- .../test/ClaimAssetTestVectorsRealTx.t.sol | 115 +++++++++++++-- .../ClaimedGlobalIndexHashChainVectors.t.sol | 139 ------------------ crates/miden-agglayer/src/lib.rs | 12 ++ .../miden-testing/tests/agglayer/bridge_in.rs | 38 ++++- .../tests/agglayer/cgi_chain_hash.rs | 106 ------------- crates/miden-testing/tests/agglayer/mod.rs | 1 - .../tests/agglayer/test_utils.rs | 30 +--- 12 files changed, 237 insertions(+), 293 deletions(-) delete mode 100644 crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json delete mode 100644 crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol delete mode 100644 crates/miden-testing/tests/agglayer/cgi_chain_hash.rs diff --git a/Makefile b/Makefile index c663f40a79..019bbd2bf9 100644 --- a/Makefile +++ b/Makefile @@ -151,7 +151,6 @@ generate-solidity-test-vectors: ## Regenerate Solidity MMR test vectors using Fo 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 - cd crates/miden-agglayer/solidity-compat && forge test -vv --match-test test_generateClaimedGlobalIndexHashChainVectors # --- benchmarking -------------------------------------------------------------------------------- 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 cbea883d75..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", + "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..1abd072f09 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": "0x2180b446fe3cc5d336ddb3f4d9b7ca8a7ffe1d1e669173c3de8b56b8a290fd6e", "destination_address": "0x00000000b0E79c68cafC54802726C6F102Cca300", "destination_network": 20, "global_exit_root": "0xe1cbfbde30bd598ee9aa2ac913b60d53e3297e51ed138bf86c500dd7d2391e7d", diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json b/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json deleted file mode 100644 index 0a4612faf8..0000000000 --- a/crates/miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "cgi_chain_hash": "0xbce0afc98c69ea85e9cfbf98c87c58a77c12d857551f1858530341392f70c22d", - "global_index": "0x0000000000000000000000000000000000000000000000010000000000000000", - "leaf": "0x9d85d7c56264697df18f458b4b12a457b87b7e7f7a9b16dcb368514729ef680d", - "old_cgi_chain_hash": "0x0000000000000000000000000000000000000000000000000000000000000000" -} \ No newline at end of file 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/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol deleted file mode 100644 index a8c7cffcb9..0000000000 --- a/crates/miden-agglayer/solidity-compat/test/ClaimedGlobalIndexHashChainVectors.t.sol +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "forge-std/Test.sol"; -import "@agglayer/v2/sovereignChains/BridgeL2SovereignChain.sol"; -import "@agglayer/lib/GlobalExitRootLib.sol"; -import "@agglayer/interfaces/IBasePolygonZkEVMGlobalExitRoot.sol"; -import "./DepositContractTestHelpers.sol"; - -contract MockGlobalExitRootManager is IBasePolygonZkEVMGlobalExitRoot { - mapping(bytes32 => uint256) public override globalExitRootMap; - - function updateExitRoot(bytes32) external override {} - - function setGlobalExitRoot(bytes32 globalExitRoot) external { - globalExitRootMap[globalExitRoot] = block.number; - } -} - -/** - * @title ClaimedGlobalIndexHashChainVectors - * @notice Generates a test vector for claimedGlobalIndexHashChain using _verifyLeafBridge. - * - * Run with: forge test -vv --match-test test_generateClaimedGlobalIndexHashChainVectors - */ -contract ClaimedGlobalIndexHashChainVectors is Test, BridgeL2SovereignChain, DepositContractTestHelpers { - function test_generateClaimedGlobalIndexHashChainVectors() public { - string memory obj = "root"; - - // ====== BRIDGE TRANSACTION PARAMETERS ====== - uint8 leafType = 0; - uint32 originNetwork = 0; - address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; - uint32 destinationNetwork = 20; - address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; - uint256 amount = 100000000000000000000; - - bytes memory metadata = abi.encode("Test Token", "TEST", uint8(18)); - bytes32 metadataHash = keccak256(metadata); - - // ====== COMPUTE LEAF VALUE AND ADD TO TREE ====== - bytes32 leafValue = getLeafValue( - leafType, - originNetwork, - originTokenAddress, - destinationNetwork, - destinationAddress, - amount, - metadataHash - ); - - _addLeaf(leafValue); - uint256 leafIndex = depositCount - 1; - bytes32 localExitRoot = getRoot(); - - // ====== GENERATE MERKLE PROOF ====== - bytes32[32] memory canonicalZeros = _computeCanonicalZeros(); - bytes32[32] memory smtProofLocalExitRoot = - _generateLocalProof(leafIndex, canonicalZeros); - bytes32[32] memory smtProofRollupExitRoot; - - // ====== COMPUTE EXIT ROOTS ====== - bytes32 mainnetExitRoot = localExitRoot; - bytes32 rollupExitRoot = keccak256(abi.encodePacked("rollup_exit_root_simulated")); - bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot( - mainnetExitRoot, - rollupExitRoot - ); - - // ====== SET GLOBAL EXIT ROOT MANAGER ====== - MockGlobalExitRootManager gerManager = new MockGlobalExitRootManager(); - gerManager.setGlobalExitRoot(globalExitRoot); - globalExitRootManager = IBasePolygonZkEVMGlobalExitRoot(address(gerManager)); - - // Use a non-zero network ID to match sovereign-chain requirements - networkID = 10; - - // ====== COMPUTE GLOBAL INDEX ====== - uint256 globalIndex = (uint256(1) << 64) | uint256(leafIndex); - - // ====== COMPUTE CLAIMED GLOBAL INDEX HASH CHAIN ====== - bytes32 oldClaimedHashChain = claimedGlobalIndexHashChain; - - this.verifyLeafBridgeHarness( - smtProofLocalExitRoot, - smtProofRollupExitRoot, - globalIndex, - mainnetExitRoot, - rollupExitRoot, - leafType, - originNetwork, - originTokenAddress, - destinationNetwork, - destinationAddress, - amount, - metadataHash - ); - - bytes32 claimedHashChain = claimedGlobalIndexHashChain; - - // ====== SERIALIZE OUTPUT ====== - vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); - vm.serializeBytes32(obj, "leaf", leafValue); - vm.serializeBytes32(obj, "cgi_chain_hash", claimedHashChain); - string memory json = vm.serializeBytes32(obj, "old_cgi_chain_hash", oldClaimedHashChain); - - vm.writeJson(json, "test-vectors/claimed_global_index_hash_chain.json"); - } - - 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/lib.rs b/crates/miden-agglayer/src/lib.rs index 3789fd5886..13e971627a 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -175,6 +175,8 @@ static CGI_CHAIN_HASH_HI_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. @@ -234,6 +236,16 @@ impl AggLayerBridge { pub fn ger_manager_slot_name() -> &'static StorageSlotName { &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 + } } impl From for AccountComponent { diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 94aaf59d78..d83b85a3b2 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -23,7 +23,7 @@ use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::NoteType; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use miden_protocol::transaction::OutputNote; -use miden_protocol::{Felt, FieldElement}; +use miden_protocol::{Felt, FieldElement, Word}; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::P2idNote; @@ -63,6 +63,7 @@ use super::test_utils::{ #[case::simulated(ClaimDataSource::Simulated)] #[tokio::test] async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { + use miden_agglayer::AggLayerBridge; use miden_protocol::account::auth::AuthScheme; let mut builder = MockChain::builder(); @@ -86,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. @@ -226,6 +227,26 @@ 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_lo = updated_bridge_account + .storage() + .get_item(AggLayerBridge::cgi_lo_slot_name()) + .expect("failed to get CGI hash chain lo slot"); + let actual_cgi_chain_hash_hi = updated_bridge_account + .storage() + .get_item(AggLayerBridge::cgi_hi_slot_name()) + .expect("failed to get CGI hash chain hi slot"); + + let actual_cgi_chain_hash = + two_words_to_cgi_chain_hash(actual_cgi_chain_hash_lo, actual_cgi_chain_hash_hi); + + 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); @@ -406,3 +427,16 @@ fn merkle_proof_verification_code( "# ) } + +// TODO: this procedure should be removed in favor of Agglayer Bridge helper procedure, which should +// be created in a follow up PR after https://github.com/0xMiden/protocol/pull/2562 +fn two_words_to_cgi_chain_hash(hash_lo: Word, hash_hi: Word) -> Keccak256Output { + let hash_bytes = hash_lo + .iter() + .chain(hash_hi.iter()) + .flat_map(|felt| (felt.as_int() as u32).to_le_bytes()) + .collect::>(); + Keccak256Output::new( + hash_bytes.try_into().expect("keccak hash should consist of exactly 32 bytes"), + ) +} diff --git a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs b/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs deleted file mode 100644 index 6de4cc9ec2..0000000000 --- a/crates/miden-testing/tests/agglayer/cgi_chain_hash.rs +++ /dev/null @@ -1,106 +0,0 @@ -extern crate alloc; - -use miden_agglayer::claim_note::Keccak256Output; -use miden_agglayer::{GlobalIndex, agglayer_library}; -use miden_standards::code_builder::CodeBuilder; -use miden_testing::TransactionContextBuilder; -use miden_tx::utils::hex_to_bytes; - -use super::test_utils::{CGIChainHashTestData, CLAIMED_GLOBAL_INDEX_HASH_CHAIN}; - -/// Checks the correctness of the claimed global index chain hash computation, used during the CLAIM -/// note execution. -#[tokio::test] -#[ignore] -async fn compute_cgi_hash_chain_matches_solidity_vector() -> anyhow::Result<()> { - let cgi_chain_hash_data: &CGIChainHashTestData = &CLAIMED_GLOBAL_INDEX_HASH_CHAIN; - - let [global_index_lo, global_index_hi] = - GlobalIndex::from_hex(&cgi_chain_hash_data.global_index) - .expect("valid global index hex") - .to_words(); - - let leaf_bytes: [u8; 32] = - hex_to_bytes(&cgi_chain_hash_data.leaf).expect("leaf value must be 32 bytes"); - let [leaf_lo, leaf_hi] = Keccak256Output::from(leaf_bytes).to_words(); - - let expected_cgi_hash_bytes: [u8; 32] = - hex_to_bytes(&cgi_chain_hash_data.cgi_chain_hash).expect("claimed hash must be 32 bytes"); - let [expected_cgi_hash_lo, expected_cgi_hash_hi] = - Keccak256Output::from(expected_cgi_hash_bytes).to_words(); - - let old_cgi_hash_bytes: [u8; 32] = hex_to_bytes(&cgi_chain_hash_data.old_cgi_chain_hash) - .expect("claimed hash must be 32 bytes"); - let [old_cgi_hash_lo, old_cgi_hash_hi] = Keccak256Output::from(old_cgi_hash_bytes).to_words(); - - let source = format!( - r#" - use miden::core::crypto::hashes::keccak256 - use miden::core::word - use miden::core::sys - - use miden::agglayer::bridge::bridge_in - use miden::agglayer::common::utils - - const LEAF_VALUE_PTR = 0 - const GLOBAL_INDEX_PTR = 512 - - begin - # push the expected hash onto the stack - push.{expected_cgi_hash_hi} exec.word::reverse - push.{expected_cgi_hash_lo} exec.word::reverse - # => [EXPECTED_CGI_HASH[8]] - - # push the old CGI chain hash onto the stack - push.{old_cgi_hash_hi} exec.word::reverse - push.{old_cgi_hash_lo} exec.word::reverse - # => [OLD_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] - - # push the leaf value onto the stack and save it into the memory - push.LEAF_VALUE_PTR - push.{leaf_hi} exec.word::reverse - push.{leaf_lo} exec.word::reverse - exec.utils::mem_store_double_word dropw dropw - # => [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] - - # push the global index onto the stack and save it into the memory - push.GLOBAL_INDEX_PTR - push.{global_index_hi} exec.word::reverse - push.{global_index_lo} exec.word::reverse - exec.utils::mem_store_double_word dropw dropw drop - # => [leaf_value_ptr, OLD_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] - - # compute the CGI chain hash - exec.bridge_in::compute_cgi_hash_chain - # => [NEW_CGI_CHAIN_HASH[8], EXPECTED_CGI_HASH[8]] - - # assert that the hashes are identical - # => [NEW_CGI_CHAIN_HASH_LO, NEW_CGI_CHAIN_HASH_HI, EXPECTED_CGI_HASH_LO, EXPECTED_CGI_HASH_HI] - - swapw.3 - # => [EXPECTED_CGI_HASH_HI, NEW_CGI_CHAIN_HASH_HI, EXPECTED_CGI_HASH_LO, NEW_CGI_CHAIN_HASH_LO] - - assert_eqw.err="CGI chain hash (HI) is incorrect" - # => [EXPECTED_CGI_HASH_LO, NEW_CGI_CHAIN_HASH_LO] - - assert_eqw.err="CGI chain hash (LO) is incorrect" - # => [] - - exec.sys::truncate_stack - # => [] - end - "# - ); - - let tx_script = CodeBuilder::new() - .with_statically_linked_library(&agglayer_library())? - .compile_tx_script(source)?; - - TransactionContextBuilder::with_existing_mock_account() - .tx_script(tx_script.clone()) - .build()? - .execute() - .await?; - - Ok(()) -} diff --git a/crates/miden-testing/tests/agglayer/mod.rs b/crates/miden-testing/tests/agglayer/mod.rs index 0f61653b57..a497f74230 100644 --- a/crates/miden-testing/tests/agglayer/mod.rs +++ b/crates/miden-testing/tests/agglayer/mod.rs @@ -1,7 +1,6 @@ pub mod asset_conversion; mod bridge_in; mod bridge_out; -mod cgi_chain_hash; mod config_bridge; mod global_index; mod leaf_utils; diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 82d44557c3..ff8c28dc99 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -54,11 +54,6 @@ pub const CANONICAL_ZEROS_JSON: &str = pub const MMR_FRONTIER_VECTORS_JSON: &str = include_str!("../../../miden-agglayer/solidity-compat/test-vectors/mmr_frontier_vectors.json"); -/// Claimed global index hash chain JSON from the Foundry-generated file. -pub const CLAIMED_GLOBAL_INDEX_HASH_CHAIN_JSON: &str = include_str!( - "../../../miden-agglayer/solidity-compat/test-vectors/claimed_global_index_hash_chain.json" -); - // SERDE HELPERS // ================================================================================================ @@ -111,6 +106,7 @@ pub struct LeafValueVector { pub metadata_hash: String, #[allow(dead_code)] pub leaf_value: String, + pub claimed_global_index_hash_chain: String, } impl LeafValueVector { @@ -190,15 +186,6 @@ pub struct ClaimAssetVector { pub leaf: LeafValueVector, } -/// Deserialized claimed global index hash chain data from Solidity-generated JSON. -#[derive(Debug, Deserialize)] -pub struct CGIChainHashTestData { - pub global_index: String, - pub leaf: String, - pub cgi_chain_hash: String, - pub old_cgi_chain_hash: String, -} - /// 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. @@ -250,12 +237,6 @@ pub static CLAIM_ASSET_VECTOR_LOCAL: LazyLock = LazyLock::new( .expect("failed to parse bridge asset vectors JSON") }); -/// Lazily parsed claimed global index hash chain data from the JSON file. -pub static CLAIMED_GLOBAL_INDEX_HASH_CHAIN: LazyLock = LazyLock::new(|| { - serde_json::from_str(CLAIMED_GLOBAL_INDEX_HASH_CHAIN_JSON) - .expect("failed to parse claimed global index hash chain vector JSON") -}); - /// Lazily parsed Merkle proof vectors from the JSON file. pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = LazyLock::new(|| { @@ -288,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, @@ -296,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.leaf.claimed_global_index_hash_chain) + .expect("invalid CGI chain hash"), + ); + + (vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger, cgi_chain_hash) } } From 877b06ab3cdff50894feb480404f1797b81bd49d Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Thu, 12 Mar 2026 15:56:26 +0300 Subject: [PATCH 09/11] test: fix bug, test is passing --- crates/miden-agglayer/src/lib.rs | 27 ++++++++++++++++++ .../miden-testing/tests/agglayer/bridge_in.rs | 28 ++----------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index 13e971627a..c4bc39f751 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -46,6 +46,8 @@ pub use eth_types::{ }; pub use update_ger_note::UpdateGerNote; +use crate::claim_note::Keccak256Output; + // AGGLAYER NOTE SCRIPTS // ================================================================================================ @@ -246,6 +248,31 @@ impl AggLayerBridge { pub fn cgi_hi_slot_name() -> &'static StorageSlotName { &CGI_CHAIN_HASH_HI_SLOT_NAME } + + /// TODO: update this procedure during the https://github.com/0xMiden/protocol/issues/2580 after + /// https://github.com/0xMiden/protocol/pull/2562 PR is merged + pub fn cgi_chain_hash(bridge_account: &Account) -> Keccak256Output { + 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::>(); + Keccak256Output::new( + cgi_chain_hash_bytes + .try_into() + .expect("keccak hash should consist of exactly 32 bytes"), + ) + } } impl From for AccountComponent { diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 613a6f95cb..3794da931d 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -25,7 +25,7 @@ use miden_protocol::crypto::rand::FeltRng; use miden_protocol::note::NoteType; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; use miden_protocol::transaction::OutputNote; -use miden_protocol::{Felt, FieldElement, Word}; +use miden_protocol::{Felt, FieldElement}; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::P2idNote; @@ -65,7 +65,6 @@ use super::test_utils::{ #[case::real(ClaimDataSource::Real)] #[case::simulated(ClaimDataSource::Simulated)] #[tokio::test] -#[ignore = "WiP"] async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { let mut builder = MockChain::builder(); @@ -235,17 +234,7 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a let mut updated_bridge_account = bridge_account.clone(); updated_bridge_account.apply_delta(claim_executed.account_delta())?; - let actual_cgi_chain_hash_lo = updated_bridge_account - .storage() - .get_item(AggLayerBridge::cgi_lo_slot_name()) - .expect("failed to get CGI hash chain lo slot"); - let actual_cgi_chain_hash_hi = updated_bridge_account - .storage() - .get_item(AggLayerBridge::cgi_hi_slot_name()) - .expect("failed to get CGI hash chain hi slot"); - - let actual_cgi_chain_hash = - two_words_to_cgi_chain_hash(actual_cgi_chain_hash_lo, actual_cgi_chain_hash_hi); + let actual_cgi_chain_hash = AggLayerBridge::cgi_chain_hash(&updated_bridge_account); assert_eq!(cgi_chain_hash, actual_cgi_chain_hash); @@ -429,16 +418,3 @@ fn merkle_proof_verification_code( "# ) } - -// TODO: this procedure should be removed in favor of Agglayer Bridge helper procedure, which should -// be created in a follow up PR after https://github.com/0xMiden/protocol/pull/2562 -fn two_words_to_cgi_chain_hash(hash_lo: Word, hash_hi: Word) -> Keccak256Output { - let hash_bytes = hash_lo - .iter() - .chain(hash_hi.iter()) - .flat_map(|felt| (felt.as_int() as u32).to_le_bytes()) - .collect::>(); - Keccak256Output::new( - hash_bytes.try_into().expect("keccak hash should consist of exactly 32 bytes"), - ) -} From a40feb91e471207acf33f9495e61e4e5431e700f Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Thu, 12 Mar 2026 16:03:54 +0300 Subject: [PATCH 10/11] chore: fix doc format --- crates/miden-agglayer/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-agglayer/src/lib.rs b/crates/miden-agglayer/src/lib.rs index c4bc39f751..34179fbe3e 100644 --- a/crates/miden-agglayer/src/lib.rs +++ b/crates/miden-agglayer/src/lib.rs @@ -249,8 +249,8 @@ impl AggLayerBridge { &CGI_CHAIN_HASH_HI_SLOT_NAME } - /// TODO: update this procedure during the https://github.com/0xMiden/protocol/issues/2580 after - /// https://github.com/0xMiden/protocol/pull/2562 PR is merged + /// TODO: update this procedure during the + /// after PR is merged pub fn cgi_chain_hash(bridge_account: &Account) -> Keccak256Output { let cgi_chain_hash_lo = bridge_account .storage() From be00e01e8dec36fefec41327be4a05c430580c69 Mon Sep 17 00:00:00 2001 From: Andrey Khmuro Date: Mon, 16 Mar 2026 19:27:52 +0300 Subject: [PATCH 11/11] refactor: remove unused code, update list of bridge storage slots, update docs --- crates/miden-agglayer/src/bridge.rs | 4 +++- .../miden-agglayer/src/eth_types/global_index.rs | 15 +-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/crates/miden-agglayer/src/bridge.rs b/crates/miden-agglayer/src/bridge.rs index 51326c91a5..6eb7a9a267 100644 --- a/crates/miden-agglayer/src/bridge.rs +++ b/crates/miden-agglayer/src/bridge.rs @@ -183,7 +183,7 @@ impl AggLayerBridge { &CGI_CHAIN_HASH_LO_SLOT_NAME } - /// Storage slot name for the lower 128 bits of the CGI chain hash. + /// Storage slot name for the upper 128 bits of the CGI chain hash. pub fn cgi_hi_slot_name() -> &'static StorageSlotName { &CGI_CHAIN_HASH_HI_SLOT_NAME } @@ -387,6 +387,8 @@ impl AggLayerBridge { &*TOKEN_REGISTRY_SLOT_NAME, &*BRIDGE_ADMIN_SLOT_NAME, &*GER_MANAGER_SLOT_NAME, + &*CGI_CHAIN_HASH_LO_SLOT_NAME, + &*CGI_CHAIN_HASH_HI_SLOT_NAME, ] } } diff --git a/crates/miden-agglayer/src/eth_types/global_index.rs b/crates/miden-agglayer/src/eth_types/global_index.rs index 66d66dd2aa..4c4688edc0 100644 --- a/crates/miden-agglayer/src/eth_types/global_index.rs +++ b/crates/miden-agglayer/src/eth_types/global_index.rs @@ -1,9 +1,8 @@ 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 @@ -100,18 +99,6 @@ 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)]