From 0a7ebaf1eb3d62844d532cd6087c006f59078d8c Mon Sep 17 00:00:00 2001 From: The Dark Jester Date: Wed, 17 Jul 2024 18:17:11 +0200 Subject: [PATCH] Update MiMC and SparseMerkleProof (#20) --- contracts/lib/Mimc.sol | 45 +++++--- contracts/lib/SparseMerkleProof.sol | 62 ++++++++++- hardhat.config.ts | 8 +- test/SparseMerkleProof.ts | 153 ++++++++++++++++++++++++++++ test/mimc.ts | 17 ++++ 5 files changed, 262 insertions(+), 23 deletions(-) diff --git a/contracts/lib/Mimc.sol b/contracts/lib/Mimc.sol index 93a9b02..79b511f 100644 --- a/contracts/lib/Mimc.sol +++ b/contracts/lib/Mimc.sol @@ -15,18 +15,46 @@ // limitations under the License. // Code generated by gnark DO NOT EDIT -pragma solidity 0.8.24; +pragma solidity 0.8.25; +/** + * @title Library to perform MiMC hashing + * @author ConsenSys Software Inc. + * @custom:security-contact security-report@linea.build + */ library Mimc { - uint256 constant FR_FIELD = 8444461749428370424248824938781546531375899335154063827935233455917409239041; + /** + * Thrown when the data is not provided + */ + error DataMissing(); + + /** + * Thrown when the data is not purely in 32 byte chunks + */ + error DataIsNotMod32(); + uint256 constant FR_FIELD = 8444461749428370424248824938781546531375899335154063827935233455917409239041; + /** + * @notice Performs a MiMC hash on the data provided + * @param _msg The data to be hashed + * @dev Only data that has length modulus 32 is hashed, reverts otherwise + * @return mimcHash The computed MiMC hash + */ function hash(bytes calldata _msg) external pure returns (bytes32 mimcHash) { + if (_msg.length == 0) { + revert DataMissing(); + } + + if (_msg.length % 0x20 != 0) { + revert DataIsNotMod32(); + } + assembly { let chunks := div(add(_msg.length, 0x1f), 0x20) for { let i := 0 - } lt(i, sub(chunks, 1)) { + } lt(i, chunks) { i := add(i, 1) } { let offset := add(_msg.offset, mul(i, 0x20)) @@ -36,17 +64,6 @@ library Mimc { mimcHash := addmod(addmod(mimcHash, r, FR_FIELD), chunk, FR_FIELD) } - let offset := add(_msg.offset, mul(sub(chunks, 1), 0x20)) - let lastChunk := calldataload(offset) - - if iszero(eq(mod(_msg.length, 0x20), 0)) { - let remaining := mod(_msg.length, 0x20) - lastChunk := shr(mul(sub(0x20, remaining), 0x8), lastChunk) - } - - let r := encrypt(mimcHash, lastChunk) - mimcHash := addmod(addmod(mimcHash, r, FR_FIELD), lastChunk, FR_FIELD) - function encrypt(h, chunk) -> output { let frField := FR_FIELD let tmpSum := 0 diff --git a/contracts/lib/SparseMerkleProof.sol b/contracts/lib/SparseMerkleProof.sol index a9e4856..dcc7b12 100644 --- a/contracts/lib/SparseMerkleProof.sol +++ b/contracts/lib/SparseMerkleProof.sol @@ -1,11 +1,20 @@ // SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.24; +pragma solidity 0.8.25; import { Mimc } from "./Mimc.sol"; +/** + * @title Library to perform SparseMerkleProof actions using the MiMC hashing algorithm + * @author ConsenSys Software Inc. + * @custom:security-contact security-report@linea.build + */ library SparseMerkleProof { using Mimc for *; + /** + * The Account struct represents the state of the account including the storage root, nonce, balance and codesize + * @dev This is mapped directly to the output of the storage proof + */ struct Account { uint64 nonce; uint256 balance; @@ -15,6 +24,10 @@ library SparseMerkleProof { uint64 codeSize; } + /** + * Represents the leaf structure in both account and storage tries + * @dev This is mapped directly to the output of the storage proof + */ struct Leaf { uint256 prev; uint256 next; @@ -22,18 +35,43 @@ library SparseMerkleProof { bytes32 hValue; } + /** + * Thrown when expected bytes length is incorrect + */ error WrongBytesLength(uint256 expectedLength, uint256 bytesLength); + /** + * Thrown when the length of bytes is not in exactly 32 byte chunks + */ + error LengthNotMod32(); + + /** + * Thrown when the leaf index is higher than the tree depth + */ + error MaxTreeLeafIndexExceed(); + + /** + * Thrown when the length of the unformatted proof is not provided exactly as expected (UNFORMATTED_PROOF_LENGTH) + */ + error WrongProofLength(uint256 expectedLength, uint256 actualLength); + uint256 internal constant TREE_DEPTH = 40; + uint256 internal constant UNFORMATTED_PROOF_LENGTH = 42; bytes32 internal constant ZERO_HASH = 0x0; + uint256 internal constant MAX_TREE_LEAF_INDEX = 2 ** TREE_DEPTH - 1; /** - * @notice Format input and verify sparse merkle proof + * @notice Formats input, computes root and returns true if it matches the provided merkle root * @param _rawProof Raw sparse merkle tree proof * @param _leafIndex Index of the leaf * @param _root Sparse merkle root + * @return If the computed merkle root matches the provided one */ function verifyProof(bytes[] calldata _rawProof, uint256 _leafIndex, bytes32 _root) external pure returns (bool) { + if (_rawProof.length != UNFORMATTED_PROOF_LENGTH) { + revert WrongProofLength(UNFORMATTED_PROOF_LENGTH, _rawProof.length); + } + (bytes32 nextFreeNode, bytes32 leafHash, bytes32[] memory proof) = _formatProof(_rawProof); return _verify(proof, leafHash, _leafIndex, _root, nextFreeNode); } @@ -103,7 +141,7 @@ library SparseMerkleProof { * @return {Leaf} Formatted leaf struct */ function _parseLeaf(bytes calldata _encodedLeaf) private pure returns (Leaf memory) { - if (_encodedLeaf.length < 128) { + if (_encodedLeaf.length != 128) { revert WrongBytesLength(128, _encodedLeaf.length); } return abi.decode(_encodedLeaf, (Leaf)); @@ -115,7 +153,7 @@ library SparseMerkleProof { * @return {Account} Formatted account struct */ function _parseAccount(bytes calldata _value) private pure returns (Account memory) { - if (_value.length < 192) { + if (_value.length != 192) { revert WrongBytesLength(192, _value.length); } return abi.decode(_value, (Account)); @@ -144,6 +182,11 @@ library SparseMerkleProof { uint256 formattedProofLength = rawProofLength - 2; bytes32[] memory proof = new bytes32[](formattedProofLength); + + if (_rawProof[0].length != 0x40) { + revert WrongBytesLength(0x40, _rawProof[0].length); + } + bytes32 nextFreeNode = bytes32(_rawProof[0][:32]); bytes32 leafHash = Mimc.hash(_rawProof[rawProofLength - 1]); @@ -170,6 +213,10 @@ library SparseMerkleProof { * @return isZeroBytes true if bytes contain only zero byte elements, false otherwise */ function _isZeroBytes(bytes calldata _data) private pure returns (bool isZeroBytes) { + if (_data.length % 0x20 != 0) { + revert LengthNotMod32(); + } + isZeroBytes = true; assembly { let dataStart := _data.offset @@ -190,12 +237,13 @@ library SparseMerkleProof { } /** - * @notice Verify sparse merkle proof + * @notice Computes merkle root from proof and compares it to the provided root * @param _proof Sparse merkle tree proof * @param _leafHash Leaf hash * @param _leafIndex Index of the leaf * @param _root Sparse merkle root * @param _nextFreeNode Next free node + * @return If the computed merkle root matches the provided one */ function _verify( bytes32[] memory _proof, @@ -207,6 +255,10 @@ library SparseMerkleProof { bytes32 computedHash = _leafHash; uint256 currentIndex = _leafIndex; + if (_leafIndex > MAX_TREE_LEAF_INDEX) { + revert MaxTreeLeafIndexExceed(); + } + for (uint256 height; height < TREE_DEPTH; ++height) { if ((currentIndex >> height) & 1 == 1) computedHash = Mimc.hash(abi.encodePacked(_proof[height], computedHash)); else computedHash = Mimc.hash(abi.encodePacked(computedHash, _proof[height])); diff --git a/hardhat.config.ts b/hardhat.config.ts index 012912b..f68028c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -22,7 +22,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.24", + version: "0.8.25", settings: { optimizer: { enabled: true, @@ -32,22 +32,22 @@ const config: HardhatUserConfig = { }, }, { - version: "0.8.19", + version: "0.8.24", settings: { optimizer: { enabled: true, runs: 100000, }, + evmVersion: "cancun", }, }, { - version: "0.8.15", + version: "0.8.19", settings: { optimizer: { enabled: true, runs: 100000, }, - evmVersion: "london", }, }, ], diff --git a/test/SparseMerkleProof.ts b/test/SparseMerkleProof.ts index fc5ab7f..3f4a358 100644 --- a/test/SparseMerkleProof.ts +++ b/test/SparseMerkleProof.ts @@ -4,6 +4,7 @@ import { ethers } from "hardhat"; import { Mimc, SparseMerkleProof } from "../typechain-types"; import merkleProofTestData from "./testData/merkle-proof-data.json"; import { deployFromFactory } from "./utils/deployment"; +import { expectRevertWithCustomError } from "./utils/helpers"; describe("SparseMerkleProof", () => { let sparseMerkleProof: SparseMerkleProof; @@ -24,6 +25,88 @@ describe("SparseMerkleProof", () => { describe("verifyProof", () => { describe("account proof", () => { + it("Should revert when proof length is < 42", async () => { + const { + accountProof: { + proof: { proofRelatedNodes }, + }, + } = merkleProofTestData; + + const stateRoot = "0x0e080582960965e3c180b1457b16da48041e720af628ae6c1725d13bd98ba9f0"; + const leafIndex = 200; + + const proofRelatedNodesPopped = proofRelatedNodes.slice(0, proofRelatedNodes.length - 1); + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.verifyProof(proofRelatedNodesPopped, leafIndex, stateRoot), + "WrongProofLength", + [42, 41], + ); + }); + + it("Should revert when proof length is > 42", async () => { + const { + accountProof: { + proof: { proofRelatedNodes }, + }, + } = merkleProofTestData; + + const stateRoot = "0x0e080582960965e3c180b1457b16da48041e720af628ae6c1725d13bd98ba9f0"; + const leafIndex = 200; + + const clonedProof = proofRelatedNodes.slice(0, proofRelatedNodes.length); + clonedProof.push("0x0e080582960965e3c180b1457b16da48041e720af628ae6c1725d13bd98ba9f0"); + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.verifyProof(clonedProof, leafIndex, stateRoot), + "WrongProofLength", + [42, 43], + ); + }); + + it("Should revert when a value is not mod 32", async () => { + const { + accountProof: { + proof: { proofRelatedNodes }, + }, + } = merkleProofTestData; + + const stateRoot = "0x0e080582960965e3c180b1457b16da48041e720af628ae6c1725d13bd98ba9f0"; + const leafIndex = 200; + + const clonedProof = proofRelatedNodes.slice(0, proofRelatedNodes.length); + clonedProof[40] = "0x1234"; // set the second last item in the array + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.verifyProof(clonedProof, leafIndex, stateRoot), + "LengthNotMod32", + ); + }); + + it("Should revert when index 0 is not 64 bytes", async () => { + const { + accountProof: { + proof: { proofRelatedNodes }, + }, + } = merkleProofTestData; + + const stateRoot = "0x0e080582960965e3c180b1457b16da48041e720af628ae6c1725d13bd98ba9f0"; + const leafIndex = 200; + + const clonedProof = proofRelatedNodes.slice(0, proofRelatedNodes.length); + clonedProof[0] = "0x1234"; + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.verifyProof(clonedProof, leafIndex, stateRoot), + "WrongBytesLength", + [64, 2], + ); + }); + it("Should return false when the account proof is not correct", async () => { const { accountProof: { @@ -38,6 +121,25 @@ describe("SparseMerkleProof", () => { expect(result).to.be.false; }); + it("Should revert when leaf index is higher than max leaf index", async () => { + const { + accountProof: { + proof: { proofRelatedNodes }, + leafIndex, + }, + } = merkleProofTestData; + + const higherLeafIndex = leafIndex + Math.pow(2, 40); + + const stateRoot = "0x0e080582960965e3c180b1457b16da48041e720af628ae6c1725d13bd98ba9f0"; + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.verifyProof(proofRelatedNodes, higherLeafIndex, stateRoot), + "MaxTreeLeafIndexExceed", + ); + }); + it("Should return true when the account proof is correct", async () => { const { accountProof: { @@ -96,6 +198,29 @@ describe("SparseMerkleProof", () => { const hVal = await sparseMerkleProof.hashAccountValue(value); expect(hVal).to.be.equal("0x05d9557beb35be64f9f0be17af76dd4f19d5016b4108ce8a552458dcf8ec6d4b"); }); + + it("Should error if less than 192 length", async () => { + const shortValue = "0x0012"; + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.hashAccountValue(shortValue), + "WrongBytesLength", + [192, 2], + ); + }); + + it("Should error if more than 192 length", async () => { + const longValue = + "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d2a66d5598b4fc5482c311f22d2dc657579b5452ab4b3e60fb1a9e9dbbfc99e00c24dd0468f02fbece668291f3c3eb20e06d1baec856f28430555967f2bf280d798c662debc23e8199fbf0b0a3a95649f2defe90af458d7f62c03881f916b3f0000000000000000000000000000000000000000000000000000000000003030454748"; + + await expectRevertWithCustomError( + sparseMerkleProof, + sparseMerkleProof.hashAccountValue(longValue), + "WrongBytesLength", + [192, 195], + ); + }); }); describe("hashStorageValue", () => { @@ -128,6 +253,20 @@ describe("SparseMerkleProof", () => { .withArgs(128, ethers.dataLength(wrongLeaftValue)); }); + it("Should revert when leaf bytes length > 128", async () => { + const { + accountProof: { + proof: { proofRelatedNodes }, + }, + } = merkleProofTestData; + + const wrongLeaftValue = `${proofRelatedNodes[proofRelatedNodes.length - 1]}1234`; + + await expect(sparseMerkleProof.getLeaf(wrongLeaftValue)) + .to.revertedWithCustomError(sparseMerkleProof, "WrongBytesLength") + .withArgs(128, ethers.dataLength(wrongLeaftValue)); + }); + it("Should return parsed leaf", async () => { const { accountProof: { @@ -195,6 +334,20 @@ describe("SparseMerkleProof", () => { .withArgs(192, ethers.dataLength(wrongAccountValue)); }); + it("Should revert when account bytes length > 192", async () => { + const { + accountProof: { + proof: { value }, + }, + } = merkleProofTestData; + + const wrongAccountValue = `${value}123456`; + + await expect(sparseMerkleProof.getAccount(wrongAccountValue)) + .to.revertedWithCustomError(sparseMerkleProof, "WrongBytesLength") + .withArgs(192, ethers.dataLength(wrongAccountValue)); + }); + it("Should return parsed leaf", async () => { const { accountProof: { diff --git a/test/mimc.ts b/test/mimc.ts index 60a5f50..5d9ca1f 100644 --- a/test/mimc.ts +++ b/test/mimc.ts @@ -4,6 +4,7 @@ import { ethers } from "hardhat"; import { Mimc } from "../typechain-types"; import mimcTestData from "./testData/mimc-test-data.json"; import { deployFromFactory } from "./utils/deployment"; +import { expectRevertWithCustomError } from "./utils/helpers"; describe("Mimc", () => { let mimc: Mimc; @@ -23,5 +24,21 @@ describe("Mimc", () => { expect(await mimc.hash(msgs)).to.equal(element.out); } }); + + it("Should revert if the data is zero length", async () => { + await expectRevertWithCustomError(mimc, mimc.hash("0x"), "DataMissing"); + }); + + it("Should revert if the data is less than 32 and not mod32", async () => { + await expectRevertWithCustomError(mimc, mimc.hash("0x12"), "DataIsNotMod32"); + }); + + it("Should revert if the data is greater than 32 and not mod32", async () => { + await expectRevertWithCustomError( + mimc, + mimc.hash("0x103adbc490c2067eac112873462707eb2072813005a4ac3ab182135be336742423456789"), + "DataIsNotMod32", + ); + }); }); });