diff --git a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm index 53a17ded37..2348e3bad5 100644 --- a/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm +++ b/crates/miden-agglayer/asm/agglayer/bridge/bridge_in.masm @@ -15,35 +15,40 @@ type MemoryAddress = u32 # ERRORS # ================================================================================================= -const ERR_BRIDGE_NOT_MAINNET = "bridge not mainnet" +const ERR_BRIDGE_NOT_MAINNET = "mainnet flag must be 1 for a mainnet deposit" +const ERR_BRIDGE_NOT_ROLLUP = "mainnet flag must be 0 for a rollup deposit" const ERR_LEADING_BITS_NON_ZERO = "leading bits of global index must be zero" +const ERR_MAINNET_FLAG_INVALID = "mainnet flag must be 0 or 1" const ERR_ROLLUP_INDEX_NON_ZERO = "rollup index must be zero for a mainnet deposit" const ERR_SMT_ROOT_VERIFICATION_FAILED = "merkle proof verification failed: provided SMT root does not match the computed root" # CONSTANTS # ================================================================================================= -# Memory pointers for proof data layout +# Memory layout for proof data (loaded from advice map via pipe_preimage_to_memory). +# The proof data occupies addresses [PROOF_DATA_PTR .. PROOF_DATA_PTR + 535]: +# [0..255] smtProofLocalExitRoot (256 felts = 32 Keccak256 nodes) +# [256..511] smtProofRollupExitRoot (256 felts = 32 Keccak256 nodes) +# [512..519] globalIndex (8 felts) +# [520..527] mainnetExitRoot (8 felts) +# [528..535] rollupExitRoot (8 felts) const PROOF_DATA_PTR = 0 -const SMT_PROOF_LOCAL_EXIT_ROOT_PTR = 0 # local SMT proof is first -const GLOBAL_INDEX_PTR = PROOF_DATA_PTR + 2 * 256 # 512 -const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 # 520 -const MAINNET_EXIT_ROOT_PTR = EXIT_ROOTS_PTR # it's the first exit root - -# the memory address where leaf data is stored for get_leaf_value +const SMT_PROOF_LOCAL_EXIT_ROOT_PTR = PROOF_DATA_PTR +const SMT_PROOF_ROLLUP_EXIT_ROOT_PTR = SMT_PROOF_LOCAL_EXIT_ROOT_PTR + 256 +const GLOBAL_INDEX_PTR = SMT_PROOF_ROLLUP_EXIT_ROOT_PTR + 256 +const EXIT_ROOTS_PTR = GLOBAL_INDEX_PTR + 8 +const MAINNET_EXIT_ROOT_PTR = EXIT_ROOTS_PTR +const ROLLUP_EXIT_ROOT_PTR = EXIT_ROOTS_PTR + 8 + +# Memory layout for leaf data (loaded separately via get_leaf_value) const LEAF_DATA_START_PTR = 0 -# The offset of the first half of the current Keccak256 hash value in the local memory of the -# `calculate_root` procedure. +# Local memory offsets for the `calculate_root` procedure's current hash const CUR_HASH_LO_LOCAL = 0 - -# The offset of the second half of the current Keccak256 hash value in the local memory of the -# `calculate_root` procedure. const CUR_HASH_HI_LOCAL = 4 # Data sizes const PROOF_DATA_WORD_LEN = 134 -# the number of words (4 felts each) in the advice map leaf data const LEAF_DATA_NUM_WORDS = 8 # PUBLIC INTERFACE @@ -116,17 +121,16 @@ end #! #! Invocation: exec pub proc process_global_index_mainnet - # for v0.1, let's only implement the mainnet branch # the top 191 bits of the global index are zero repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end # the next element is the mainnet flag (LE-packed u32) # byte-swap to get the BE value, then assert it is exactly 1 - # => [mainnet_flag_le, rollup_index_le, leaf_index_le, ...] + # => [mainnet_flag_le, rollup_index_le, leaf_index_le] exec.utils::swap_u32_bytes assert.err=ERR_BRIDGE_NOT_MAINNET - # the next element is the rollup index, must be zero for a mainnet deposit + # the rollup index must be zero for a mainnet deposit # (zero is byte-order-independent, so no swap needed) assertz.err=ERR_ROLLUP_INDEX_NON_ZERO @@ -135,6 +139,38 @@ pub proc process_global_index_mainnet # => [leaf_index] end +#! Assert the global index is valid for a rollup deposit. +#! +#! Each element of the global index is a LE-packed u32 felt (as produced by +#! `bytes_to_packed_u32_felts` / `GlobalIndex::to_elements()`). +#! +#! Inputs: [GLOBAL_INDEX[8]] +#! Outputs: [leaf_index, rollup_index] +#! +#! Panics if: +#! - the leading bits of the global index are not zero. +#! - the mainnet flag is not 0. +#! +#! Invocation: exec +pub proc process_global_index_rollup + # the top 191 bits of the global index are zero + repeat.5 assertz.err=ERR_LEADING_BITS_NON_ZERO end + + # the next element is the mainnet flag (LE-packed u32) + # for a rollup deposit it must be exactly 0; zero is byte-order-independent, + # so no swap is needed before asserting + # => [mainnet_flag_le, rollup_index_le, leaf_index_le] + assertz.err=ERR_BRIDGE_NOT_ROLLUP + + # byte-swap rollup_index from LE to BE + exec.utils::swap_u32_bytes + # => [rollup_index, leaf_index_le] + + # byte-swap leaf_index from LE to BE + swap exec.utils::swap_u32_bytes + # => [leaf_index, rollup_index] +end + #! Computes the Global Exit Tree (GET) root from the mainnet and rollup exit roots. #! #! The mainnet exit root is expected at `exit_roots_ptr` and @@ -263,27 +299,69 @@ proc verify_leaf padw push.GLOBAL_INDEX_PTR add.4 mem_loadw_le swapw # => [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 - # TODO currently only implemented for mainnet deposits (mainnet flag must be 1) - exec.process_global_index_mainnet - # => [leaf_index, LEAF_VALUE[8]] + # Determine if we're dealing with a deposit from mainnet or from a rollup. + # The global index is laid out as: + # [gi0, gi1, gi2, gi3, gi4, mainnet_flag_le, rollup_index_le, leaf_index_le] + # gi0 is on top (position 0). The mainnet flag is at stack position 5. - # load the pointers to the merkle proof and root, to pass to `verify_merkle_proof` - push.MAINNET_EXIT_ROOT_PTR swap - push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR - # => [smt_proof_ptr, leaf_index, mainnet_exit_root_ptr, LEAF_VALUE[8]] + # Duplicate the mainnet flag element, byte-swap from LE to BE, + # assert it is a valid boolean (0 or 1), then use it to branch. + dup.5 exec.utils::swap_u32_bytes dup + # => [mainnet_flag, mainnet_flag, GLOBAL_INDEX[8], LEAF_VALUE[8]] - # prepare the stack for the verify_merkle_proof procedure: move the pointers deep in the stack - movdn.10 movdn.10 movdn.10 - # => [LEAF_VALUE[8], smt_proof_ptr, leaf_index, mainnet_exit_root_ptr] + u32lt.2 assert.err=ERR_MAINNET_FLAG_INVALID + # => [mainnet_flag, GLOBAL_INDEX[8], LEAF_VALUE[8]] - exec.verify_merkle_proof - # => [verification_flag] + if.true + # ==================== MAINNET DEPOSIT ==================== + exec.process_global_index_mainnet + # => [leaf_index, LEAF_VALUE[8]] - # verify_merkle_proof procedure returns `true` if the verification was successful and `false` - # otherwise. Assert that `true` was returned. - assert.err=ERR_SMT_ROOT_VERIFICATION_FAILED - # => [] + # verify single Merkle proof: leaf against mainnetExitRoot + push.MAINNET_EXIT_ROOT_PTR swap + push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR + # => [smt_proof_ptr, leaf_index, mainnet_exit_root_ptr, LEAF_VALUE[8]] + + movdn.10 movdn.10 movdn.10 + # => [LEAF_VALUE[8], smt_proof_ptr, leaf_index, mainnet_exit_root_ptr] + + exec.verify_merkle_proof + # => [verification_flag] + + assert.err=ERR_SMT_ROOT_VERIFICATION_FAILED + # => [] + else + # ==================== ROLLUP DEPOSIT ==================== + # mainnet_flag = 0; extract rollup_index and leaf_index via helper, + # then do two-level verification + exec.process_global_index_rollup + # => [leaf_index, rollup_index, LEAF_VALUE[8]] + + # Step 1: calculate_root(leafValue, smtProofLocalExitRoot, leafIndex) -> localExitRoot + # calculate_root expects: [LEAF_VALUE_LO, LEAF_VALUE_HI, merkle_path_ptr, leaf_index] + movdn.9 movdn.9 + # => [LEAF_VALUE[8], leaf_index, rollup_index] + + # Insert smt_proof_local_ptr before leaf_index + push.SMT_PROOF_LOCAL_EXIT_ROOT_PTR movdn.8 + # => [LEAF_VALUE[8], smt_proof_local_ptr, leaf_index, rollup_index] + + exec.calculate_root + # => [LOCAL_EXIT_ROOT_LO, LOCAL_EXIT_ROOT_HI, rollup_index] + + # Step 2: verify_merkle_proof(localExitRoot, smtProofRollupExitRoot, rollupIndex, rollupExitRootPtr) + push.ROLLUP_EXIT_ROOT_PTR movdn.9 + # => [LOCAL_EXIT_ROOT_LO, LOCAL_EXIT_ROOT_HI, rollup_index, rollup_exit_root_ptr] + push.SMT_PROOF_ROLLUP_EXIT_ROOT_PTR movdn.8 + + # => [LOCAL_EXIT_ROOT[8], smt_proof_rollup_ptr, rollup_index, rollup_exit_root_ptr] + + exec.verify_merkle_proof + # => [verification_flag] + + assert.err=ERR_SMT_ROOT_VERIFICATION_FAILED + # => [] + end end #! Computes the root of the SMT based on the provided Merkle path, leaf value and leaf index. diff --git a/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json new file mode 100644 index 0000000000..78b6c0c017 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json @@ -0,0 +1,86 @@ +{ + "amount": "100000000000000000000", + "deposit_count": 3, + "description": "Rollup deposit test vectors with valid two-level Merkle proofs (non-zero indices)", + "destination_address": "0x00000000AA0000000000bb000000cc000000Dd00", + "destination_network": 20, + "global_exit_root": "0x677d4ecba0ff4871f33163e70ea39a13fe97f2fa9b4dbad110e398830a324159", + "global_index": "0x0000000000000000000000000000000000000000000000000000000500000002", + "leaf_type": 0, + "leaf_value": "0x4a6a047a2b89dd9c557395833c5e34c4f72e6f9aae70779e856f14a6a2827585", + "local_exit_root": "0x985cff7ee35794b30fba700b64546b4ec240d2d78aaf356d56e83d907009367f", + "mainnet_exit_root": "0x4d63440b08ffffe5a049aae4161d54821a09973965a1a1728534a0f117b6d6c9", + "metadata": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000a5465737420546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045445535400000000000000000000000000000000000000000000000000000000", + "metadata_hash": "0x4d0d9fb7f9ab2f012da088dc1c228173723db7e09147fe4fea2657849d580161", + "origin_network": 3, + "origin_token_address": "0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF", + "rollup_exit_root": "0x91105681934ca0791f4e760fb1f702050d81e4b7c866d42f540710999c90ea97", + "smt_proof_local_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xa8367b4263332f7e5453faa770f07ef4ce3e74fc411e0a788a98b38b91fd3b3e", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344", + "0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d", + "0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968", + "0xffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f83", + "0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af", + "0xcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0", + "0xf9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5", + "0xf8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf892", + "0x3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c", + "0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb", + "0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc", + "0xda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d2", + "0x2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f", + "0xe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a", + "0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0", + "0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0", + "0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2", + "0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9", + "0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377", + "0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652", + "0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef", + "0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d", + "0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0", + "0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e", + "0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e", + "0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322", + "0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735", + "0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9" + ], + "smt_proof_rollup_exit_root": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30", + "0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85", + "0xe58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a19344", + "0x0eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d", + "0x887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968", + "0xffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f83", + "0x9867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756af", + "0xcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0", + "0xf9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5", + "0xf8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf892", + "0x3490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99c", + "0xc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb", + "0x5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8becc", + "0xda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d2", + "0x2733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981f", + "0xe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a", + "0x5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0", + "0xb46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0", + "0xc65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2", + "0xf4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd9", + "0x5a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e377", + "0x4df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652", + "0xcdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef", + "0x0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618d", + "0xb8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0", + "0x838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e", + "0x662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e", + "0x388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea322", + "0x93237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d735", + "0x8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9" + ] +} \ No newline at end of file diff --git a/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol new file mode 100644 index 0000000000..9e740a8d37 --- /dev/null +++ b/crates/miden-agglayer/solidity-compat/test/ClaimAssetTestVectorsRollupTx.t.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@agglayer/v2/lib/DepositContractV2.sol"; +import "@agglayer/lib/GlobalExitRootLib.sol"; +import "./DepositContractTestHelpers.sol"; + +/** + * @title RollupExitTree + * @notice Simulates the rollup exit tree from PolygonRollupManager. + * Each registered rollup has a fixed slot (rollupID - 1). The tree is a depth-32 + * Merkle tree where unregistered positions contain zero leaves. + * See PolygonRollupManager.getRollupExitRoot() for the production implementation. + */ +contract RollupExitTree is DepositContractBase, DepositContractTestHelpers { + /// @notice Place a local exit root at a specific rollup index (= rollupID - 1). + /// Earlier positions are filled with zero leaves (unregistered rollups). + function setLocalExitRootAt(bytes32 localExitRoot, uint256 rollupIndex) external { + for (uint256 i = 0; i < rollupIndex; i++) { + _addLeaf(bytes32(0)); + } + _addLeaf(localExitRoot); + } + + function generateProof(uint256 leafIndex) external view returns (bytes32[32] memory) { + bytes32[32] memory canonicalZeros = _computeCanonicalZeros(); + return _generateLocalProof(leafIndex, canonicalZeros); + } +} + +/** + * @title ClaimAssetTestVectorsRollupTx + * @notice Test contract that generates test vectors for a rollup deposit (mainnet_flag=0). + * This simulates a deposit on a rollup chain whose local exit root is then included + * in the rollup exit tree, requiring two-level Merkle proof verification. + * + * Uses non-zero leafIndex and indexRollup to exercise byte-ordering paths. + * + * Run with: forge test -vv --match-contract ClaimAssetTestVectorsRollupTx + * + * The output can be used to verify Miden's ability to process rollup bridge transactions. + */ +contract ClaimAssetTestVectorsRollupTx is Test, DepositContractV2, DepositContractTestHelpers { + /** + * @notice Generates rollup deposit test vectors with valid two-level Merkle proofs. + * + * Output file: test-vectors/claim_asset_vectors_rollup_tx.json + */ + function test_generateClaimAssetVectorsRollupTx() public { + string memory obj = "root"; + + // ====== BRIDGE TRANSACTION PARAMETERS ====== + + uint8 leafType = 0; + uint32 originNetwork = 3; // rollup network ID + address originTokenAddress = 0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF; + uint32 destinationNetwork = 20; + // Destination address with zero MSB (embeds a Miden AccountId) + address destinationAddress = 0x00000000AA0000000000bb000000cc000000Dd00; + uint256 amount = 100000000000000000000; + + bytes memory metadata = abi.encode("Test Token", "TEST", uint8(18)); + bytes32 metadataHash = keccak256(metadata); + + // ====== STEP 1: BUILD THE ROLLUP'S LOCAL EXIT TREE ====== + // Add dummy leaves before the target to get a non-zero leafIndex, + // exercising byte-swap paths in the MASM verification. + + bytes32 leafValue = getLeafValue( + leafType, originNetwork, originTokenAddress, destinationNetwork, destinationAddress, amount, metadataHash + ); + + // Add 2 dummy deposits before the real one -> leafIndex = 2 + _addLeaf(keccak256("dummy_deposit_0")); + _addLeaf(keccak256("dummy_deposit_1")); + _addLeaf(leafValue); + + uint256 leafIndex = depositCount - 1; // = 2 + bytes32 localExitRoot = getRoot(); + + // Generate the local exit root proof (leaf -> localExitRoot) + bytes32[32] memory canonicalZeros = _computeCanonicalZeros(); + bytes32[32] memory smtProofLocal = _generateLocalProof(leafIndex, canonicalZeros); + + // Verify local proof is valid + require( + this.verifyMerkleProof(leafValue, smtProofLocal, uint32(leafIndex), localExitRoot), + "Local Merkle proof is invalid!" + ); + + // ====== STEP 2: BUILD THE ROLLUP EXIT TREE ====== + // The rollup exit tree is a sparse Merkle tree where each rollup has a fixed slot + // at position (rollupID - 1). We place our localExitRoot at indexRollup = 5 + // (simulating rollupID = 6, with 5 earlier rollups having no bridge activity). + + RollupExitTree rollupTree = new RollupExitTree(); + + uint256 indexRollup = 5; + rollupTree.setLocalExitRootAt(localExitRoot, indexRollup); + + bytes32 rollupExitRoot = rollupTree.getRoot(); + + // Generate the rollup exit root proof (localExitRoot -> rollupExitRoot) + bytes32[32] memory smtProofRollup = rollupTree.generateProof(indexRollup); + + // Verify rollup proof is valid + require( + rollupTree.verifyMerkleProof(localExitRoot, smtProofRollup, uint32(indexRollup), rollupExitRoot), + "Rollup Merkle proof is invalid!" + ); + + // ====== STEP 3: VERIFY TWO-LEVEL PROOF (matching Solidity _verifyLeaf rollup path) ====== + // For rollup deposits, verification is: + // 1. calculateRoot(leafValue, smtProofLocal, leafIndex) == localExitRoot + // 2. verifyMerkleProof(localExitRoot, smtProofRollup, indexRollup, rollupExitRoot) + + bytes32 computedLocalRoot = this.calculateRoot(leafValue, smtProofLocal, uint32(leafIndex)); + require(computedLocalRoot == localExitRoot, "Two-level step 1 failed: computed local root mismatch"); + require( + this.verifyMerkleProof(computedLocalRoot, smtProofRollup, uint32(indexRollup), rollupExitRoot), + "Two-level step 2 failed: rollup proof verification failed" + ); + + // ====== STEP 4: COMPUTE EXIT ROOTS AND GLOBAL INDEX ====== + + // For a rollup deposit, mainnetExitRoot is arbitrary (simulated) + bytes32 mainnetExitRoot = keccak256(abi.encodePacked("mainnet_exit_root_simulated")); + + // Compute global exit root + bytes32 globalExitRoot = GlobalExitRootLib.calculateGlobalExitRoot(mainnetExitRoot, rollupExitRoot); + + // Global index for rollup deposits: (indexRollup << 32) | leafIndex (no mainnet flag bit) + uint256 globalIndex = (uint256(indexRollup) << 32) | uint256(leafIndex); + + // ====== SERIALIZE TO JSON ====== + _serializeProofs(obj, smtProofLocal, smtProofRollup); + + { + vm.serializeUint(obj, "leaf_type", leafType); + vm.serializeUint(obj, "origin_network", originNetwork); + vm.serializeAddress(obj, "origin_token_address", originTokenAddress); + vm.serializeUint(obj, "destination_network", destinationNetwork); + vm.serializeAddress(obj, "destination_address", destinationAddress); + vm.serializeUint(obj, "amount", amount); + vm.serializeBytes(obj, "metadata", metadata); + vm.serializeBytes32(obj, "metadata_hash", metadataHash); + vm.serializeBytes32(obj, "leaf_value", leafValue); + } + + { + vm.serializeUint(obj, "deposit_count", uint256(depositCount)); + vm.serializeBytes32(obj, "global_index", bytes32(globalIndex)); + vm.serializeBytes32(obj, "local_exit_root", localExitRoot); + vm.serializeBytes32(obj, "mainnet_exit_root", mainnetExitRoot); + vm.serializeBytes32(obj, "rollup_exit_root", rollupExitRoot); + vm.serializeBytes32(obj, "global_exit_root", globalExitRoot); + + string memory json = vm.serializeString( + obj, "description", "Rollup deposit test vectors with valid two-level Merkle proofs (non-zero indices)" + ); + + string memory outputPath = "test-vectors/claim_asset_vectors_rollup_tx.json"; + vm.writeJson(json, outputPath); + + console.log("Generated rollup deposit test vectors with valid two-level Merkle proofs"); + console.log("Output file:", outputPath); + console.log("Leaf index:", leafIndex); + console.log("Rollup index:", indexRollup); + } + } + + /** + * @notice Helper function to serialize SMT proofs (avoids stack too deep) + */ + function _serializeProofs(string memory obj, bytes32[32] memory smtProofLocal, bytes32[32] memory smtProofRollup) + internal + { + bytes32[] memory smtProofLocalDyn = new bytes32[](32); + bytes32[] memory smtProofRollupDyn = new bytes32[](32); + for (uint256 i = 0; i < 32; i++) { + smtProofLocalDyn[i] = smtProofLocal[i]; + smtProofRollupDyn[i] = smtProofRollup[i]; + } + + vm.serializeBytes32(obj, "smt_proof_local_exit_root", smtProofLocalDyn); + vm.serializeBytes32(obj, "smt_proof_rollup_exit_root", smtProofRollupDyn); + } +} diff --git a/crates/miden-agglayer/src/errors/agglayer.rs b/crates/miden-agglayer/src/errors/agglayer.rs index 91e98d3725..21eba60f60 100644 --- a/crates/miden-agglayer/src/errors/agglayer.rs +++ b/crates/miden-agglayer/src/errors/agglayer.rs @@ -16,8 +16,10 @@ pub const ERR_B2AGG_UNEXPECTED_NUMBER_OF_STORAGE_ITEMS: MasmError = MasmError::f /// Error Message: "B2AGG script requires exactly 1 note asset" pub const ERR_B2AGG_WRONG_NUMBER_OF_ASSETS: MasmError = MasmError::from_static_str("B2AGG script requires exactly 1 note asset"); -/// Error Message: "bridge not mainnet" -pub const ERR_BRIDGE_NOT_MAINNET: MasmError = MasmError::from_static_str("bridge not mainnet"); +/// Error Message: "mainnet flag must be 1 for a mainnet deposit" +pub const ERR_BRIDGE_NOT_MAINNET: MasmError = MasmError::from_static_str("mainnet flag must be 1 for a mainnet deposit"); +/// Error Message: "mainnet flag must be 0 for a rollup deposit" +pub const ERR_BRIDGE_NOT_ROLLUP: MasmError = MasmError::from_static_str("mainnet flag must be 0 for a rollup deposit"); /// Error Message: "CLAIM note attachment target account does not match consuming account" pub const ERR_CLAIM_TARGET_ACCT_MISMATCH: MasmError = MasmError::from_static_str("CLAIM note attachment target account does not match consuming account"); @@ -42,6 +44,9 @@ pub const ERR_INVALID_CLAIM_PROOF: MasmError = MasmError::from_static_str("inval /// Error Message: "leading bits of global index must be zero" pub const ERR_LEADING_BITS_NON_ZERO: MasmError = MasmError::from_static_str("leading bits of global index must be zero"); +/// Error Message: "mainnet flag must be 0 or 1" +pub const ERR_MAINNET_FLAG_INVALID: MasmError = MasmError::from_static_str("mainnet flag must be 0 or 1"); + /// Error Message: "number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)" pub const ERR_MMR_FRONTIER_LEAVES_NUM_EXCEED_LIMIT: MasmError = MasmError::from_static_str("number of leaves in the MMR of the MMR Frontier would exceed 4294967295 (2^32 - 1)"); diff --git a/crates/miden-agglayer/src/eth_types/global_index.rs b/crates/miden-agglayer/src/eth_types/global_index.rs index 4c4688edc0..7f365e8247 100644 --- a/crates/miden-agglayer/src/eth_types/global_index.rs +++ b/crates/miden-agglayer/src/eth_types/global_index.rs @@ -13,7 +13,7 @@ use miden_protocol::utils::{HexParseError, hex_to_bytes}; pub enum GlobalIndexError { /// The leading 160 bits of the global index are not zero. LeadingBitsNonZero, - /// The mainnet flag is not 1. + /// The mainnet flag is not a valid boolean (must be exactly 0 or 1). InvalidMainnetFlag, /// The rollup index is not zero for a mainnet deposit. RollupIndexNonZero, @@ -50,31 +50,39 @@ impl GlobalIndex { Self(bytes) } - /// Validates that this is a valid mainnet deposit global index. + /// Validates this global index. /// /// Checks that: - /// - The top 160 bits (limbs 0-4, bytes 0-19) are zero - /// - The mainnet flag (limb 5, bytes 20-23) is exactly 1 - /// - The rollup index (limb 6, bytes 24-27) is 0 - pub fn validate_mainnet(&self) -> Result<(), GlobalIndexError> { - // Check limbs 0-4 are zero (bytes 0-19) + /// - The top 160 bits (bytes 0-19) are zero + /// - The mainnet flag (bytes 20-23) is exactly 0 or 1 + /// - For mainnet deposits (flag = 1): the rollup index is 0 + pub fn validate(&self) -> Result<(), GlobalIndexError> { + // Check leading 160 bits are zero if self.0[0..20].iter().any(|&b| b != 0) { return Err(GlobalIndexError::LeadingBitsNonZero); } - // Check mainnet flag limb (bytes 20-23) is exactly 1 - if !self.is_mainnet() { + // Check mainnet flag is a valid boolean (exactly 0 or 1) + let flag = self.mainnet_flag(); + if flag > 1 { return Err(GlobalIndexError::InvalidMainnetFlag); } - // Check rollup index is zero (bytes 24-27) - if u32::from_be_bytes([self.0[24], self.0[25], self.0[26], self.0[27]]) != 0 { + // For mainnet deposits, rollup index must be zero + if flag == 1 && self.rollup_index() != 0 { return Err(GlobalIndexError::RollupIndexNonZero); } Ok(()) } + /// Returns the raw mainnet flag value (limb 5, bytes 20-23). + /// + /// Valid values are 0 (rollup) or 1 (mainnet). + pub fn mainnet_flag(&self) -> u32 { + u32::from_be_bytes([self.0[20], self.0[21], self.0[22], self.0[23]]) + } + /// Returns the leaf index (limb 7, lowest 32 bits). pub fn leaf_index(&self) -> u32 { u32::from_be_bytes([self.0[28], self.0[29], self.0[30], self.0[31]]) @@ -87,7 +95,7 @@ impl GlobalIndex { /// Returns true if this is a mainnet deposit (mainnet flag = 1). pub fn is_mainnet(&self) -> bool { - u32::from_be_bytes([self.0[20], self.0[21], self.0[22], self.0[23]]) == 1 + self.mainnet_flag() == 1 } /// Converts to field elements for note storage / MASM processing. @@ -107,6 +115,60 @@ mod tests { use super::*; + #[test] + fn test_rollup_global_index_validation() { + // Rollup global index: mainnet_flag=0, rollup_index=5, leaf_index=42 + // Format: (rollup_index << 32) | leaf_index + let mut bytes = [0u8; 32]; + // mainnet flag = 0 (bytes 20-23): already zero + // rollup index = 5 (bytes 24-27, BE) + bytes[27] = 5; + // leaf index = 42 (bytes 28-31, BE) + bytes[31] = 42; + + let gi = GlobalIndex::new(bytes); + + assert!(!gi.is_mainnet()); + assert_eq!(gi.rollup_index(), 5); + assert_eq!(gi.leaf_index(), 42); + assert!(gi.validate().is_ok()); + assert!(gi.validate().is_ok()); + } + + #[test] + fn test_rollup_global_index_rejects_leading_bits() { + let mut bytes = [0u8; 32]; + bytes[3] = 1; // non-zero leading bits + bytes[27] = 5; // rollup index = 5 + bytes[31] = 42; // leaf index = 42 + + let gi = GlobalIndex::new(bytes); + assert_eq!(gi.validate(), Err(GlobalIndexError::LeadingBitsNonZero)); + assert_eq!(gi.validate(), Err(GlobalIndexError::LeadingBitsNonZero)); + } + + #[test] + fn test_rollup_global_index_various_indices() { + // Test with larger rollup index and leaf index values + let test_cases = [ + (1u32, 0u32), // first rollup, first leaf + (7, 1000), // rollup 7, leaf 1000 + (100, 999999), // larger values + ]; + + for (rollup_idx, leaf_idx) in test_cases { + let mut bytes = [0u8; 32]; + bytes[24..28].copy_from_slice(&rollup_idx.to_be_bytes()); + bytes[28..32].copy_from_slice(&leaf_idx.to_be_bytes()); + + let gi = GlobalIndex::new(bytes); + assert!(!gi.is_mainnet()); + assert_eq!(gi.rollup_index(), rollup_idx); + assert_eq!(gi.leaf_index(), leaf_idx); + assert!(gi.validate().is_ok()); + } + } + #[test] fn test_mainnet_global_indices_from_production() { // Real mainnet global indices from production @@ -122,7 +184,7 @@ mod tests { let gi = GlobalIndex::from_hex(hex).expect("valid hex"); // Validate as mainnet - assert!(gi.validate_mainnet().is_ok(), "should be valid mainnet global index"); + assert!(gi.validate().is_ok(), "should be valid mainnet global index"); // Construction sanity checks assert!(gi.is_mainnet()); @@ -151,4 +213,15 @@ mod tests { ); } } + + #[test] + fn test_invalid_mainnet_flag_rejected() { + // mainnet flag = 3 (invalid, must be 0 or 1) + let mut bytes = [0u8; 32]; + bytes[23] = 3; + bytes[31] = 2; + + let gi = GlobalIndex::new(bytes); + assert_eq!(gi.validate(), Err(GlobalIndexError::InvalidMainnetFlag)); + } } diff --git a/crates/miden-testing/tests/agglayer/bridge_in.rs b/crates/miden-testing/tests/agglayer/bridge_in.rs index 1d579d3218..54e25c21ae 100644 --- a/crates/miden-testing/tests/agglayer/bridge_in.rs +++ b/crates/miden-testing/tests/agglayer/bridge_in.rs @@ -107,6 +107,7 @@ fn merkle_proof_verification_code( #[rstest::rstest] #[case::real(ClaimDataSource::Real)] #[case::simulated(ClaimDataSource::Simulated)] +#[case::rollup(ClaimDataSource::Rollup)] #[tokio::test] async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> anyhow::Result<()> { use miden_protocol::account::auth::AuthScheme; @@ -167,8 +168,11 @@ async fn test_bridge_in_claim_to_p2id(#[case] data_source: ClaimDataSource) -> a .to_account_id() .expect("destination address is not an embedded Miden AccountId"); - // For the simulated case, create the destination account so we can consume the P2ID note - let destination_account = if matches!(data_source, ClaimDataSource::Simulated) { + // For the simulated/rollup case, create the destination account so we can consume the P2ID note + let destination_account = if matches!( + data_source, + ClaimDataSource::Simulated | ClaimDataSource::Rollup + ) { use miden_standards::testing::mock_account::MockAccountExt; let dest = diff --git a/crates/miden-testing/tests/agglayer/global_index.rs b/crates/miden-testing/tests/agglayer/global_index.rs index 4e046cd9ab..4985d71b31 100644 --- a/crates/miden-testing/tests/agglayer/global_index.rs +++ b/crates/miden-testing/tests/agglayer/global_index.rs @@ -4,6 +4,7 @@ use alloc::sync::Arc; use miden_agglayer::errors::{ ERR_BRIDGE_NOT_MAINNET, + ERR_BRIDGE_NOT_ROLLUP, ERR_LEADING_BITS_NON_ZERO, ERR_ROLLUP_INDEX_NON_ZERO, }; @@ -15,7 +16,7 @@ use miden_testing::{ExecError, assert_execution_error}; use crate::agglayer::test_utils::execute_program_with_default_host; -fn assemble_process_global_index_program(global_index: GlobalIndex) -> Program { +fn assemble_process_global_index_program(global_index: GlobalIndex, proc_name: &str) -> Program { // Convert GlobalIndex to 8 field elements (big-endian: [0]=MSB, [7]=LSB) let elements = global_index.to_elements(); let [g0, g1, g2, g3, g4, g5, g6, g7] = elements.try_into().unwrap(); @@ -27,7 +28,7 @@ fn assemble_process_global_index_program(global_index: GlobalIndex) -> Program { begin push.{g7}.{g6}.{g5}.{g4}.{g3}.{g2}.{g1}.{g0} - exec.bridge_in::process_global_index_mainnet + exec.bridge_in::{proc_name} exec.sys::truncate_stack end "# @@ -42,6 +43,9 @@ fn assemble_process_global_index_program(global_index: GlobalIndex) -> Program { .unwrap() } +// MAINNET GLOBAL INDEX TESTS +// ================================================================================================ + #[tokio::test] async fn test_process_global_index_mainnet_returns_leaf_index() -> anyhow::Result<()> { // Global index format (32 bytes, big-endian like Solidity uint256): @@ -52,7 +56,10 @@ async fn test_process_global_index_mainnet_returns_leaf_index() -> anyhow::Resul let mut bytes = [0u8; 32]; bytes[23] = 1; // mainnet flag = 1 (BE: LSB at byte 23) bytes[31] = 2; // leaf index = 2 (BE: LSB at byte 31) - let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_mainnet", + ); let exec_output = execute_program_with_default_host(program, None).await?; @@ -66,7 +73,10 @@ async fn test_process_global_index_mainnet_rejects_non_zero_leading_bits() { bytes[3] = 1; // non-zero leading bits (BE: LSB of first u32 limb) bytes[23] = 1; // mainnet flag = 1 bytes[31] = 2; // leaf index = 2 - let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_mainnet", + ); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_LEADING_BITS_NON_ZERO); @@ -77,7 +87,10 @@ async fn test_process_global_index_mainnet_rejects_flag_limb_upper_bits() { let mut bytes = [0u8; 32]; bytes[23] = 3; // mainnet flag limb = 3 (upper bits set, only lowest bit allowed) bytes[31] = 2; // leaf index = 2 - let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_mainnet", + ); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_BRIDGE_NOT_MAINNET); @@ -89,8 +102,65 @@ async fn test_process_global_index_mainnet_rejects_non_zero_rollup_index() { bytes[23] = 1; // mainnet flag = 1 bytes[27] = 7; // rollup index = 7 (BE: LSB at byte 27) bytes[31] = 2; // leaf index = 2 - let program = assemble_process_global_index_program(GlobalIndex::new(bytes)); + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_mainnet", + ); let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); assert_execution_error!(err, ERR_ROLLUP_INDEX_NON_ZERO); } + +// ROLLUP GLOBAL INDEX TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_process_global_index_rollup_returns_leaf_and_rollup_index() -> anyhow::Result<()> { + // Global index for rollup: mainnet_flag=0, rollup_index=5, leaf_index=42 + let mut bytes = [0u8; 32]; + // mainnet flag = 0 (already zero) + bytes[27] = 5; // rollup index = 5 (BE: LSB at byte 27) + bytes[31] = 42; // leaf index = 42 (BE: LSB at byte 31) + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_rollup", + ); + + let exec_output = execute_program_with_default_host(program, None).await?; + + // process_global_index_rollup returns [leaf_index, rollup_index] + // stack[0] = leaf_index (top), stack[1] = rollup_index + assert_eq!(exec_output.stack[0].as_int(), 42, "leaf_index should be 42"); + assert_eq!(exec_output.stack[1].as_int(), 5, "rollup_index should be 5"); + Ok(()) +} + +#[tokio::test] +async fn test_process_global_index_rollup_rejects_non_zero_leading_bits() { + let mut bytes = [0u8; 32]; + bytes[3] = 1; // non-zero leading bits + bytes[27] = 5; // rollup index = 5 + bytes[31] = 42; // leaf index = 42 + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_rollup", + ); + + let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); + assert_execution_error!(err, ERR_LEADING_BITS_NON_ZERO); +} + +#[tokio::test] +async fn test_process_global_index_rollup_rejects_mainnet_flag() { + let mut bytes = [0u8; 32]; + bytes[23] = 1; // mainnet flag = 1 (should be 0 for rollup) + bytes[27] = 5; // rollup index = 5 + bytes[31] = 42; // leaf index = 42 + let program = assemble_process_global_index_program( + GlobalIndex::new(bytes), + "process_global_index_rollup", + ); + + let err = execute_program_with_default_host(program, None).await.map_err(ExecError::new); + assert_execution_error!(err, ERR_BRIDGE_NOT_ROLLUP); +} diff --git a/crates/miden-testing/tests/agglayer/test_utils.rs b/crates/miden-testing/tests/agglayer/test_utils.rs index 50fb537243..7fb568a652 100644 --- a/crates/miden-testing/tests/agglayer/test_utils.rs +++ b/crates/miden-testing/tests/agglayer/test_utils.rs @@ -38,6 +38,12 @@ const BRIDGE_ASSET_VECTORS_JSON: &str = include_str!( "../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_local_tx.json" ); +/// Rollup deposit test vectors JSON — contains test data for a rollup deposit with two-level +/// Merkle proofs. +const ROLLUP_ASSET_VECTORS_JSON: &str = include_str!( + "../../../miden-agglayer/solidity-compat/test-vectors/claim_asset_vectors_rollup_tx.json" +); + /// Leaf data test vectors JSON from the Foundry-generated file. pub const LEAF_VALUE_VECTORS_JSON: &str = include_str!("../../../miden-agglayer/solidity-compat/test-vectors/leaf_value_vectors.json"); @@ -236,6 +242,12 @@ pub static CLAIM_ASSET_VECTOR_LOCAL: LazyLock = LazyLock::new( .expect("failed to parse bridge asset vectors JSON") }); +/// Lazily parsed rollup deposit test vector from the JSON file. +pub static CLAIM_ASSET_VECTOR_ROLLUP: LazyLock = LazyLock::new(|| { + serde_json::from_str(ROLLUP_ASSET_VECTORS_JSON) + .expect("failed to parse rollup asset vectors JSON") +}); + /// Lazily parsed Merkle proof vectors from the JSON file. pub static SOLIDITY_MERKLE_PROOF_VECTORS: LazyLock = LazyLock::new(|| { @@ -264,6 +276,8 @@ pub enum ClaimDataSource { Real, /// Locally simulated bridgeAsset data from claim_asset_vectors_local_tx.json. Simulated, + /// Rollup deposit data from claim_asset_vectors_rollup_tx.json. + Rollup, } impl ClaimDataSource { @@ -272,6 +286,7 @@ impl ClaimDataSource { let vector = match self { ClaimDataSource::Real => &*CLAIM_ASSET_VECTOR, ClaimDataSource::Simulated => &*CLAIM_ASSET_VECTOR_LOCAL, + ClaimDataSource::Rollup => &*CLAIM_ASSET_VECTOR_ROLLUP, }; let ger = ExitRoot::new( hex_to_bytes(&vector.proof.global_exit_root).expect("valid global exit root hex"),