-
Notifications
You must be signed in to change notification settings - Fork 445
test: improve Merkle
coverage
#1603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
0xClandestine
wants to merge
9
commits into
main
Choose a base branch
from
test/merkle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
6d3ea84
wip
0xClandestine 6acdf62
natspec
0xClandestine 9f767e7
natspec
0xClandestine f90a201
more tests
0xClandestine aeafae9
sha
0xClandestine 9772e09
test
0xClandestine f3dc852
test
0xClandestine 9e983b4
refactor: review changes
0xClandestine ca784b6
test: improve leaf length range
0xClandestine File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule forge-std
updated
9 files
+1 −0 | .github/CODEOWNERS | |
+54 −100 | .github/workflows/ci.yml | |
+1 −1 | README.md | |
+1 −1 | package.json | |
+142 −47 | src/StdAssertions.sol | |
+2 −2 | src/StdStorage.sol | |
+0 −1 | src/StdUtils.sol | |
+75 −4 | src/Vm.sol | |
+1 −1 | test/Vm.t.sol |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
pragma solidity ^0.8.27; | ||
|
||
import "forge-std/Test.sol"; | ||
import "src/contracts/libraries/Merkle.sol"; | ||
import "src/test/utils/Murky.sol"; | ||
|
||
abstract contract MerkleBaseTest is Test, MurkyBase { | ||
uint internal constant MIN_LEAVES = 2; // Minimum number of leaves. | ||
uint internal constant MAX_LEAVES = 65; // Maximum number of leaves. | ||
|
||
bytes32[] leaves; // The contents of the merkle tree (unsorted). | ||
bytes32 root; // The root of the merkle tree. | ||
bytes[] proofs; // The proofs for each leaf in the tree. | ||
|
||
/// @dev Takes in `seed` which should be provided as a fuzz input. | ||
/// Ensures that RNG is deterministic, and tests are easily reproducible. | ||
modifier rng(uint seed) { | ||
_rng(seed); | ||
_; | ||
} | ||
|
||
function _rng(uint seed) internal { | ||
vm.setSeed(seed); | ||
leaves = _genLeaves(vm.randomUint(MIN_LEAVES, MAX_LEAVES)); | ||
proofs = _genProofs(leaves); | ||
root = _genRoot(leaves); | ||
} | ||
|
||
/// ----------------------------------------------------------------------- | ||
/// Keccak + Sha256 Tests | ||
/// ----------------------------------------------------------------------- | ||
|
||
/// @notice Verifies that (Murky's) proofs are compatible with our implementation. | ||
function testFuzz_verifyInclusion_ValidProof(uint seed) public rng(seed) { | ||
_checkAllProofs(true); | ||
} | ||
|
||
/// @notice Verifies that an empty proof(s) is invalid. | ||
function testFuzz_verifyInclusion_EmptyProofs(uint seed) public rng(seed) { | ||
if (!usingSha()) vm.skip(true); // TODO: Breaking change, add in future. | ||
proofs = new bytes[](proofs.length); | ||
_checkAllProofs(false); | ||
} | ||
|
||
/// @notice Verifies that using wrong root fails verification | ||
function testFuzz_verifyInclusion_WrongRoot(uint seed) public rng(seed) { | ||
root = bytes32(vm.randomUint()); | ||
_checkAllProofs(false); | ||
} | ||
|
||
/// @notice Verifies valid proofs cannot be used to prove invalid leaves. | ||
function testFuzz_verifyInclusion_WrongProofs(uint seed) public rng(seed) { | ||
bytes memory proof0 = proofs[0]; | ||
bytes memory proof1 = proofs[1]; | ||
(proofs[0], proofs[1]) = (proof1, proof0); | ||
_checkSingleProof(false, 0); | ||
_checkSingleProof(false, 1); | ||
} | ||
|
||
/// @notice Verifies that a valid proof with excess data appended is invalid. | ||
function testFuzz_verifyInclusion_ExcessProofLength(uint seed) public rng(seed) { | ||
unchecked { | ||
proofs[0] = abi.encodePacked(proofs[0], vm.randomBytes(vm.randomUint(1, 10) * vm.randomUint(31, 32))); | ||
} | ||
_checkSingleProof(false, 0); | ||
} | ||
|
||
/// @notice Verifies that a valid proof with a truncated length is invalid. | ||
function testFuzz_verifyInclusion_TruncatedProofLength(uint seed) public rng(seed) { | ||
if (!usingSha()) vm.skip(true); // TODO: Breaking change, add in future. | ||
bytes memory proof = proofs[0]; | ||
console.log("proof length %d", proof.length); // 32 | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
mstore(proof, sub(mload(proof), 32)) | ||
} | ||
console.log("proof length %d", proof.length); // 0 | ||
proofs[0] = proof; | ||
_checkSingleProof(false, 0); // Should revert, but doesn't... | ||
} | ||
|
||
/// @notice Verifies that a valid proof with a manipulated word is invalid. | ||
function testFuzz_verifyInclusion_ManipulatedProof(uint seed) public rng(seed) { | ||
bytes memory proof = proofs[0]; | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
let m := add(proof, 0x20) | ||
let manipulated := shr(8, mload(m)) // Shift the first word to the right by 8 bits. | ||
mstore(m, manipulated) | ||
} | ||
proofs[0] = proof; | ||
_checkSingleProof(false, 0); | ||
} | ||
|
||
/// @notice Verifies that an out-of-bounds index reverts. | ||
function testFuzz_verifyInclusion_IndexOutOfBounds(uint seed) public rng(seed) { | ||
uint index = vm.randomUint(leaves.length, type(uint).max); | ||
vm.expectRevert(stdError.indexOOBError); | ||
_checkSingleProof(false, index); | ||
} | ||
|
||
/// @notice Verifies that an internal node cannot be used as a proof. | ||
function testFuzz_verifyInclusion_InternalNodeAsProof(uint seed) public rng(seed) { | ||
// Generate a tree with at least 4 leaves to ensure internal nodes exist | ||
leaves = _genLeaves(vm.randomUint(4, 8)); | ||
proofs = _genProofs(leaves); | ||
root = _genRoot(leaves); | ||
function (bytes memory proof, bytes32 root, bytes32 leaf, uint256 index) view returns (bool) verifyInclusion = | ||
usingSha() ? Merkle.verifyInclusionSha256 : Merkle.verifyInclusionKeccak; | ||
assertFalse(verifyInclusion(proofs[2], root, hashLeafPairs(leaves[0], leaves[1]), 2)); | ||
} | ||
|
||
/// @notice Verifies behavior with duplicate leaves in the tree | ||
function testFuzz_verifyInclusion_DuplicateLeaves(uint seed) public rng(seed) { | ||
leaves = _genLeaves(vm.randomUint(4, 8)); | ||
leaves[0] = leaves[vm.randomUint(1, leaves.length - 1)]; | ||
proofs = _genProofs(leaves); | ||
root = _genRoot(leaves); | ||
_checkAllProofs(true); | ||
} | ||
|
||
/// ----------------------------------------------------------------------- | ||
/// Assertions | ||
/// ----------------------------------------------------------------------- | ||
|
||
/// @dev Checks that all proofs are valid for their respective leaves. | ||
function _checkAllProofs(bool status) internal virtual { | ||
function (bytes memory proof, bytes32 root, bytes32 leaf, uint256 index) returns (bool) verifyInclusion = | ||
usingSha() ? Merkle.verifyInclusionSha256 : Merkle.verifyInclusionKeccak; | ||
for (uint i = 0; i < leaves.length; ++i) { | ||
if (proofs[i].length == 0 || proofs[i].length % 32 != 0) vm.expectRevert(Merkle.InvalidProofLength.selector); | ||
assertEq(verifyInclusion(proofs[i], root, leaves[i], i), status); | ||
} | ||
} | ||
|
||
/// @dev Checks that a single proof is valid for its respective leaf. | ||
function _checkSingleProof(bool status, uint index) internal virtual { | ||
function (bytes memory proof, bytes32 root, bytes32 leaf, uint256 index) view returns (bool) verifyInclusion = | ||
usingSha() ? Merkle.verifyInclusionSha256 : Merkle.verifyInclusionKeccak; | ||
if (proofs[index].length == 0 || proofs[index].length % 32 != 0) vm.expectRevert(Merkle.InvalidProofLength.selector); | ||
assertEq(verifyInclusion(proofs[index], root, leaves[index], index), status); | ||
} | ||
|
||
/// ----------------------------------------------------------------------- | ||
/// Helpers | ||
/// ----------------------------------------------------------------------- | ||
|
||
/// @dev Efficiently pads the length of leaves to the next power of 2 by appending zeros. | ||
function _padLeaves(bytes32[] memory leaves) internal view virtual returns (bytes32[] memory paddedLeaves) { | ||
uint numLeaves = _roundUpPow2(leaves.length); | ||
paddedLeaves = new bytes32[](numLeaves); | ||
for (uint i = 0; i < leaves.length; ++i) { | ||
paddedLeaves[i] = leaves[i]; | ||
} | ||
} | ||
|
||
/// @dev Generates a random list of leaves without iterative hashing. | ||
function _genLeaves(uint numLeaves) internal view virtual returns (bytes32[] memory leaves) { | ||
bytes memory _leavesAsBytes = vm.randomBytes(numLeaves * 32); | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
leaves := _leavesAsBytes // Typecast bytes -> bytes32[]. | ||
mstore(leaves, numLeaves) // Update length n*32 -> n. | ||
} | ||
} | ||
|
||
/// @dev Generates proofs for each leaf in the tree. | ||
function _genProofs(bytes32[] memory leaves) internal view virtual returns (bytes[] memory proofs) { | ||
uint numLeaves = _roundUpPow2(leaves.length); | ||
bytes32[] memory paddedLeaves = _padLeaves(leaves); | ||
proofs = new bytes[](leaves.length); | ||
for (uint i = 0; i < leaves.length; ++i) { | ||
proofs[i] = abi.encodePacked(getProof(paddedLeaves, i)); | ||
} | ||
} | ||
|
||
/// @dev Computes the merkle root using the appropriate hash function | ||
function _genRoot(bytes32[] memory leaves) internal view virtual returns (bytes32) { | ||
function (bytes32[] memory leaves) view returns (bytes32) merkleize = usingSha() ? Merkle.merkleizeSha256 : Merkle.merkleizeKeccak; | ||
if (usingSha()) leaves = _padLeaves(leaves); | ||
return merkleize(leaves); | ||
} | ||
|
||
/// @dev Rounds up to the next power of 2. | ||
/// https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 | ||
function _roundUpPow2(uint v) internal pure returns (uint) { | ||
unchecked { | ||
v -= 1; | ||
v |= v >> 1; | ||
v |= v >> 2; | ||
v |= v >> 4; | ||
v |= v >> 8; | ||
v |= v >> 16; | ||
v |= v >> 32; | ||
v |= v >> 64; | ||
v |= v >> 128; | ||
return v + 1; | ||
} | ||
} | ||
|
||
function usingSha() internal view virtual returns (bool); | ||
} | ||
|
||
contract MerkleKeccakTest is MerkleBaseTest, MerkleKeccak { | ||
function usingSha() internal view virtual override returns (bool) { | ||
return false; | ||
} | ||
} | ||
|
||
contract MerkleShaTest is MerkleBaseTest, MerkleSha { | ||
function usingSha() internal view virtual override returns (bool) { | ||
return true; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows us to test library reverts without needing a harness.