Repository: xmtp/smart-contracts
Pull request #55: feat: sequential merkle proofs
Author: login: deluca-mike
Labels: name: enhancement
description: New feature or request
Created: 2025-05-02T18:32:10Z | Merged: 2025-05-09T19:04:31Z

### Implement sequential merkle proof verification library to validate ordered leaf sequences in merkle trees
The changes introduce a new cryptographic verification library in [SequentialMerkleProofs.sol](https://github.com/xmtp/smart-contracts/pull/55/files#diff-ff40daaf8a5948d34b0bb31c466d78910cb7381d9e82701848d8b81edf6fe765) that provides functionality for verifying sequential leaf positions in merkle trees. The library includes:

* Core verification functions `verify()` and `getRoot()` for validating sequential merkle proofs
* Specialized hashing functions with distinct prefixes for leaves, nodes, and roots
* Bit manipulation utilities for tree calculations
* Support for both balanced and unbalanced tree verification

#### 📍Where to Start
Start with the `verify()` function in [SequentialMerkleProofs.sol](https://github.com/xmtp/smart-contracts/pull/55/files#diff-ff40daaf8a5948d34b0bb31c466d78910cb7381d9e82701848d8b81edf6fe765) which contains the main verification logic for sequential merkle proofs.

----

_[Macroscope](https://app.macroscope.com) summarized 12e777f._

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a library for verifying sequential Merkle proofs, enabling users to validate that a sequence of leaves appears consecutively in a Merkle tree.
- **Tests**
  - Added comprehensive unit tests covering proof verification, root calculation, hashing, and utility functions, ensuring correctness across various scenarios.
  - Provided a test harness to facilitate direct testing of all public and internal library functions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/libraries/SequentialMerkleProofs.sol b/src/libraries/SequentialMerkleProofs.sol
new file mode 100644
index 00000000..6ef5e8f3
--- /dev/null
+++ b/src/libraries/SequentialMerkleProofs.sol
@@ -0,0 +1,341 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    bytes32 internal constant EMPTY_TREE_ROOT = bytes32(0);
+
+    /// @notice The leaf prefix used to hash to a leaf ("leaf|").
+    bytes5 internal constant LEAF_PREFIX = 0x6c6561667c;
+
+    /// @notice The node prefix used to hash to a node ("node|").
+    bytes5 internal constant NODE_PREFIX = 0x6e6f64657c;
+
+    /// @notice The root prefix used to hash to a root.
+    bytes5 internal constant ROOT_PREFIX = 0x726f6f747c;
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when no leaves are provided.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _bitCount32 is greater than type(uint32).max.
+    error InvalidBitCount32Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /// @notice Thrown when no proof elements are provided.
+    error NoProofElements();
+
+    /// @notice Thrown when the leaf count is greater than type(uint32).max.
+    error InvalidLeafCount();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        bytes32 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        bytes32[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        bytes32[] calldata proofElements_
+    ) internal pure returns (bytes32 root_) {
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
+
+    /**
+     * @notice Extracts the leaf count from a sequential merkle proof, without verifying the proof.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree.
+     * @dev    Does not verify the proof. Only extracts the leaf count from the proof elements.
+     */
+    function getLeafCount(bytes32[] calldata proofElements_) internal pure returns (uint32 leafCount_) {
+        require(proofElements_.length > 0, NoProofElements());
+
+        if (uint256(proofElements_[0]) > type(uint32).max) revert InvalidLeafCount();
+
+        return uint32(uint256(proofElements_[0]));
+    }
+
+    /* ============ Helper Functions ============ */
+
+    /**
+     * @notice Counts number of set bits (1's) in 32-bit unsigned integer.
+     * @dev    See https://en.wikipedia.org/wiki/Hamming_weight implementation `popcount64b`.
+     * @param  n_        The number to count the set bits of.
+     * @return bitCount_ The number of set bits in `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _bitCount32(uint256 n_) internal pure returns (uint256 bitCount_) {
+        require(n_ <= type(uint32).max, InvalidBitCount32Input());
+
+        unchecked {
+            n_ -= (n_ >> 1) & 0x55555555;
+            n_ = (n_ & 0x33333333) + ((n_ >> 2) & 0x33333333);
+            n_ = (n_ + (n_ >> 4)) & 0x0f0f0f0f;
+            n_ += n_ >> 8;
+            n_ += n_ >> 16;
+
+            return n_ & 0x7f;
+        }
+    }
+
+    /**
+     * @notice Rounds a 32-bit unsigned integer up to the nearest power of 2.
+     * @param  n_        The number to round up to the nearest power of 2.
+     * @return powerOf2_ The nearest power of 2 to `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _roundUpToPowerOf2(uint256 n_) internal pure returns (uint256 powerOf2_) {
+        if (_bitCount32(n_) == 1) return n_;
+
+        unchecked {
+            n_ |= n_ >> 1;
+            n_ |= n_ >> 2;
+            n_ |= n_ >> 4;
+            n_ |= n_ >> 8;
+            n_ |= n_ >> 16;
+
+            return n_ + 1;
+        }
+    }
+
+    /**
+     * @notice Gets the balanced leaf count for a sequential merkle proof.
+     * @param  leafCount_         The number of leaves in the merkle tree.
+     * @return balancedLeafCount_ The balanced leaf count.
+     */
+    function _getBalancedLeafCount(uint256 leafCount_) internal pure returns (uint256 balancedLeafCount_) {
+        return leafCount_ <= 1 ? (leafCount_ * 2) : _roundUpToPowerOf2(leafCount_);
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  hashes_        The leaf hashes (in reverse order) to prove sequential existence from the starting index.
+     * @param  proofElements_         The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     */
+    function _getRoot(
+        uint256 startingIndex_,
+        bytes32[] memory hashes_,
+        bytes32[] calldata proofElements_
+    ) internal pure returns (bytes32 root_) {
+        require(proofElements_.length > 0, NoProofElements());
+
+        if (startingIndex_ == 0 && hashes_.length == 0 && uint256(proofElements_[0]) == 0) return EMPTY_TREE_ROOT;
+
+        require(hashes_.length > 0, NoLeaves());
+        require(startingIndex_ + hashes_.length <= uint256(proofElements_[0]), InvalidProof());
+
+        uint256 count_ = hashes_.length;
+        uint256[] memory treeIndices_ = new uint256[](count_);
+
+        /**
+         * @dev The following variables are used while iterating through the root reconstruction from the proof.
+         * @dev `readIndex_` is the index of `hashes_` circular queue currently being read from.
+         * @dev `writeIndex_` is the index of `hashes_` circular queue currently being written to.
+         * @dev `proofIndex_` is the index of `proofElements_` array that is to be read from.
+         * @dev `upperBound_` is the rightmost tree index, of the current level of the tree, such that all nodes to the
+         *         right, if any, are non-existent.
+         * @dev `lowestTreeIndex_` is the tree index, of the current level of the tree, of the leftmost node we have.
+         * @dev `highestLeafNodeIndex_` is the tree index of the rightmost leaf node we have.
+         */
+        uint256 readIndex_ = 0;
+        uint256 writeIndex_ = 0;
+        uint256 proofIndex_ = 1; // proofElements_[0] is the total leaf count, and is already consumed.
+        uint256 upperBound_ = _getBalancedLeafCount(uint256(proofElements_[0])) + uint256(proofElements_[0]) - 1;
+        uint256 lowestTreeIndex_ = _getBalancedLeafCount(uint256(proofElements_[0])) + startingIndex_;
+        uint256 highestLeafNodeIndex_ = lowestTreeIndex_ + count_ - 1;
+
+        while (true) {
+            /// @dev `nodeIndex_` is the tree index of the current node we are handling.
+            // Instead of doing a full pass through the empty tree indices array to build a starting sequential set of
+            // indices, we can just check if we are in that "first pass" by checking if `readIndex_ < count_`, and if so
+            // compute the index as needed given the `highestLeafNodeIndex_` and the `readIndex_`.
+            uint256 nodeIndex_ = readIndex_ < count_
+                ? highestLeafNodeIndex_ - readIndex_
+                : treeIndices_[readIndex_ % count_];
+
+            // If we reach the sub-root (i.e. `nodeIndex_ == 1`), we can return the root (i.e. `nodeIndex_ == 0`) by
+            // hashing the tree's leaf count with the last computed hash.
+            if (nodeIndex_ == 1) return _hashRoot(uint256(proofElements_[0]), hashes_[(writeIndex_ - 1) % count_]);
+
+            // If node index we are handling is the upper bound and is even, then it's sibling to the right does not
+            // exist (since this is an unbalanced tree), so we can just copy the hash up one level.
+            if ((nodeIndex_ == upperBound_) && _isEven(nodeIndex_)) {
+                hashes_[writeIndex_ % count_] = _hashPairlessNode(hashes_[readIndex_ % count_]);
+                treeIndices_[writeIndex_ % count_] = nodeIndex_ >> 1;
+
+                unchecked {
+                    ++readIndex_;
+                    ++writeIndex_;
+                }
+
+                // If we are not at the lowest tree index (i.e. there are nodes to the left that we have yet to process
+                // at this level), then continue.
+                if (nodeIndex_ != lowestTreeIndex_) continue;
+
+                // If we are at the lowest tree index (i.e. there are no nodes to the left that we have yet to process
+                // at this level), then we can update the lower bound and upper bound for the next level up.
+                lowestTreeIndex_ >>= 1;
+                upperBound_ >>= 1;
+
+                continue;
+            }
+
+            /// @dev `nextNodeIndex_` is the tree index of the next node we may be handling.
+            // Instead of doing a full pass through the empty tree indices array to build a starting sequential set of
+            // indices, we can just check if we are in that "first pass" by checking if `readIndex_ + 1 < count_`, and
+            // if so compute the next index as needed given the `highestLeafNodeIndex_` and the `readIndex_`.
+            uint256 nextNodeIndex_ = (readIndex_ + 1) < count_
+                ? highestLeafNodeIndex_ - (readIndex_ + 1)
+                : treeIndices_[(readIndex_ + 1) % count_];
+
+            /// @dev `root_` will temporarily be used as the right part of the node pair hash, it is being used to
+            ///      save much needed stack space.
+            // Since we are processing nodes from right to left, then if the current node index is even, and there
+            // exists nodes to the right (or else the previous if-continue would have been hit), then the right part of
+            // the hash is a decommitment. If the current node index is odd, then the right part of the hash we already
+            // have computed.
+            unchecked {
+                root_ = _isEven(nodeIndex_) ? proofElements_[proofIndex_++] : hashes_[readIndex_++ % count_];
+            }
+
+            /// @dev `left_` is the left part of the node pair hash.
+            bytes32 left_;
+
+            // Based on the current node index and the next node index, we can determine if the left part of the hash
+            // is an existing computed hash or a decommitment.
+            unchecked {
+                left_ = _isLeftAnExistingHash(nodeIndex_, nextNodeIndex_)
+                    ? hashes_[readIndex_++ % count_]
+                    : proofElements_[proofIndex_++];
+            }
+
+            hashes_[writeIndex_ % count_] = _hashNodePair(left_, root_);
+            treeIndices_[writeIndex_ % count_] = nodeIndex_ >> 1;
+
+            unchecked {
+                ++writeIndex_;
+            }
+
+            // If we are not at the lowest tree index (i.e. there are nodes to the left that we have yet to process
+            // at this level), then continue.
+            // NOTE: Technically, if only `nextNodeIndex_ == lowestTreeIndex_`, and we did not use the hash at that
+            // `nextNodeIndex_` as part of this step's hashing, then it was a node not yet handled, but it will be
+            // handled in the next iteration, so the process will continue normally even if we prematurely "leveled up".
+            if (nodeIndex_ != lowestTreeIndex_ && nextNodeIndex_ != lowestTreeIndex_) continue;
+
+            // If we are at the lowest tree index (i.e. there are no nodes to the left that we have yet to process
+            // at this level), then we can update the lower bound and upper bound for the next level up.
+            // NOTE: Again, see the NOTE above.
+            lowestTreeIndex_ >>= 1;
+            upperBound_ >>= 1;
+        }
+    }
+
+    function _isEven(uint256 n_) internal pure returns (bool isEven_) {
+        return (n_ & 1) == 0;
+    }
+
+    /**
+     * @notice Checks if the left part of the hash should be an existing computed hash.
+     * @param  nodeIndex_            The index of the current node in the tree indices array.
+     * @param  nextNodeIndex_        The index of the next (lower) node in the tree indices array.
+     * @return isLeftAnExistingHash_ True if the left part of the hash should be an existing computed hash.
+     */
+    function _isLeftAnExistingHash(
+        uint256 nodeIndex_,
+        uint256 nextNodeIndex_
+    ) internal pure returns (bool isLeftAnExistingHash_) {
+        unchecked {
+            return _isEven(nodeIndex_) || (nextNodeIndex_ == nodeIndex_ - 1);
+        }
+    }
+
+    /**
+     * @notice Hashes a leaf of arbitrary size into a 32-byte leaf node.
+     * @param  leaf_ The leaf to hash.
+     * @return hash_ The hash of the leaf.
+     */
+    function _hashLeaf(bytes calldata leaf_) internal pure returns (bytes32 hash_) {
+        return keccak256(abi.encodePacked(LEAF_PREFIX, leaf_));
+    }
+
+    /**
+     * @notice Hashes a pair of 32-byte nodes into a 32-byte parent node.
+     * @param  leftNode_  The left node to hash.
+     * @param  rightNode_ The right node to hash.
+     * @return hash_      The hash of the pair of nodes.
+     */
+    function _hashNodePair(bytes32 leftNode_, bytes32 rightNode_) internal pure returns (bytes32 hash_) {
+        return keccak256(abi.encodePacked(NODE_PREFIX, leftNode_, rightNode_));
+    }
+
+    /**
+     * @notice Hashes a 32-byte node, without a right paired node, into a 32-byte parent node.
+     * @param  node_ The node to hash.
+     * @return hash_ The hash of the node.
+     */
+    function _hashPairlessNode(bytes32 node_) internal pure returns (bytes32 hash_) {
+        return keccak256(abi.encodePacked(NODE_PREFIX, node_));
+    }
+
+    /**
+     * @notice Hashes the topmost 32-byte node in the tree, combined with the tree's leaf count, into a 32-byte root.
+     * @param  leafCount_ The number of leaves in the merkle tree.
+     * @param  node_      The topmost node in the tree.
+     * @return hash_      The root hash of the tree.
+     */
+    function _hashRoot(uint256 leafCount_, bytes32 node_) internal pure returns (bytes32 hash_) {
+        return keccak256(abi.encodePacked(ROOT_PREFIX, leafCount_, node_));
+    }
+
+    /// @notice Get leaf nodes from arbitrary size leaves in calldata, in reverse order.
+    function _getReversedLeafNodesFromLeaves(
+        bytes[] calldata leaves_
+    ) internal pure returns (bytes32[] memory leafNodes_) {
+        uint256 count_ = leaves_.length;
+        leafNodes_ = new bytes32[](count_);
+        uint256 readIndex_ = count_;
+        uint256 writeIndex_;
+
+        while (writeIndex_ < count_) {
+            unchecked {
+                leafNodes_[writeIndex_++] = _hashLeaf(leaves_[--readIndex_]);
+            }
+        }
+    }
+}
diff --git a/test/unit/SequentialMerkleProofs.t.sol b/test/unit/SequentialMerkleProofs.t.sol
new file mode 100644
index 00000000..8d52818a
--- /dev/null
+++ b/test/unit/SequentialMerkleProofs.t.sol
@@ -0,0 +1,644 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    function test_getRoot_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    /* ============ getLeafCount ============ */
+
+    function test_getLeafCount_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getLeafCount(new bytes32[](0));
+    }
+
+    function test_getLeafCount_invalidLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidLeafCount.selector);
+        _sequentialMerkleProofs.getLeafCount(proofElements_);
+    }
+
+    function test_getLeafCount() external view {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+
+        assertEq(_sequentialMerkleProofs.getLeafCount(proofElements_), 0);
+
+        proofElements_[0] = bytes32(uint256(1));
+
+        assertEq(_sequentialMerkleProofs.getLeafCount(proofElements_), 1);
+
+        proofElements_[0] = bytes32(uint256(1111));
+
+        assertEq(_sequentialMerkleProofs.getLeafCount(proofElements_), 1111);
+    }
+
+    /* ============ bitCount32 ============ */
+
+    function test_bitCount32_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max) + 1);
+    }
+
+    function test_bitCount32() external view {
+        assertEq(_sequentialMerkleProofs.__bitCount32(0), 0);
+        assertEq(_sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max)), 32);
+
+        for (uint256 i_ = 0; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__bitCount32(1 << i_), 1); // One bit set
+            assertEq(_sequentialMerkleProofs.__bitCount32((1 << i_) - 1), i_); // i_ bits set
+        }
+    }
+
+    /* ============ roundUpToPowerOf2 ============ */
+
+    function test_roundUpToPowerOf2_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max) + 1);
+    }
+
+    function test_roundUpToPowerOf2() external view {
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(0), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(2), 2);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(3), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(4), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(5), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(6), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(7), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getBalancedLeafCount ============ */
+
+    function test_getBalancedLeafCount_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max) + 1);
+    }
+
+    function test_getBalancedLeafCount() external view {
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(0), 0);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(2), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(3), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(4), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(5), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(6), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(7), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getReversedLeafNodesFromLeaves ============ */
+
+    function test_getReversedLeafNodesFromLeaves() external view {
+        bytes[] memory leaves_ = new bytes[](4);
+
+        leaves_[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        leaves_[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        leaves_[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        leaves_[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        bytes32[] memory reversedLeafNodes_ = _sequentialMerkleProofs.__getReversedLeafNodesFromLeaves(leaves_);
+
+        assertEq(reversedLeafNodes_[0], 0x8613e086c6c42ab3b7027723e29bf61497f3629e0fb0c1b0453c93f053e22a18);
+        assertEq(reversedLeafNodes_[1], 0x09a458689dbd19291ce83007725819f0c69504e282d7669b7d08576162bd7f52);
+        assertEq(reversedLeafNodes_[2], 0x569dab69f958322840fb959938fee8dbc2bde84f55584935a8230303db6aaebd);
+        assertEq(reversedLeafNodes_[3], 0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b);
+    }
+
+    /* ============ helpers ============ */
+
+    function _buildBalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 2,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](2),
+                root: 0xeeef536868dc2c030bec2d3602cc13fbe660bd5d63deca6a0a4dfd201eb941c0
+            })
+        );
+
+        _balancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _balancedSamples[0].proofElements[0] = bytes32(_balancedSamples[0].leafCount);
+        _balancedSamples[0].proofElements[1] = bytes32(
+            0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b
+        );
+
+        /* ============ Sample 2 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 1,
+                leafCount: 8,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](4),
+                root: 0x00f8c0ad3c60c727ededce5717c8baa64047b5c3f29e409085df14dc3bfda1a7
+            })
+        );
+
+        _balancedSamples[1].leaves[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        _balancedSamples[1].leaves[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        _balancedSamples[1].leaves[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        _balancedSamples[1].leaves[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        _balancedSamples[1].proofElements[0] = bytes32(_balancedSamples[1].leafCount);
+        _balancedSamples[1].proofElements[1] = 0xb7f40118daf8bbb92b1759e810c6eb4b0b92e04d1b7e4be48a147721ea457d87;
+        _balancedSamples[1].proofElements[2] = 0x06331c9b61be683c1819b2cd20c83f315499477f6253ae8d8bb02cbc6bd93c9f;
+        _balancedSamples[1].proofElements[3] = 0xc3d064e0f1ace0f92e6c822d5346de5330fb23a6c31a0c84a80d9d5c8543cc0e;
+
+        /* ============ Sample 3 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 16,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](5),
+                root: 0x31338b156e26447f0a3a965981b0be87957bf5606b44e0dcdc99eb4646048942
+            })
+        );
+
+        _balancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _balancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _balancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _balancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _balancedSamples[2].proofElements[0] = bytes32(_balancedSamples[2].leafCount);
+        _balancedSamples[2].proofElements[1] = 0x194f8cb59bc03a6e9dfcc3566bed0a85a069f50de609f51af6c74dee673450d4;
+        _balancedSamples[2].proofElements[2] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _balancedSamples[2].proofElements[3] = 0x9d8799ecccbca75f60304876c8426007565302fdb72449fe980917d7847d43e7;
+        _balancedSamples[2].proofElements[4] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+    }
+
+    function _buildUnbalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 1,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](1),
+                root: 0x5b833bdf4f55e39d1838653841d4a2c651a71b5626b7936e1bedb5212cae96e3
+            })
+        );
+
+        _unbalancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _unbalancedSamples[0].proofElements[0] = bytes32(_unbalancedSamples[0].leafCount);
+
+        /* ============ Sample 2 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 4,
+                leafCount: 7,
+                leaves: new bytes[](2),
+                proofElements: new bytes32[](3),
+                root: 0x38631dd8b5081555ec3c51cc8db7918ee90158fa33a70674c1399234d23908b2
+            })
+        );
+
+        _unbalancedSamples[1].leaves[0] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+        _unbalancedSamples[1].leaves[1] = bytes(hex"51b7ae2bab96bd3fbb3b26e1efefb0b9b6a60054ed7ffcfa700374d58f315a31");
+
+        _unbalancedSamples[1].proofElements[0] = bytes32(_unbalancedSamples[1].leafCount);
+        _unbalancedSamples[1].proofElements[1] = 0x6b379ab3dd3b0b8df76cbe09e2a04f21ebf601f2932d3505ecfb89417f3de836;
+        _unbalancedSamples[1].proofElements[2] = 0x52bd35868bccc1b1f4c3ac67dd7e3b3db4a24f60436411162d633e6d1118de89;
+
+        /* ============ Sample 3 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 13,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](3),
+                root: 0xf92d4ca528834b0350cecd9307bec2dd97d0a6bbb58b077ab51cdad36fc5c087
+            })
+        );
+
+        _unbalancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _unbalancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _unbalancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _unbalancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _unbalancedSamples[2].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[2].proofElements[1] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _unbalancedSamples[2].proofElements[2] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+
+        /* ============ Sample 4 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 200,
+                leafCount: 324,
+                leaves: new bytes[](22),
+                proofElements: new bytes32[](7),
+                root: 0x8f28d0f19d1805a3b539bcc2bb0e627ded4bf5b873bebc9199ed179a30ca312c
+            })
+        );
+
+        _unbalancedSamples[3].leaves[0] = bytes(hex"fe0403dd500c862ddbbb4736c071a575d9fd43fbdeea602833234e9cd198bb3f");
+        _unbalancedSamples[3].leaves[1] = bytes(hex"5129d77889ad52ef899ba3d85e01a6df5c230804ccc586faa7e8376b89aa0600");
+        _unbalancedSamples[3].leaves[2] = bytes(hex"f1d6a296210c112eda6ca002273abc7d21b933cf99ca0c8292e344ba2d62f750");
+        _unbalancedSamples[3].leaves[3] = bytes(hex"907f141e1702157a9ca13881b0ecb0cb10be9b588bd6499479496965ce933225");
+        _unbalancedSamples[3].leaves[4] = bytes(hex"c77ccc18e17ec5d50bbbf775f564debfa1f9da8031be2fc7ce618cea8a625226");
+        _unbalancedSamples[3].leaves[5] = bytes(hex"aad8eb018edb0690bbdcebe48db820a6a13e3f8102bb0e5535fdfed79eaa5c39");
+        _unbalancedSamples[3].leaves[6] = bytes(hex"374346003edf10ea2da568e8e973857ec4291bf10a8bf81612b36efaf6e93c23");
+        _unbalancedSamples[3].leaves[7] = bytes(hex"45a3c2e32a3c8ea5e34562806f8279e85cfad69f6015588b599dc2e774d50cee");
+        _unbalancedSamples[3].leaves[8] = bytes(hex"9322c7cbfb342280d72d4eba15c0d9711e3f730072375bc4b0cf538970014e7a");
+        _unbalancedSamples[3].leaves[9] = bytes(hex"b3017e1b1874f68c4508c0b147a4f2b96dd166274c81bb4a86082d391efad6fa");
+        _unbalancedSamples[3].leaves[10] = bytes(hex"76d7f81c6813544dc594db8b5f3d6cef292e91d02a50ad556e6bcddc1eb59f8c");
+        _unbalancedSamples[3].leaves[11] = bytes(hex"320cfee2fb238c49255d102213b395963c2a3ebd7a8ee424e8df02b01361e484");
+        _unbalancedSamples[3].leaves[12] = bytes(hex"697fa39a1e23106c54eeecf38d213bfe2fcf896f55b790815085241b7e7e7361");
+        _unbalancedSamples[3].leaves[13] = bytes(hex"510fde4a04ecbe4bf94a514ab836074270959d2b96492a56909706de0b0248be");
+        _unbalancedSamples[3].leaves[14] = bytes(hex"5fa7828c8c661f4243c97527d637bf318e5e801dcf3b488f0f302622abdf61e9");
+        _unbalancedSamples[3].leaves[15] = bytes(hex"6f7fafb63e26bafeb5fd21a76615d0f2b45e6684324e294f10078661c00b4026");
+        _unbalancedSamples[3].leaves[16] = bytes(hex"e80b189bd7a1e5d0a04d9ba778d11f1bbffec4e276f502780a97fc7006715ac5");
+        _unbalancedSamples[3].leaves[17] = bytes(hex"25e66cd82e8fa7b6072d7759d2b49d0979bad20b018c730f4d1fcac7a6959bd4");
+        _unbalancedSamples[3].leaves[18] = bytes(hex"0695e86d43e2c5097acd182042d09cda438fc1f773335ce0f190dbed668ff508");
+        _unbalancedSamples[3].leaves[19] = bytes(hex"783eb995b1a99bb4181dad1c386dff06c5c02469a4893ca8c0a5b5fd82f7747a");
+        _unbalancedSamples[3].leaves[20] = bytes(hex"44fb0e55a04f4d5accd7a9e00fd5bbbf8926083ef169c0711f503ae16b589c20");
+        _unbalancedSamples[3].leaves[21] = bytes(hex"2815ef424dd0613d69f70546821ebddf2fe1b7452510cd21d25f3e438863e8a3");
+
+        _unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[3].leafCount);
+        _unbalancedSamples[3].proofElements[1] = 0x55f9b393403d39fdf3b35fe8f394e13a272509df05d5562e5da6937c11bb1214;
+        _unbalancedSamples[3].proofElements[2] = 0xa4c0ad363d17cb84be9e3f757278de2b26843e918e48e33e1b00438764fefa2b;
+        _unbalancedSamples[3].proofElements[3] = 0xafee522c5ba27318361dd426e355fa02e5821fbc8b2ba52dea3b36885d76ec94;
+        _unbalancedSamples[3].proofElements[4] = 0x25171fda9d5059c4cc4b8a86af5e0634dd8217cd2167c8d868733eef3f23054f;
+        _unbalancedSamples[3].proofElements[5] = 0x1e3106cd69a1fec956fa702eae218bdba94f7150d87c05210b282e334218d265;
+        _unbalancedSamples[3].proofElements[6] = 0xd2b4a539a1349d2d145fe307c30ce5bd43fa10887ce20a8a71da53300c0a6150;
+    }
+
+    function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
+        unchecked {
+            sample_.root = bytes32(uint256(sample_.root) + 1);
+        }
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        unchecked {
+            sample_.root = bytes32(uint256(sample_.root) - 1);
+        }
+    }
+
+    function _testVerifyWithIncorrectStartingIndex(Sample storage sample_) internal {
+        unchecked {
+            sample_.startingIndex += 1;
+        }
+
+        vm.expectRevert();
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        unchecked {
+            sample_.startingIndex -= 1;
+        }
+    }
+
+    function _testVerifyWithOutOfBoundsStartingIndex(Sample storage sample_) internal {
+        uint256 originalStartingIndex_ = sample_.startingIndex;
+
+        unchecked {
+            sample_.startingIndex = sample_.leafCount + 1;
+        }
+
+        vm.expectRevert();
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        unchecked {
+            sample_.startingIndex = originalStartingIndex_;
+        }
+    }
+
+    function _testVerifyWithIncorrectLeaf(Sample storage sample_) internal {
+        bytes memory originalLeaf_ = sample_.leaves[0];
+
+        sample_.leaves[0] = bytes(hex"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f");
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        sample_.leaves[0] = originalLeaf_;
+    }
+
+    function _testVerifyWithIncorrectLeafCount(Sample storage sample_) internal {
+        unchecked {
+            sample_.proofElements[0] = bytes32(uint256(sample_.proofElements[0]) + 1);
+        }
+
+        vm.expectRevert();
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        unchecked {
+            sample_.proofElements[0] = bytes32(uint256(sample_.proofElements[0]) - 1);
+        }
+    }
+
+    function _testVerifyWithIncorrectProofElement(Sample storage sample_) internal {
+        unchecked {
+            sample_.proofElements[1] = bytes32(uint256(sample_.proofElements[1]) + 1);
+        }
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        unchecked {
+            sample_.proofElements[1] = bytes32(uint256(sample_.proofElements[1]) - 1);
+        }
+    }
+}
diff --git a/test/utils/Harnesses.sol b/test/utils/Harnesses.sol
index 2a2fc519..51655063 100644
--- a/test/utils/Harnesses.sol
+++ b/test/utils/Harnesses.sol
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
 import { EnumerableSet } from "../../lib/oz/contracts/utils/structs/EnumerableSet.sol";
 
 import { ParameterKeys } from "../../src/libraries/ParameterKeys.sol";
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
 
 import { AppChainGateway } from "../../src/app-chain/AppChainGateway.sol";
 import { AppChainParameterRegistry } from "../../src/app-chain/AppChainParameterRegistry.sol";
@@ -356,3 +357,60 @@ contract ParameterKeysHarness {
         return ParameterKeys.uint256ToKeyComponent(value_);
     }
 }
+
+contract SequentialMerkleProofsHarness {
+    function verify(
+        bytes32 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        bytes32[] calldata proofElements_
+    ) external pure {
+        SequentialMerkleProofs.verify(root_, startingIndex_, leaves_, proofElements_);
+    }
+
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        bytes32[] calldata proofElements_
+    ) external pure returns (bytes32 root_) {
+        return SequentialMerkleProofs.getRoot(startingIndex_, leaves_, proofElements_);
+    }
+
+    function getLeafCount(bytes32[] calldata proofElements_) external pure returns (uint256 leafCount_) {
+        return SequentialMerkleProofs.getLeafCount(proofElements_);
+    }
+
+    function __bitCount32(uint256 n_) external pure returns (uint256 bitCount_) {
+        return SequentialMerkleProofs._bitCount32(n_);
+    }
+
+    function __roundUpToPowerOf2(uint256 n_) external pure returns (uint256 powerOf2_) {
+        return SequentialMerkleProofs._roundUpToPowerOf2(n_);
+    }
+
+    function __getBalancedLeafCount(uint256 leafCount_) external pure returns (uint256 balancedLeafCount_) {
+        return SequentialMerkleProofs._getBalancedLeafCount(leafCount_);
+    }
+
+    function __hashLeaf(bytes calldata leaf_) external pure returns (bytes32 hash_) {
+        return SequentialMerkleProofs._hashLeaf(leaf_);
+    }
+
+    function __hashNodePair(bytes32 leftNode_, bytes32 rightNode_) external pure returns (bytes32 hash_) {
+        return SequentialMerkleProofs._hashNodePair(leftNode_, rightNode_);
+    }
+
+    function __hashPairlessNode(bytes32 node_) external pure returns (bytes32 hash_) {
+        return SequentialMerkleProofs._hashPairlessNode(node_);
+    }
+
+    function __hashRoot(uint256 leafCount_, bytes32 root_) external pure returns (bytes32 hash_) {
+        return SequentialMerkleProofs._hashRoot(leafCount_, root_);
+    }
+
+    function __getReversedLeafNodesFromLeaves(
+        bytes[] calldata leaves_
+    ) external pure returns (bytes32[] memory reversedLeaves_) {
+        return SequentialMerkleProofs._getReversedLeafNodesFromLeaves(leaves_);
+    }
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (3)</summary><blockquote>

`45-52`: **Cheaper revert pattern for `verify`**  
Using `require(cond, CustomError())` is valid but costs an extra stack slot and builds the revert data eagerly.  
A straight `if (!cond) revert CustomError();` is ~20–40 gas cheaper and clearer.

```diff
-function verify(
-    uint256 root_,
-    uint256 startingIndex_,
-    bytes[] calldata leaves_,
-    uint256[] calldata proofElements_
-) internal pure {
-    require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
-}
+function verify(
+    uint256 root_,
+    uint256 startingIndex_,
+    bytes[] calldata leaves_,
+    uint256[] calldata proofElements_
+) internal pure {
+    if (getRoot(startingIndex_, leaves_, proofElements_) != root_) {
+        revert InvalidProof();
+    }
+}
```

---

`103-115`: **Unchecked arithmetic can save gas in hot path**  
`_roundUpToPowerOf2` uses several `|=` and `+ 1` operations on a bounded `uint64`. Wrapping the whole body in `unchecked { … }` avoids redundant overflow checks while remaining safe due to the earlier `require`.  

```diff
function _roundUpToPowerOf2(uint256 n_) internal pure returns (uint256 powerOf2_) {
    require(n_ <= type(uint64).max, InvalidRoundUpToPowerOf2Input());
-   if (_bitCount64(n_) == 1) return n_;
-   n_ |= n_ >> 1;
-   …
-   return n_ + 1;
+   if (_bitCount64(n_) == 1) return n_;
+   unchecked {
+       n_ |= n_ >> 1;
+       …
+       return n_ + 1;
+   }
}
```
A similar pattern applies to the counters in `_getRoot`.

---

`141-210`: **Safety & readability of the `while (true)` loop**  
The main reconstruction loop is intricate and relies on several invariants for termination.  
Consider:

1. Adding an explicit upper-bound guard (e.g., `while (++iterations < 256)`) to prevent accidental infinite loops if invariants are broken by future edits.
2. Documenting the loop invariants (expected relationships between `readIndex_`, `writeIndex_`, `lowestTreeIndex_`, etc.) in NatSpec comments to aid maintenance.

These additions cost little gas (when done with `unchecked`) but greatly improve auditability.

</blockquote></details>
<details>
<summary>test/utils/Harnesses.sol (1)</summary><blockquote>

`361-378`: **Return a bool to simplify test ergonomics (optional)**  
`SequentialMerkleProofs.verify` reverts on failure and has no return value.  
Exposing it in the harness as a function that *returns* a boolean (`true` on success) instead of reverting would let tests use `assertTrue()` instead of `vm.expectRevert` in some cases.

```diff
-function verify( … ) external pure {
-    SequentialMerkleProofs.verify( … );
+function verify( … ) external pure returns (bool success_) {
+    SequentialMerkleProofs.verify( … );
+    return true;
 }
```

Not critical—current tests work—but it can make future property-based fuzzing easier.

</blockquote></details>
<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary><blockquote>

`824-830`: **Nit: micro-optimise the loop to keep Forge runs fast**  
The `for (uint256 i_ = 0; i_ < 64; ++i_)` loop performs 128 assertions and is executed every test run.  
Using `unchecked { ++i_; }` and caching the harness reference outside the loop body can shave a few hundred gas off each test iteration, speeding up large test suites.

Not a blocker—purely a test performance nicety.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 905b9fd7b8d266602078750784036e9194c3a9f2 and 12e777f520379ccaa955405b3f9fa3f0e7266703.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Tests
* GitHub Check: Slither
* GitHub Check: Contacts Sizes
* GitHub Check: Lint check
* GitHub Check: Formatting check
* GitHub Check: Gas Report
* GitHub Check: Code Coverage

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (1)</summary>

`2-2`: **Unreleased compiler version may break CI**  
`pragma solidity 0.8.28;` targets a compiler that (at the time of writing) is not yet officially released.  
Most public CI pipelines and toolchains only ship up to 0.8.25/0.8.26, which will cause build failures for contributors and users.

```diff
-pragma solidity 0.8.28;
+pragma solidity ^0.8.20; // or the latest released minor you rely on
```

Pin the version once 0.8.28 is officially available, otherwise require-wide caret (`^`) or lower the minor.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T18:36:29Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T18:46:50Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T18:47:11Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (4)</summary><blockquote>

`22-24`: **Tiny wording issue in the `NoLeaves` error doc-string**

`"Thrown when the no leaves are passed"` → drop the extra article.

```diff
-/// @notice Thrown when the no leaves are passed.
+/// @notice Thrown when no leaves are passed.
```

---

`48-55`: **`verify` signature & root type could be more idiomatic**

You treat hashes as `uint256`, yet most tooling (and other contracts) use `bytes32` for Merkle roots and nodes.  
Sticking to `bytes32` avoids unintended integer arithmetic and makes interfaces clearer.

```diff
-function verify(
-    uint256 root_,
+function verify(
+    bytes32 root_,
 ...
 ) internal pure {
-    require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    require(bytes32(getRoot(startingIndex_, leaves_, proofElements_)) == root_, InvalidProof());
 }
```

This also avoids silently accepting numeric roots larger than `2**256-1` once cast from `bytes32`.  
(You would do the analogous change for `getRoot` and the hashing helpers.)

---

`155-160`: **Minor comment typos (“aa full pass”)**

The inline documentation is great, but the duplicated typo might trip linters / static-analysis tooling.

```diff
-// Instead of doing aa full pass through the empty tree indices array
+// Instead of doing a full pass through the empty tree indices array
```



Also applies to: 185-190

---

`245-247`: **Gas-optimise prefix constants**

`abi.encodePacked(LEAF_PREFIX, leaf_)` copies the dynamic `bytes` prefix into memory each iteration.  
Storing the prefix as a fixed-length `bytes32` (left-aligned, zero-padded) lets the compiler embed it directly and saves 3–4 gas per hash.

```diff
-bytes internal constant LEAF_PREFIX = bytes("leaf|");
+bytes32 internal constant LEAF_PREFIX = 0x6c6561667c000000000000000000000000000000000000000000000000000000;
...
-return uint256(keccak256(abi.encodePacked(LEAF_PREFIX, leaf_)));
+return uint256(keccak256(abi.encodePacked(LEAF_PREFIX, leaf_)));
```

(The same applies to `NODE_PREFIX` and `ROOT_PREFIX`.)

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 12e777f520379ccaa955405b3f9fa3f0e7266703 and 29aa73cd4703b03217f9307cdca9b64e3431f03c.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* test/unit/SequentialMerkleProofs.t.sol

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Tests
* GitHub Check: Contacts Sizes
* GitHub Check: Slither
* GitHub Check: Lint check
* GitHub Check: Formatting check
* GitHub Check: Gas Report
* GitHub Check: Code Coverage

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (1)</summary>

`1-2`: <details>
<summary>❌ Incorrect review comment</summary>

**Pragma targets an unreleased compiler version**

`pragma solidity 0.8.28;` requires a compiler that (at the time of writing) is not yet published.  
Unless you have a custom tool-chain, this will break CI and downstream integrators.  
Prefer pinning to the latest released minor (e.g. `0.8.26`) or use a caret range (`^0.8.20`) if you need flexibility.

```diff
-pragma solidity 0.8.28;
+pragma solidity ^0.8.20; // or the latest released patch
```

---

🌐 Web query:

```web
What is the latest released patch version of Solidity 0.8.x?
```

💡 Result:

The latest released patch version of Solidity 0.8.x, as of May 2, 2025, is **Solidity v0.8.29**[3][7]. This version was officially announced on March 12, 2025, and introduces experimental support for the EVM Object Format (EOF), support for custom storage layouts, initial support for ethdebug, and other improvements[7].

Citations:

- 1: https://docs.soliditylang.org
- 2: https://soliditylang.org/blog/category/releases/
- 3: https://github.com/ethereum/solidity/releases
- 4: https://docs.soliditylang.org/en/latest/080-breaking-changes.html
- 5: https://soliditylang.org/blog/2024/03/14/solidity-0.8.25-release-announcement/
- 6: https://soliditylang.org/blog/2020/10/28/solidity-0.8.x-preview/
- 7: https://soliditylang.org/blog/2025/03/12/solidity-0.8.29-release-announcement/
- 8: https://github.com/ethereum/solidity/blob/develop/Changelog.md
- 9: https://soliditylang.org/blog/2020/12/16/solidity-v0.8.0-release-announcement/
- 10: https://docs.soliditylang.org/_/downloads/en/latest/pdf/

---



</details>

**Pragma Version Valid**

The specified `pragma solidity 0.8.28;` is already a published compiler release (superseded only by 0.8.29). You won’t break CI by targeting 0.8.28.  
If you’d rather track the very latest patch, you can bump to:

```solidity
pragma solidity 0.8.29;
```

Or, to automatically pick up any future 0.8.x patch, use a caret range:

```solidity
pragma solidity ^0.8.0;
```


> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T18:52:04Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:38:02Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T14:38:29Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (3)</summary><blockquote>

`145-168`: **Cache `proofElements_[0]` to save gas and avoid double `calldata` reads**

`proofElements_[0]` is accessed (and cast) three times in this block – once for the `require` and twice for the `upperBound_` / `lowestTreeIndex_` calculations.  
Because `bytes32[] calldata` lives in `calldata`, every read costs 3 gas. Caching it into a `uint256 proofLeafCount_` eliminates two of those external reads and one redundant `_getBalancedLeafCount` call.

```diff
-        require(startingIndex_ + hashes_.length <= uint256(proofElements_[0]), InvalidProof());

-        uint256 count_ = hashes_.length;
+        uint256 proofLeafCount_ = uint256(proofElements_[0]);
+        require(startingIndex_ + hashes_.length <= proofLeafCount_, InvalidProof());
+
+        uint256 count_ = hashes_.length;

-        uint256 upperBound_ = _getBalancedLeafCount(uint256(proofElements_[0])) + uint256(proofElements_[0]) - 1;
-        uint256 lowestTreeIndex_ = _getBalancedLeafCount(uint256(proofElements_[0])) + startingIndex_;
+        uint256 balancedCount_ = _getBalancedLeafCount(proofLeafCount_);
+        uint256 upperBound_ = balancedCount_ + proofLeafCount_ - 1;
+        uint256 lowestTreeIndex_ = balancedCount_ + startingIndex_;
```

This small change pays for itself even for short proofs and keeps the algorithm untouched.

---

`84-108`: **Duplicate 64-bit range check – consider removing for gas efficiency**

Both `_bitCount64` (line 84) and `_roundUpToPowerOf2` (line 106) enforce  
`n_ <= type(uint64).max`. Because `_roundUpToPowerOf2` always calls `_bitCount64`, most callers hit the check twice, burning an extra `require`.

Unless you expect `_bitCount64` to be called in isolation elsewhere, you can safely drop the guard in one of the two helpers (preferably `_bitCount64`) and rely on the other to defend against overflow, shaving 20–40 gas per invocation.

---

`304-307`: **Typo in NatSpec: “lead count” → “leaf count”**

Minor documentation typo that could confuse users skimming the comments.

```diff
- * @notice Hashes the topmost 32-byte node in the tree, combined with the tree's lead count, into a 32-byte root.
+ * @notice Hashes the topmost 32-byte node in the tree, combined with the tree's leaf count, into a 32-byte root.
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 29aa73cd4703b03217f9307cdca9b64e3431f03c and 8cf050b19a64b7a23fe88d323d4c706aa6a27eec.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* test/utils/Harnesses.sol
* test/unit/SequentialMerkleProofs.t.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>📓 Common learnings</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>
<details>
<summary>src/libraries/SequentialMerkleProofs.sol (1)</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Tests
* GitHub Check: Slither
* GitHub Check: Code Coverage
* GitHub Check: Gas Report
* GitHub Check: Formatting check
* GitHub Check: Lint check

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T15:30:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (3)</summary><blockquote>

`17-37`: **`view` mutability on test functions is unnecessary and slightly misleading**

All those test functions (`test_hashLeaf`, `test_hashNodePair`, `test_hashPairlessNode`, `test_hashRoot`, …) call `assertEq`, which internally emits events and therefore changes the transaction’s log.  
Emitting logs is a side-effect, so the functions are **not** strictly “view” even though the EVM allows it. Prefer leaving mutability unspecified (default is non-payable) to:

1. Avoid confusing readers who may think the tests are pure/view.
2. Silence solidity-lint warnings raised by some IDEs.

```diff
-function test_hashLeaf() external view {
+function test_hashLeaf() external {
```

Apply the same change to the other `*_hash*` tests.  



Also applies to: 39-63, 65-93, 95-113

---

`807-815`: **Exhaustive loop can be replaced by fuzzing to reduce gas/time**

Iterating 64 times inside `forge test` is fine, but using Foundry’s fuzzing would give the same (or better) coverage with fewer hard-coded assertions:

```solidity
function test_bitCount64_fuzz(uint64 x) external {
    assertEq(_sequentialMerkleProofs.__bitCount64(x), popcount(x));
}
```

(Implement `popcount` in the test or rely on the library itself.)

This keeps the test suite fast as it grows.

---

`528-537`: **Hard-coding large proof samples in test files inflates repo size**

Seven 32-byte constants per sample + dozens of samples quickly add up.  
Consider generating proofs on-the-fly (or loading them from compressed JSON/fixture files) to keep the Solidity source light and readable.

If the fixtures must stay on-chain for determinism, moving them to `vm.readFileBinary()` is another alternative.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8cf050b19a64b7a23fe88d323d4c706aa6a27eec and 6750ad8ba2cef859f6a624b3a6736a2b68ccab97.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/libraries/SequentialMerkleProofs.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📓 Common learnings</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Slither
* GitHub Check: Tests
* GitHub Check: Contacts Sizes
* GitHub Check: Lint check
* GitHub Check: Gas Report
* GitHub Check: Formatting check
* GitHub Check: Code Coverage

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary>

`871-885`: **Missing negative-case for `_getReversedLeafNodesFromLeaves`**

You already cover successful paths, but not the revert scenario when `leaves_.length == 0`. Adding one assertion similar to other “*_noLeaves” tests would lock the behaviour down and protect against future regressions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T18:27:46Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (1)</summary><blockquote>

`2-2`: **Unreleased compiler version may break CI / tooling**  

`pragma solidity 0.8.28;` pins to a version that is not yet available on the official Solidity release channel.  
Unless you are deliberately targeting a custom/nightly build, most contributors (and the CI pipeline) will fail to compile this contract.

```diff
-pragma solidity 0.8.28;
+// Consider pinning to the latest released version or a semver range.
+pragma solidity ^0.8.20; // adjust upper bound if you rely on ≥0.8.25 features
```

If you genuinely need 0.8.28 please drop a comment explaining the rationale and how other devs can obtain the matching compiler.

</blockquote></details>
<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary><blockquote>

`2-2`: **Same unreleased compiler version issue as core library**  

The test contract also targets `pragma solidity 0.8.28;`, which is not on the public release track and will cause `forge test` (and other tool-chains) to fail out-of-the-box.  Please align with a released version or add guidance for contributors on obtaining the required compiler build.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/libraries/SequentialMerkleProofs.sol (1)</summary><blockquote>

`95-100`: **Typo in doc-string: “632-bit” should be “32-bit”**  

The NatSpec comment for `_roundUpToPowerOf2` refers to “a 632-bit unsigned integer”, which is confusing and contradicts the 32-bit constraint enforced by `_bitCount32`.  Correcting the comment avoids future misunderstandings.

```diff
- * @notice Rounds a 632-bit unsigned integer up to the nearest power of 2.
+ * @notice Rounds a 32-bit unsigned integer up to the nearest power of 2.
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6750ad8ba2cef859f6a624b3a6736a2b68ccab97 and ad68a179f75de40da89b67e2f3fc4a2de58820ab.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* test/utils/Harnesses.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>📓 Common learnings</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>
<details>
<summary>src/libraries/SequentialMerkleProofs.sol (1)</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Slither
* GitHub Check: Formatting check
* GitHub Check: Code Coverage
* GitHub Check: Gas Report
* GitHub Check: Lint check
* GitHub Check: Contacts Sizes
* GitHub Check: Tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T05:31:10Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T16:54:23Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T16:54:59Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (4)</summary><blockquote>

`642-645`: **Extraneous parentheses create a no-op expression**

The call to `getRoot` is wrapped in an additional pair of parentheses.  
Although harmless, it adds noise and may raise questions for future readers.

```diff
-(_sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1)));
+_sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
```

---

`851-857`: **Constant `1 >> 1` is less explicit than using `0` directly**

`1 >> 1` evaluates to `0`, so the first assertion is effectively `bitCount32(0) == 0`.  
Using the literal `0` keeps the intent crystal-clear and avoids any mental indirection.

```diff
-assertEq(_sequentialMerkleProofs.__bitCount32(1 >> 1), 0);
+assertEq(_sequentialMerkleProofs.__bitCount32(0), 0);
```

---

`158-174`: **Consider consolidating repeated sample-data setup**

`test_verify_balanced_sample1` and its *_invalidProof* counterpart duplicate the set-up
for `startingIndex_`, `leafCount_`, `leaves_`, `proofElements_`, and `root_`.
This pattern repeats for every “sampleX” pair, bloating the file and making updates error-prone.

A small helper (internal) function that returns the tuple or populates a struct could
eliminate the duplication and keep each test focused on its unique assertion / revert
expectation.

Example sketch (not a full diff):

```solidity
function _balancedSample1()
    internal
    pure
    returns (
        uint256 startingIndex,
        bytes[] memory leaves,
        bytes32[] memory proofElements,
        bytes32 root
    )
{ ... }
```

Then each test becomes a 2-liner:

```solidity
(uint256 s, bytes[] memory l, bytes32[] memory p, bytes32 r) = _balancedSample1();
vm.expectRevert(...);
_sequentialMerkleProofs.verify(r, s, l, p);
```



Also applies to: 176-213

---

`868-885`: **Gas-intensive loops can be run unchecked in tests**

The three `for`-loops (`bitCount32`, `roundUpToPowerOf2`, `getBalancedLeafCount`)
run only 28 iterations each, so gas is negligible.  
Still, wrapping the increment in `unchecked { ++i_; }` avoids unnecessary overflow
checks and documents that safety is not a concern inside the test context.

```diff
-for (uint256 i_ = 0; i_ < 32; ++i_) {
+for (uint256 i_ = 0; i_ < 32; ) {
     ...
-    ++i_;
+    unchecked { ++i_; }
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 781c5b60ac40aa5588b71d885887c3322cd19e4f and 78ebe245342e84024caeeff2942ccbd52181f1bf.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/libraries/SequentialMerkleProofs.sol

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary>

`1-3`: **Version pinning looks good, just ensure consistency across the repo**

`pragma solidity 0.8.28;` is a released compiler version, so the choice is valid.
Please double-check that every source file (including the newly added library and
harness) pins to the same compiler version to avoid mixed-version build issues.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-07T15:03:24Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary><blockquote>

`4-4`: **Minor: unused import**

`stdError` is imported but never referenced, which may trigger a compiler or linter warning. Remove it unless you plan to `assertEq` on revert data sizes.

```diff
-import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+import { Test } from "../../lib/forge-std/src/Test.sol";
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 78ebe245342e84024caeeff2942ccbd52181f1bf and 67c2995e6312a8c72ba31c63f6bfeedb762687cd.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Tests
* GitHub Check: Slither
* GitHub Check: Contacts Sizes
* GitHub Check: Gas Report
* GitHub Check: Code Coverage
* GitHub Check: Formatting check
* GitHub Check: Lint check

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T18:35:58Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T00:41:28Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T00:42:04Z
user: login: fbac
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T15:15:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (6)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (6)</summary><blockquote>

`173-176`: _⚠️ Potential issue_

**Loop only checks the first balanced sample – iterator ignored**

`sample_` is hard-wired to `_balancedSamples[0]`, so the loop silently skips every sample except the first one.  
All subsequent balanced proofs remain untested, defeating the purpose of populating the array.

```diff
-            Sample storage sample_ = _balancedSamples[0];
+            Sample storage sample_ = _balancedSamples[index_];
```

---

`196-199`: _⚠️ Potential issue_

**Same iterator bug for unbalanced samples**

Identical issue: the loop iterates, but always re-uses element `0`.

```diff
-            Sample storage sample_ = _unbalancedSamples[0];
+            Sample storage sample_ = _unbalancedSamples[index_];
```

---

`259-263`: _⚠️ Potential issue_

**getRoot (balanced) – only first sample exercised**

`sample_` should be selected via `index_`; otherwise six balanced roots are never asserted.

```diff
-            Sample storage sample_ = _balancedSamples[0];
+            Sample storage sample_ = _balancedSamples[index_];
```

---

`270-274`: _⚠️ Potential issue_

**getRoot (unbalanced) – identical indexing bug**

Same copy-&-paste error; replace constant `0` with `index_`.

```diff
-            Sample storage sample_ = _unbalancedSamples[0];
+            Sample storage sample_ = _unbalancedSamples[index_];
```

---

`558-558`: _⚠️ Potential issue_

**Incorrect leaf-count element for unbalanced sample 4**

`proofElements[0]` must encode sample 4’s `leafCount`, but it erroneously copies from sample 3.  
The proof is therefore invalid and the test will pass for the wrong reason.

```diff
-_unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+_unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[3].leafCount);
```

---

`567-574`: _🛠️ Refactor suggestion_

**Mutating shared fixture in place risks overflow & test pollution**

1. If `sample_.root == type(uint256).max`, the `+ 1` causes an overflow revert (Solidity ≥0.8 checks).
2. Altering the struct in-place can leak the tampered value into later tests if a revert occurs elsewhere.

Use a local copy (or `unchecked`) instead of mutating storage:

```diff
-function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
-    sample_.root = bytes32(uint256(sample_.root) + 1);
+function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
+    bytes32 wrongRoot_;
+    unchecked {
+        wrongRoot_ = bytes32(uint256(sample_.root) + 1);
+    }
     vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
-    _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
-    sample_.root = bytes32(uint256(sample_.root) - 1);
+    _sequentialMerkleProofs.verify(wrongRoot_, sample_.startingIndex, sample_.leaves, sample_.proofElements);
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary><blockquote>

`24-29`: **Minor gas/readability nit – make `setUp` `internal`**

`setUp` is only invoked by Foundry; it need not be `external`. Declaring it `internal` (the default) removes an external‐call slot from the test bytecode.

```diff
-function setUp() external {
+function setUp() internal {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 67c2995e6312a8c72ba31c63f6bfeedb762687cd and dab0b89f0bbc7d75ed897257a2ae1b6da922b80f.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/libraries/SequentialMerkleProofs.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/libraries/SequentialMerkleProofs.sol

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* test/utils/Harnesses.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📓 Common learnings</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.079Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Tests
* GitHub Check: Slither
* GitHub Check: Contacts Sizes
* GitHub Check: Lint check
* GitHub Check: Code Coverage
* GitHub Check: Formatting check
* GitHub Check: Gas Report

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T16:00:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary><blockquote>

`585-586`: **Consider specifying expected error types.**

The tests use `vm.expectRevert()` without specifying which error is expected. For better test precision and documentation, consider specifying the expected error selector.

```diff
- vm.expectRevert();
+ vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector); // Or the appropriate error
```


Also applies to: 600-601, 624-625

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dab0b89f0bbc7d75ed897257a2ae1b6da922b80f and bece9db99f1e18d96a7f54e97338da4c10bbd51d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📓 Common learnings</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.079Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: Slither
* GitHub Check: Formatting check
* GitHub Check: Lint check
* GitHub Check: Gas Report
* GitHub Check: Code Coverage

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (5)</summary>

`2-2`: **Solid choice for latest released Solidity version.**

Using Solidity 0.8.28 is appropriate, as it's a stable released version with good compiler optimizations.

---

`567-578`: **Great use of unchecked blocks for test manipulations.**

The unchecked arithmetic for test data modification is a good optimization since we're just changing values for testing purposes and don't need overflow protection.

---

`173-176`: **Fixed indexing bug in sample loops.**

The previous issue where only the first sample was being tested has been properly fixed. Now each loop correctly uses the index variable to access array elements.



Also applies to: 196-199, 258-266, 270-277

---

`558-558`: **Fixed leaf count in sample 4.**

The proofElements now correctly uses leaf count from the current sample rather than from sample 2 as previously noted.

---

`10-644`: **Comprehensive test suite with excellent coverage.**

This test contract provides thorough coverage of the SequentialMerkleProofs library, including:
- Hash function verification with known outputs
- Validation of proof verification for both balanced and unbalanced trees
- Root calculation tests
- Leaf count extraction
- Bit manipulation utilities
- Error handling for invalid inputs
- Edge case testing through data corruption

The samples used cover diverse tree sizes and structures, giving confidence in the library's robustness.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T16:34:01Z

Review comments:
diff_hunk: @@ -0,0 +1,259 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    /// @notice The leaf prefix used to hash to a leaf.
+    bytes internal constant LEAF_PREFIX = bytes("leaf|");
+
+    /// @notice The node prefix used to hash to a node.
+    bytes internal constant NODE_PREFIX = bytes("node|");
+
+    /// @notice The root prefix used to hash to a root.
+    bytes internal constant ROOT_PREFIX = bytes("root|");
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when the no leaves are passed.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _roundUpToPowerOf2 is greater than type(uint64).max.
+    error InvalidRoundUpToPowerOf2Input();
+
+    /// @notice Thrown when the input to _bitCount64 is greater than type(uint64).max.
+    error InvalidBitCount64Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        uint256 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        require(leaves_.length > 0, NoLeaves());
+
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
path: src/libraries/SequentialMerkleProofs.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing length-check for `proofElements_`**  
`getRoot` immediately reads `proofElements_[0]`; if the caller passes an empty array, execution reverts with a generic out-of-bounds error instead of a clear custom error.

```diff
require(leaves_.length > 0, NoLeaves());
+require(proofElements_.length > 0, InvalidProof());
```

This keeps revert semantics explicit and prevents ambiguous failure reasons.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    function getRoot(
        uint256 startingIndex_,
        bytes[] calldata leaves_,
        uint256[] calldata proofElements_
    ) internal pure returns (uint256 root_) {
        require(leaves_.length > 0, NoLeaves());
        require(proofElements_.length > 0, InvalidProof());

        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit 29aa73c

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T18:36:28Z
author_association: NONE
diff_hunk: @@ -0,0 +1,259 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    /// @notice The leaf prefix used to hash to a leaf.
+    bytes internal constant LEAF_PREFIX = bytes("leaf|");
+
+    /// @notice The node prefix used to hash to a node.
+    bytes internal constant NODE_PREFIX = bytes("node|");
+
+    /// @notice The root prefix used to hash to a root.
+    bytes internal constant ROOT_PREFIX = bytes("root|");
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when the no leaves are passed.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _roundUpToPowerOf2 is greater than type(uint64).max.
+    error InvalidRoundUpToPowerOf2Input();
+
+    /// @notice Thrown when the input to _bitCount64 is greater than type(uint64).max.
+    error InvalidBitCount64Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        uint256 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        require(leaves_.length > 0, NoLeaves());
+
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
path: src/libraries/SequentialMerkleProofs.sol
user: login: deluca-mike
body: God catch, will add this check (and others) in the `_getRoot` function.
created_at: 2025-05-02T18:46:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,259 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    /// @notice The leaf prefix used to hash to a leaf.
+    bytes internal constant LEAF_PREFIX = bytes("leaf|");
+
+    /// @notice The node prefix used to hash to a node.
+    bytes internal constant NODE_PREFIX = bytes("node|");
+
+    /// @notice The root prefix used to hash to a root.
+    bytes internal constant ROOT_PREFIX = bytes("root|");
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when the no leaves are passed.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _roundUpToPowerOf2 is greater than type(uint64).max.
+    error InvalidRoundUpToPowerOf2Input();
+
+    /// @notice Thrown when the input to _bitCount64 is greater than type(uint64).max.
+    error InvalidBitCount64Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        uint256 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        require(leaves_.length > 0, NoLeaves());
+
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
path: src/libraries/SequentialMerkleProofs.sol
user: login: coderabbitai[bot]
body: @deluca-mike That's a good approach! Adding the check in the internal `_getRoot` function is even better as it centralizes the validation logic at the core implementation level. This ensures the validation is consistently applied regardless of how the function is called.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike That's a good approach! Adding the check in the internal `_getRoot` function is even better as it centralizes the validation logic at the core implementation level. This ensures the validation is consistently applied regardless of how the function is called.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-02T18:47:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,291 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    /// @notice The leaf prefix used to hash to a leaf.
+    bytes internal constant LEAF_PREFIX = bytes("leaf|");
+
+    /// @notice The node prefix used to hash to a node.
+    bytes internal constant NODE_PREFIX = bytes("node|");
+
+    /// @notice The root prefix used to hash to a root.
+    bytes internal constant ROOT_PREFIX = bytes("root|");
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when the no leaves are passed.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _roundUpToPowerOf2 is greater than type(uint64).max.
+    error InvalidRoundUpToPowerOf2Input();
+
+    /// @notice Thrown when the input to _bitCount64 is greater than type(uint64).max.
+    error InvalidBitCount64Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /// @notice Thrown when no proof elements are provided.
+    error NoProofElements();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        uint256 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
+
+    /* ============ Helper Functions ============ */
+
+    /**
+     * @notice Counts number of set bits (1's) in 64-bit unsigned integer.
+     * @dev    See https://en.wikipedia.org/wiki/Hamming_weight implementation `popcount64b`.
+     * @param  n_        The number to count the set bits of.
+     * @return bitCount_ The number of set bits in `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _bitCount64(uint256 n_) internal pure returns (uint256 bitCount_) {
+        require(n_ <= type(uint64).max, InvalidBitCount64Input());
+
+        n_ -= (n_ >> 1) & 0x5555555555555555;
+        n_ = (n_ & 0x3333333333333333) + ((n_ >> 2) & 0x3333333333333333);
+        n_ = (n_ + (n_ >> 4)) & 0x0f0f0f0f0f0f0f0f;
+        n_ += n_ >> 8;
+        n_ += n_ >> 16;
+        n_ += n_ >> 32;
+
+        return n_ & 0x7f;
+    }
+
+    /**
+     * @notice Rounds a 64-bit unsigned integer up to the nearest power of 2.
+     * @param  n_        The number to round up to the nearest power of 2.
+     * @return powerOf2_ The nearest power of 2 to `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _roundUpToPowerOf2(uint256 n_) internal pure returns (uint256 powerOf2_) {
+        require(n_ <= type(uint64).max, InvalidRoundUpToPowerOf2Input());
+
+        if (_bitCount64(n_) == 1) return n_;
+
+        n_ |= n_ >> 1;
+        n_ |= n_ >> 2;
+        n_ |= n_ >> 4;
+        n_ |= n_ >> 8;
+        n_ |= n_ >> 16;
+        n_ |= n_ >> 32;
+
+        return n_ + 1;
+    }
+
+    /**
+     * @notice Gets the balanced leaf count for a sequential merkle proof.
+     * @param  leafCount_         The number of leaves in the merkle tree.
+     * @return balancedLeafCount_ The balanced leaf count.
+     */
+    function _getBalancedLeafCount(uint256 leafCount_) internal pure returns (uint256 balancedLeafCount_) {
+        require(leafCount_ != 0, NoLeaves());
+
+        return leafCount_ == 1 ? 2 : _roundUpToPowerOf2(leafCount_);
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  hashes_        The leaf hashes (in reverse order) to prove sequential existence from the starting index.
+     * @param  proofElements_         The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     */
+    function _getRoot(
+        uint256 startingIndex_,
+        uint256[] memory hashes_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        require(hashes_.length > 0, NoLeaves());
+        require(proofElements_.length > 0, NoProofElements());
+        require(startingIndex_ + hashes_.length <= proofElements_[0], InvalidProof());
+
+        uint256 count_ = hashes_.length;
+        uint256[] memory treeIndices_ = new uint256[](count_);
path: src/libraries/SequentialMerkleProofs.sol
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Leaf-count bounds should be validated up-front**

`_getRoot` calls `_getBalancedLeafCount(proofElements_[0])`.  
If the tree’s leaf count exceeds `2**64-1`, `_roundUpToPowerOf2` reverts with `InvalidRoundUpToPowerOf2Input`, which is semantically unrelated to the proof itself and may surprise callers.

Add an explicit guard so the revert reason is clear and inexpensive to catch:

```diff
+        require(
+            proofElements_[0] <= type(uint64).max,
+            InvalidProof() // or introduce a dedicated error
+        );
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    function _getRoot(
        uint256 startingIndex_,
        uint256[] memory hashes_,
        uint256[] calldata proofElements_
    ) internal pure returns (uint256 root_) {
        require(hashes_.length > 0, NoLeaves());
        require(proofElements_.length > 0, NoProofElements());
        require(startingIndex_ + hashes_.length <= proofElements_[0], InvalidProof());

+       // Ensure the leaf count fits in 64 bits to avoid unexpected errors
+       require(
+           proofElements_[0] <= type(uint64).max,
+           InvalidProof()
+       );

        uint256 count_ = hashes_.length;
        uint256[] memory treeIndices_ = new uint256[](count_);
        // ...
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-02T18:52:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,291 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    /// @notice The leaf prefix used to hash to a leaf.
+    bytes internal constant LEAF_PREFIX = bytes("leaf|");
+
+    /// @notice The node prefix used to hash to a node.
+    bytes internal constant NODE_PREFIX = bytes("node|");
+
+    /// @notice The root prefix used to hash to a root.
+    bytes internal constant ROOT_PREFIX = bytes("root|");
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when the no leaves are passed.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _roundUpToPowerOf2 is greater than type(uint64).max.
+    error InvalidRoundUpToPowerOf2Input();
+
+    /// @notice Thrown when the input to _bitCount64 is greater than type(uint64).max.
+    error InvalidBitCount64Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /// @notice Thrown when no proof elements are provided.
+    error NoProofElements();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        uint256 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
+
+    /* ============ Helper Functions ============ */
+
+    /**
+     * @notice Counts number of set bits (1's) in 64-bit unsigned integer.
+     * @dev    See https://en.wikipedia.org/wiki/Hamming_weight implementation `popcount64b`.
+     * @param  n_        The number to count the set bits of.
+     * @return bitCount_ The number of set bits in `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _bitCount64(uint256 n_) internal pure returns (uint256 bitCount_) {
+        require(n_ <= type(uint64).max, InvalidBitCount64Input());
+
+        n_ -= (n_ >> 1) & 0x5555555555555555;
+        n_ = (n_ & 0x3333333333333333) + ((n_ >> 2) & 0x3333333333333333);
+        n_ = (n_ + (n_ >> 4)) & 0x0f0f0f0f0f0f0f0f;
+        n_ += n_ >> 8;
+        n_ += n_ >> 16;
+        n_ += n_ >> 32;
+
+        return n_ & 0x7f;
+    }
+
+    /**
+     * @notice Rounds a 64-bit unsigned integer up to the nearest power of 2.
+     * @param  n_        The number to round up to the nearest power of 2.
+     * @return powerOf2_ The nearest power of 2 to `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _roundUpToPowerOf2(uint256 n_) internal pure returns (uint256 powerOf2_) {
+        require(n_ <= type(uint64).max, InvalidRoundUpToPowerOf2Input());
+
+        if (_bitCount64(n_) == 1) return n_;
+
+        n_ |= n_ >> 1;
+        n_ |= n_ >> 2;
+        n_ |= n_ >> 4;
+        n_ |= n_ >> 8;
+        n_ |= n_ >> 16;
+        n_ |= n_ >> 32;
+
+        return n_ + 1;
+    }
+
+    /**
+     * @notice Gets the balanced leaf count for a sequential merkle proof.
+     * @param  leafCount_         The number of leaves in the merkle tree.
+     * @return balancedLeafCount_ The balanced leaf count.
+     */
+    function _getBalancedLeafCount(uint256 leafCount_) internal pure returns (uint256 balancedLeafCount_) {
+        require(leafCount_ != 0, NoLeaves());
+
+        return leafCount_ == 1 ? 2 : _roundUpToPowerOf2(leafCount_);
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  hashes_        The leaf hashes (in reverse order) to prove sequential existence from the starting index.
+     * @param  proofElements_         The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     */
+    function _getRoot(
+        uint256 startingIndex_,
+        uint256[] memory hashes_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        require(hashes_.length > 0, NoLeaves());
+        require(proofElements_.length > 0, NoProofElements());
+        require(startingIndex_ + hashes_.length <= proofElements_[0], InvalidProof());
+
+        uint256 count_ = hashes_.length;
+        uint256[] memory treeIndices_ = new uint256[](count_);
path: src/libraries/SequentialMerkleProofs.sol
user: login: deluca-mike
body: Fair call out, but I prefer the way it currently is because:
- the check is only performed once, rather than twice, which would be a waste of gas
- the `InvalidRoundUpToPowerOf2Input` error emitted by `_roundUpToPowerOf2` make sit more clear what the issue is , as opposed to  `InvalidProof`.
created_at: 2025-05-05T14:38:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,291 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title  Library for verifying sequential merkle proofs.
+ * @notice A sequential merkle proof is a proof that leaves appear sequentially in a merkle tree.
+ */
+library SequentialMerkleProofs {
+    /* ============ Constants ============ */
+
+    /// @notice The leaf prefix used to hash to a leaf.
+    bytes internal constant LEAF_PREFIX = bytes("leaf|");
+
+    /// @notice The node prefix used to hash to a node.
+    bytes internal constant NODE_PREFIX = bytes("node|");
+
+    /// @notice The root prefix used to hash to a root.
+    bytes internal constant ROOT_PREFIX = bytes("root|");
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Thrown when the no leaves are passed.
+    error NoLeaves();
+
+    /// @notice Thrown when the input to _roundUpToPowerOf2 is greater than type(uint64).max.
+    error InvalidRoundUpToPowerOf2Input();
+
+    /// @notice Thrown when the input to _bitCount64 is greater than type(uint64).max.
+    error InvalidBitCount64Input();
+
+    /// @notice Thrown when the proof is invalid.
+    error InvalidProof();
+
+    /// @notice Thrown when no proof elements are provided.
+    error NoProofElements();
+
+    /* ============ Main Functions ============ */
+
+    /**
+     * @notice Verifies a sequential merkle proof.
+     * @param  root_          The root of the merkle tree.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function verify(
+        uint256 root_,
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure {
+        require(getRoot(startingIndex_, leaves_, proofElements_) == root_, InvalidProof());
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  leaves_        The leaves to prove sequential existence from the starting index.
+     * @param  proofElements_ The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     * @dev    Can also revert with an array out-of-bounds access panic.
+     */
+    function getRoot(
+        uint256 startingIndex_,
+        bytes[] calldata leaves_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        return _getRoot(startingIndex_, _getReversedLeafNodesFromLeaves(leaves_), proofElements_);
+    }
+
+    /* ============ Helper Functions ============ */
+
+    /**
+     * @notice Counts number of set bits (1's) in 64-bit unsigned integer.
+     * @dev    See https://en.wikipedia.org/wiki/Hamming_weight implementation `popcount64b`.
+     * @param  n_        The number to count the set bits of.
+     * @return bitCount_ The number of set bits in `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _bitCount64(uint256 n_) internal pure returns (uint256 bitCount_) {
+        require(n_ <= type(uint64).max, InvalidBitCount64Input());
+
+        n_ -= (n_ >> 1) & 0x5555555555555555;
+        n_ = (n_ & 0x3333333333333333) + ((n_ >> 2) & 0x3333333333333333);
+        n_ = (n_ + (n_ >> 4)) & 0x0f0f0f0f0f0f0f0f;
+        n_ += n_ >> 8;
+        n_ += n_ >> 16;
+        n_ += n_ >> 32;
+
+        return n_ & 0x7f;
+    }
+
+    /**
+     * @notice Rounds a 64-bit unsigned integer up to the nearest power of 2.
+     * @param  n_        The number to round up to the nearest power of 2.
+     * @return powerOf2_ The nearest power of 2 to `n_`.
+     * @dev    Literals are inlined as they are very specific to this algorithm/function, and actually improve
+     *         readability, given their patterns.
+     */
+    function _roundUpToPowerOf2(uint256 n_) internal pure returns (uint256 powerOf2_) {
+        require(n_ <= type(uint64).max, InvalidRoundUpToPowerOf2Input());
+
+        if (_bitCount64(n_) == 1) return n_;
+
+        n_ |= n_ >> 1;
+        n_ |= n_ >> 2;
+        n_ |= n_ >> 4;
+        n_ |= n_ >> 8;
+        n_ |= n_ >> 16;
+        n_ |= n_ >> 32;
+
+        return n_ + 1;
+    }
+
+    /**
+     * @notice Gets the balanced leaf count for a sequential merkle proof.
+     * @param  leafCount_         The number of leaves in the merkle tree.
+     * @return balancedLeafCount_ The balanced leaf count.
+     */
+    function _getBalancedLeafCount(uint256 leafCount_) internal pure returns (uint256 balancedLeafCount_) {
+        require(leafCount_ != 0, NoLeaves());
+
+        return leafCount_ == 1 ? 2 : _roundUpToPowerOf2(leafCount_);
+    }
+
+    /**
+     * @notice Gets the root of a sequential merkle proof.
+     * @param  startingIndex_ The index of the first leaf provided.
+     * @param  hashes_        The leaf hashes (in reverse order) to prove sequential existence from the starting index.
+     * @param  proofElements_         The merkle proof data.
+     * @dev    proofElements_[0] is the total number of leaves in the merkle tree, the rest are the decommitments.
+     */
+    function _getRoot(
+        uint256 startingIndex_,
+        uint256[] memory hashes_,
+        uint256[] calldata proofElements_
+    ) internal pure returns (uint256 root_) {
+        require(hashes_.length > 0, NoLeaves());
+        require(proofElements_.length > 0, NoProofElements());
+        require(startingIndex_ + hashes_.length <= proofElements_[0], InvalidProof());
+
+        uint256 count_ = hashes_.length;
+        uint256[] memory treeIndices_ = new uint256[](count_);
path: src/libraries/SequentialMerkleProofs.sol
user: login: coderabbitai[bot]
body: @deluca-mike I understand your reasoning and agree with your approach. Keeping the validation in `_roundUpToPowerOf2` rather than adding a duplicate check is more gas-efficient, and the specific `InvalidRoundUpToPowerOf2Input` error provides clearer information about what went wrong compared to a generic `InvalidProof` error. Thanks for explaining your design rationale.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike I understand your reasoning and agree with your approach. Keeping the validation in `_roundUpToPowerOf2` rather than adding a duplicate check is more gas-efficient, and the specific `InvalidRoundUpToPowerOf2Input` error provides clearer information about what went wrong compared to a generic `InvalidProof` error. Thanks for explaining your design rationale.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#55
File: src/libraries/SequentialMerkleProofs.sol:136-146
Timestamp: 2025-05-05T14:38:28.054Z
Learning: For the SequentialMerkleProofs library, validation for overflow in bitwise operations is handled directly in the helper functions (e.g., _roundUpToPowerOf2) rather than adding redundant checks in caller functions to optimize gas usage and provide more specific error messages.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-05T14:38:29Z
author_association: NONE
diff_hunk: @@ -0,0 +1,886 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Unreleased Solidity version will break compilation**

`pragma solidity 0.8.28;` targets a compiler version that (at the time of writing) has not been released.  
`forge test` (or any other toolchain) will fail unless the local user installs a custom/nightly build.

```diff
-pragma solidity 0.8.28;
+// Use a released compiler or a version range
+pragma solidity ^0.8.20; // adjust upper bound if you need ≥0.8.25 features
```

If the intent is to pin to a nightly/dev build, please add a comment explaining why so other contributors don’t downgrade it by mistake.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// Use a released compiler or a version range
pragma solidity ^0.8.20; // adjust upper bound if you need ≥0.8.25 features
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-05T18:27:45Z
author_association: NONE
diff_hunk: @@ -0,0 +1,886 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
path: test/unit/SequentialMerkleProofs.t.sol
user: login: deluca-mike
body: False positive. `0.8.28` has been released for a while.
created_at: 2025-05-06T16:54:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,886 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🌐 Web query:

```web
Has Solidity 0.8.28 been released? When was it released?
```

💡 Result:

Yes, Solidity version 0.8.28 has been released. It was officially released on October 9, 2024. This release brought full support for transient storage state variables of value types, improvements that speed up compilation via the Intermediate Representation (IR), significantly lower memory usage, several bugfixes, and some enhancements to the compiler and build system[1][2][3][6][9].

Key highlights of Solidity 0.8.28 include:  
- Full support for transient storage state variables of value types.  
- Generation of JSON representations of Yul Abstract Syntax Trees (ASTs) only on demand to reduce memory usage.  
- The ability to request bytecode or IR for a subset of contracts without triggering unnecessary code generation for others.  
- Several bugfixes related to SMTChecker and Yul AST source location handling.  
- Removal of the `USE_LD_GOLD` option and defaulting to the compiler's default linker.[1][2][6]

This version followed 0.8.27 and preceded 0.8.29, which was released later in March 2025[2][3][5].  

In summary, Solidity 0.8.28 is available since October 9, 2024, and focuses on improving transient storage support and compiler efficiency among other fixes and features.

Citations:

- 1: https://soliditylang.org/blog/2024/10/09/solidity-0.8.28-release-announcement/
- 2: https://github.com/ethereum/solidity/releases
- 3: https://soliditylang.org/blog/category/releases/
- 4: https://outposts.io/article/solidity-0828-release-announcement-3b511420-1fb8-4cda-9366-1848e83e86bd
- 5: https://en.wikipedia.org/wiki/Solidity
- 6: https://forum.soliditylang.org/t/solidity-v0-8-28-is-out/3120
- 7: https://docs.soliditylang.org
- 8: https://sourceforge.net/projects/solidity.mirror/files/v0.8.28/
- 9: https://github.com/ethereum/solidity/blob/develop/Changelog.md

---



</details>

@deluca-mike You're absolutely correct. Solidity 0.8.28 was released on October 9, 2024 (about 7 months ago). The automated check incorrectly flagged this as an unreleased version. Sorry about the false positive.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🌐 Web query:

```web
Has Solidity 0.8.28 been released? When was it released?
```

💡 Result:

Yes, Solidity version 0.8.28 has been released. It was officially released on October 9, 2024. This release brought full support for transient storage state variables of value types, improvements that speed up compilation via the Intermediate Representation (IR), significantly lower memory usage, several bugfixes, and some enhancements to the compiler and build system[1][2][3][6][9].

Key highlights of Solidity 0.8.28 include:  
- Full support for transient storage state variables of value types.  
- Generation of JSON representations of Yul Abstract Syntax Trees (ASTs) only on demand to reduce memory usage.  
- The ability to request bytecode or IR for a subset of contracts without triggering unnecessary code generation for others.  
- Several bugfixes related to SMTChecker and Yul AST source location handling.  
- Removal of the `USE_LD_GOLD` option and defaulting to the compiler's default linker.[1][2][6]

This version followed 0.8.27 and preceded 0.8.29, which was released later in March 2025[2][3][5].  

In summary, Solidity 0.8.28 is available since October 9, 2024, and focuses on improving transient storage support and compiler efficiency among other fixes and features.

Citations:

- 1: https://soliditylang.org/blog/2024/10/09/solidity-0.8.28-release-announcement/
- 2: https://github.com/ethereum/solidity/releases
- 3: https://soliditylang.org/blog/category/releases/
- 4: https://outposts.io/article/solidity-0828-release-announcement-3b511420-1fb8-4cda-9366-1848e83e86bd
- 5: https://en.wikipedia.org/wiki/Solidity
- 6: https://forum.soliditylang.org/t/solidity-v0-8-28-is-out/3120
- 7: https://docs.soliditylang.org
- 8: https://sourceforge.net/projects/solidity.mirror/files/v0.8.28/
- 9: https://github.com/ethereum/solidity/blob/develop/Changelog.md

---



</details>

@deluca-mike You're absolutely correct. Solidity 0.8.28 was released on October 9, 2024 (about 7 months ago). The automated check incorrectly flagged this as an unreleased version. Sorry about the false positive.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T16:54:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    function test_getRoot_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    /* ============ bitCount32 ============ */
+
+    function test_bitCount32_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max) + 1);
+    }
+
+    function test_bitCount32() external view {
+        assertEq(_sequentialMerkleProofs.__bitCount32(0), 0);
+        assertEq(_sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max)), 32);
+
+        for (uint256 i_ = 0; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__bitCount32(1 << i_), 1); // One bit set
+            assertEq(_sequentialMerkleProofs.__bitCount32((1 << i_) - 1), i_); // i_ bits set
+        }
+    }
+
+    /* ============ roundUpToPowerOf2 ============ */
+
+    function test_roundUpToPowerOf2_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max) + 1);
+    }
+
+    function test_roundUpToPowerOf2() external view {
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(0), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(2), 2);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(3), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(4), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(5), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(6), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(7), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getBalancedLeafCount ============ */
+
+    function test_getBalancedLeafCount_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max) + 1);
+    }
+
+    function test_getBalancedLeafCount() external view {
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(0), 0);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(2), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(3), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(4), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(5), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(6), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(7), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getReversedLeafNodesFromLeaves ============ */
+
+    function test_getReversedLeafNodesFromLeaves() external view {
+        bytes[] memory leaves_ = new bytes[](4);
+
+        leaves_[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        leaves_[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        leaves_[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        leaves_[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        bytes32[] memory reversedLeafNodes_ = _sequentialMerkleProofs.__getReversedLeafNodesFromLeaves(leaves_);
+
+        assertEq(reversedLeafNodes_[0], 0x8613e086c6c42ab3b7027723e29bf61497f3629e0fb0c1b0453c93f053e22a18);
+        assertEq(reversedLeafNodes_[1], 0x09a458689dbd19291ce83007725819f0c69504e282d7669b7d08576162bd7f52);
+        assertEq(reversedLeafNodes_[2], 0x569dab69f958322840fb959938fee8dbc2bde84f55584935a8230303db6aaebd);
+        assertEq(reversedLeafNodes_[3], 0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b);
+    }
+
+    /* ============ helpers ============ */
+
+    function _buildBalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 2,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](2),
+                root: 0xeeef536868dc2c030bec2d3602cc13fbe660bd5d63deca6a0a4dfd201eb941c0
+            })
+        );
+
+        _balancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _balancedSamples[0].proofElements[0] = bytes32(_balancedSamples[0].leafCount);
+        _balancedSamples[0].proofElements[1] = bytes32(
+            0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b
+        );
+
+        /* ============ Sample 2 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 1,
+                leafCount: 8,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](4),
+                root: 0x00f8c0ad3c60c727ededce5717c8baa64047b5c3f29e409085df14dc3bfda1a7
+            })
+        );
+
+        _balancedSamples[1].leaves[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        _balancedSamples[1].leaves[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        _balancedSamples[1].leaves[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        _balancedSamples[1].leaves[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        _balancedSamples[1].proofElements[0] = bytes32(_balancedSamples[1].leafCount);
+        _balancedSamples[1].proofElements[1] = 0xb7f40118daf8bbb92b1759e810c6eb4b0b92e04d1b7e4be48a147721ea457d87;
+        _balancedSamples[1].proofElements[2] = 0x06331c9b61be683c1819b2cd20c83f315499477f6253ae8d8bb02cbc6bd93c9f;
+        _balancedSamples[1].proofElements[3] = 0xc3d064e0f1ace0f92e6c822d5346de5330fb23a6c31a0c84a80d9d5c8543cc0e;
+
+        /* ============ Sample 3 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 16,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](5),
+                root: 0x31338b156e26447f0a3a965981b0be87957bf5606b44e0dcdc99eb4646048942
+            })
+        );
+
+        _balancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _balancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _balancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _balancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _balancedSamples[2].proofElements[0] = bytes32(_balancedSamples[2].leafCount);
+        _balancedSamples[2].proofElements[1] = 0x194f8cb59bc03a6e9dfcc3566bed0a85a069f50de609f51af6c74dee673450d4;
+        _balancedSamples[2].proofElements[2] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _balancedSamples[2].proofElements[3] = 0x9d8799ecccbca75f60304876c8426007565302fdb72449fe980917d7847d43e7;
+        _balancedSamples[2].proofElements[4] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+    }
+
+    function _buildUnbalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 1,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](1),
+                root: 0x5b833bdf4f55e39d1838653841d4a2c651a71b5626b7936e1bedb5212cae96e3
+            })
+        );
+
+        _unbalancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _unbalancedSamples[0].proofElements[0] = bytes32(_unbalancedSamples[0].leafCount);
+
+        /* ============ Sample 2 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 4,
+                leafCount: 7,
+                leaves: new bytes[](2),
+                proofElements: new bytes32[](3),
+                root: 0x38631dd8b5081555ec3c51cc8db7918ee90158fa33a70674c1399234d23908b2
+            })
+        );
+
+        _unbalancedSamples[1].leaves[0] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+        _unbalancedSamples[1].leaves[1] = bytes(hex"51b7ae2bab96bd3fbb3b26e1efefb0b9b6a60054ed7ffcfa700374d58f315a31");
+
+        _unbalancedSamples[1].proofElements[0] = bytes32(_unbalancedSamples[1].leafCount);
+        _unbalancedSamples[1].proofElements[1] = 0x6b379ab3dd3b0b8df76cbe09e2a04f21ebf601f2932d3505ecfb89417f3de836;
+        _unbalancedSamples[1].proofElements[2] = 0x52bd35868bccc1b1f4c3ac67dd7e3b3db4a24f60436411162d633e6d1118de89;
+
+        /* ============ Sample 3 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 13,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](3),
+                root: 0xf92d4ca528834b0350cecd9307bec2dd97d0a6bbb58b077ab51cdad36fc5c087
+            })
+        );
+
+        _unbalancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _unbalancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _unbalancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _unbalancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _unbalancedSamples[2].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[2].proofElements[1] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _unbalancedSamples[2].proofElements[2] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+
+        /* ============ Sample 4 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 200,
+                leafCount: 324,
+                leaves: new bytes[](22),
+                proofElements: new bytes32[](7),
+                root: 0x8f28d0f19d1805a3b539bcc2bb0e627ded4bf5b873bebc9199ed179a30ca312c
+            })
+        );
+
+        _unbalancedSamples[3].leaves[0] = bytes(hex"fe0403dd500c862ddbbb4736c071a575d9fd43fbdeea602833234e9cd198bb3f");
+        _unbalancedSamples[3].leaves[1] = bytes(hex"5129d77889ad52ef899ba3d85e01a6df5c230804ccc586faa7e8376b89aa0600");
+        _unbalancedSamples[3].leaves[2] = bytes(hex"f1d6a296210c112eda6ca002273abc7d21b933cf99ca0c8292e344ba2d62f750");
+        _unbalancedSamples[3].leaves[3] = bytes(hex"907f141e1702157a9ca13881b0ecb0cb10be9b588bd6499479496965ce933225");
+        _unbalancedSamples[3].leaves[4] = bytes(hex"c77ccc18e17ec5d50bbbf775f564debfa1f9da8031be2fc7ce618cea8a625226");
+        _unbalancedSamples[3].leaves[5] = bytes(hex"aad8eb018edb0690bbdcebe48db820a6a13e3f8102bb0e5535fdfed79eaa5c39");
+        _unbalancedSamples[3].leaves[6] = bytes(hex"374346003edf10ea2da568e8e973857ec4291bf10a8bf81612b36efaf6e93c23");
+        _unbalancedSamples[3].leaves[7] = bytes(hex"45a3c2e32a3c8ea5e34562806f8279e85cfad69f6015588b599dc2e774d50cee");
+        _unbalancedSamples[3].leaves[8] = bytes(hex"9322c7cbfb342280d72d4eba15c0d9711e3f730072375bc4b0cf538970014e7a");
+        _unbalancedSamples[3].leaves[9] = bytes(hex"b3017e1b1874f68c4508c0b147a4f2b96dd166274c81bb4a86082d391efad6fa");
+        _unbalancedSamples[3].leaves[10] = bytes(hex"76d7f81c6813544dc594db8b5f3d6cef292e91d02a50ad556e6bcddc1eb59f8c");
+        _unbalancedSamples[3].leaves[11] = bytes(hex"320cfee2fb238c49255d102213b395963c2a3ebd7a8ee424e8df02b01361e484");
+        _unbalancedSamples[3].leaves[12] = bytes(hex"697fa39a1e23106c54eeecf38d213bfe2fcf896f55b790815085241b7e7e7361");
+        _unbalancedSamples[3].leaves[13] = bytes(hex"510fde4a04ecbe4bf94a514ab836074270959d2b96492a56909706de0b0248be");
+        _unbalancedSamples[3].leaves[14] = bytes(hex"5fa7828c8c661f4243c97527d637bf318e5e801dcf3b488f0f302622abdf61e9");
+        _unbalancedSamples[3].leaves[15] = bytes(hex"6f7fafb63e26bafeb5fd21a76615d0f2b45e6684324e294f10078661c00b4026");
+        _unbalancedSamples[3].leaves[16] = bytes(hex"e80b189bd7a1e5d0a04d9ba778d11f1bbffec4e276f502780a97fc7006715ac5");
+        _unbalancedSamples[3].leaves[17] = bytes(hex"25e66cd82e8fa7b6072d7759d2b49d0979bad20b018c730f4d1fcac7a6959bd4");
+        _unbalancedSamples[3].leaves[18] = bytes(hex"0695e86d43e2c5097acd182042d09cda438fc1f773335ce0f190dbed668ff508");
+        _unbalancedSamples[3].leaves[19] = bytes(hex"783eb995b1a99bb4181dad1c386dff06c5c02469a4893ca8c0a5b5fd82f7747a");
+        _unbalancedSamples[3].leaves[20] = bytes(hex"44fb0e55a04f4d5accd7a9e00fd5bbbf8926083ef169c0711f503ae16b589c20");
+        _unbalancedSamples[3].leaves[21] = bytes(hex"2815ef424dd0613d69f70546821ebddf2fe1b7452510cd21d25f3e438863e8a3");
+
+        _unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[3].proofElements[1] = 0x55f9b393403d39fdf3b35fe8f394e13a272509df05d5562e5da6937c11bb1214;
+        _unbalancedSamples[3].proofElements[2] = 0xa4c0ad363d17cb84be9e3f757278de2b26843e918e48e33e1b00438764fefa2b;
+        _unbalancedSamples[3].proofElements[3] = 0xafee522c5ba27318361dd426e355fa02e5821fbc8b2ba52dea3b36885d76ec94;
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Incorrect leaf-count element for sample 4**

`proofElements[0]` should encode **sample 4**’s `leafCount`, but it mistakenly takes the value from sample 3, making the proof invalid and the test meaningless once the loop-index bug is fixed.

```diff
-_unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+_unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[3].leafCount);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        _unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[3].leafCount);
        _unbalancedSamples[3].proofElements[1] = 0x55f9b393403d39fdf3b35fe8f394e13a272509df05d5562e5da6937c11bb1214;
        _unbalancedSamples[3].proofElements[2] = 0xa4c0ad363d17cb84be9f757278de2b26843e918e48e33e1b00438764fefa2b;
        _unbalancedSamples[3].proofElements[3] = 0xafee522c5ba27318361dd426e355fa02e5821fbc8b2ba52dea3b36885d76ec94;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit bece9db
created_at: 2025-05-08T18:35:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**getRoot – balanced sample loop suffers from identical issue**

The loop again hard-codes index 0 instead of using the iterator:

```diff
-            Sample storage sample_ = _balancedSamples[0];
+            Sample storage sample_ = _balancedSamples[index_];
```

This leaves six balanced roots un-checked.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
-            Sample storage sample_ = _balancedSamples[0];
+            Sample storage sample_ = _balancedSamples[index_];

            assertEq(
                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
                sample_.root
            );
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit bece9db
created_at: 2025-05-08T18:35:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Same indexing bug for unbalanced samples**

The unbalanced-sample loop is affected by the same copy-&-paste error:

```diff
-            Sample storage sample_ = _unbalancedSamples[0];
+            Sample storage sample_ = _unbalancedSamples[index_];
```

Currently only the first unbalanced sample is tested.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
            Sample storage sample_ = _unbalancedSamples[index_];
            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit bece9db
created_at: 2025-05-08T18:35:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    function test_getRoot_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**getRoot – unbalanced sample loop indexing bug**

Same correction needed here:

```diff
-            Sample storage sample_ = _unbalancedSamples[0];
+            Sample storage sample_ = _unbalancedSamples[index_];
```  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
-            Sample storage sample_ = _unbalancedSamples[0];
+            Sample storage sample_ = _unbalancedSamples[index_];

            assertEq(
                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
                sample_.root
            );
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit bece9db
created_at: 2025-05-08T18:35:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Indexing bug – only first balanced sample is ever exercised**

`Sample storage sample_ = _balancedSamples[0];` ignores the `index_` loop variable, so the test repeatedly verifies the first sample and silently skips the rest. The same mistake appears in several other “for‐all‐samples” tests.

```diff
-            Sample storage sample_ = _balancedSamples[0];
+            Sample storage sample_ = _balancedSamples[index_];
```

Without this fix, coverage for the majority of balanced samples is lost and future regressions could slip through unnoticed.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
            Sample storage sample_ = _balancedSamples[index_];
            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit bece9db
created_at: 2025-05-08T18:35:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    function test_getRoot_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    /* ============ bitCount32 ============ */
+
+    function test_bitCount32_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max) + 1);
+    }
+
+    function test_bitCount32() external view {
+        assertEq(_sequentialMerkleProofs.__bitCount32(0), 0);
+        assertEq(_sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max)), 32);
+
+        for (uint256 i_ = 0; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__bitCount32(1 << i_), 1); // One bit set
+            assertEq(_sequentialMerkleProofs.__bitCount32((1 << i_) - 1), i_); // i_ bits set
+        }
+    }
+
+    /* ============ roundUpToPowerOf2 ============ */
+
+    function test_roundUpToPowerOf2_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max) + 1);
+    }
+
+    function test_roundUpToPowerOf2() external view {
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(0), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(2), 2);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(3), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(4), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(5), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(6), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(7), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getBalancedLeafCount ============ */
+
+    function test_getBalancedLeafCount_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max) + 1);
+    }
+
+    function test_getBalancedLeafCount() external view {
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(0), 0);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(2), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(3), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(4), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(5), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(6), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(7), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getReversedLeafNodesFromLeaves ============ */
+
+    function test_getReversedLeafNodesFromLeaves() external view {
+        bytes[] memory leaves_ = new bytes[](4);
+
+        leaves_[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        leaves_[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        leaves_[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        leaves_[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        bytes32[] memory reversedLeafNodes_ = _sequentialMerkleProofs.__getReversedLeafNodesFromLeaves(leaves_);
+
+        assertEq(reversedLeafNodes_[0], 0x8613e086c6c42ab3b7027723e29bf61497f3629e0fb0c1b0453c93f053e22a18);
+        assertEq(reversedLeafNodes_[1], 0x09a458689dbd19291ce83007725819f0c69504e282d7669b7d08576162bd7f52);
+        assertEq(reversedLeafNodes_[2], 0x569dab69f958322840fb959938fee8dbc2bde84f55584935a8230303db6aaebd);
+        assertEq(reversedLeafNodes_[3], 0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b);
+    }
+
+    /* ============ helpers ============ */
+
+    function _buildBalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 2,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](2),
+                root: 0xeeef536868dc2c030bec2d3602cc13fbe660bd5d63deca6a0a4dfd201eb941c0
+            })
+        );
+
+        _balancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _balancedSamples[0].proofElements[0] = bytes32(_balancedSamples[0].leafCount);
+        _balancedSamples[0].proofElements[1] = bytes32(
+            0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b
+        );
+
+        /* ============ Sample 2 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 1,
+                leafCount: 8,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](4),
+                root: 0x00f8c0ad3c60c727ededce5717c8baa64047b5c3f29e409085df14dc3bfda1a7
+            })
+        );
+
+        _balancedSamples[1].leaves[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        _balancedSamples[1].leaves[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        _balancedSamples[1].leaves[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        _balancedSamples[1].leaves[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        _balancedSamples[1].proofElements[0] = bytes32(_balancedSamples[1].leafCount);
+        _balancedSamples[1].proofElements[1] = 0xb7f40118daf8bbb92b1759e810c6eb4b0b92e04d1b7e4be48a147721ea457d87;
+        _balancedSamples[1].proofElements[2] = 0x06331c9b61be683c1819b2cd20c83f315499477f6253ae8d8bb02cbc6bd93c9f;
+        _balancedSamples[1].proofElements[3] = 0xc3d064e0f1ace0f92e6c822d5346de5330fb23a6c31a0c84a80d9d5c8543cc0e;
+
+        /* ============ Sample 3 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 16,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](5),
+                root: 0x31338b156e26447f0a3a965981b0be87957bf5606b44e0dcdc99eb4646048942
+            })
+        );
+
+        _balancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _balancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _balancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _balancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _balancedSamples[2].proofElements[0] = bytes32(_balancedSamples[2].leafCount);
+        _balancedSamples[2].proofElements[1] = 0x194f8cb59bc03a6e9dfcc3566bed0a85a069f50de609f51af6c74dee673450d4;
+        _balancedSamples[2].proofElements[2] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _balancedSamples[2].proofElements[3] = 0x9d8799ecccbca75f60304876c8426007565302fdb72449fe980917d7847d43e7;
+        _balancedSamples[2].proofElements[4] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+    }
+
+    function _buildUnbalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 1,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](1),
+                root: 0x5b833bdf4f55e39d1838653841d4a2c651a71b5626b7936e1bedb5212cae96e3
+            })
+        );
+
+        _unbalancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _unbalancedSamples[0].proofElements[0] = bytes32(_unbalancedSamples[0].leafCount);
+
+        /* ============ Sample 2 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 4,
+                leafCount: 7,
+                leaves: new bytes[](2),
+                proofElements: new bytes32[](3),
+                root: 0x38631dd8b5081555ec3c51cc8db7918ee90158fa33a70674c1399234d23908b2
+            })
+        );
+
+        _unbalancedSamples[1].leaves[0] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+        _unbalancedSamples[1].leaves[1] = bytes(hex"51b7ae2bab96bd3fbb3b26e1efefb0b9b6a60054ed7ffcfa700374d58f315a31");
+
+        _unbalancedSamples[1].proofElements[0] = bytes32(_unbalancedSamples[1].leafCount);
+        _unbalancedSamples[1].proofElements[1] = 0x6b379ab3dd3b0b8df76cbe09e2a04f21ebf601f2932d3505ecfb89417f3de836;
+        _unbalancedSamples[1].proofElements[2] = 0x52bd35868bccc1b1f4c3ac67dd7e3b3db4a24f60436411162d633e6d1118de89;
+
+        /* ============ Sample 3 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 13,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](3),
+                root: 0xf92d4ca528834b0350cecd9307bec2dd97d0a6bbb58b077ab51cdad36fc5c087
+            })
+        );
+
+        _unbalancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _unbalancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _unbalancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _unbalancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _unbalancedSamples[2].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[2].proofElements[1] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _unbalancedSamples[2].proofElements[2] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+
+        /* ============ Sample 4 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 200,
+                leafCount: 324,
+                leaves: new bytes[](22),
+                proofElements: new bytes32[](7),
+                root: 0x8f28d0f19d1805a3b539bcc2bb0e627ded4bf5b873bebc9199ed179a30ca312c
+            })
+        );
+
+        _unbalancedSamples[3].leaves[0] = bytes(hex"fe0403dd500c862ddbbb4736c071a575d9fd43fbdeea602833234e9cd198bb3f");
+        _unbalancedSamples[3].leaves[1] = bytes(hex"5129d77889ad52ef899ba3d85e01a6df5c230804ccc586faa7e8376b89aa0600");
+        _unbalancedSamples[3].leaves[2] = bytes(hex"f1d6a296210c112eda6ca002273abc7d21b933cf99ca0c8292e344ba2d62f750");
+        _unbalancedSamples[3].leaves[3] = bytes(hex"907f141e1702157a9ca13881b0ecb0cb10be9b588bd6499479496965ce933225");
+        _unbalancedSamples[3].leaves[4] = bytes(hex"c77ccc18e17ec5d50bbbf775f564debfa1f9da8031be2fc7ce618cea8a625226");
+        _unbalancedSamples[3].leaves[5] = bytes(hex"aad8eb018edb0690bbdcebe48db820a6a13e3f8102bb0e5535fdfed79eaa5c39");
+        _unbalancedSamples[3].leaves[6] = bytes(hex"374346003edf10ea2da568e8e973857ec4291bf10a8bf81612b36efaf6e93c23");
+        _unbalancedSamples[3].leaves[7] = bytes(hex"45a3c2e32a3c8ea5e34562806f8279e85cfad69f6015588b599dc2e774d50cee");
+        _unbalancedSamples[3].leaves[8] = bytes(hex"9322c7cbfb342280d72d4eba15c0d9711e3f730072375bc4b0cf538970014e7a");
+        _unbalancedSamples[3].leaves[9] = bytes(hex"b3017e1b1874f68c4508c0b147a4f2b96dd166274c81bb4a86082d391efad6fa");
+        _unbalancedSamples[3].leaves[10] = bytes(hex"76d7f81c6813544dc594db8b5f3d6cef292e91d02a50ad556e6bcddc1eb59f8c");
+        _unbalancedSamples[3].leaves[11] = bytes(hex"320cfee2fb238c49255d102213b395963c2a3ebd7a8ee424e8df02b01361e484");
+        _unbalancedSamples[3].leaves[12] = bytes(hex"697fa39a1e23106c54eeecf38d213bfe2fcf896f55b790815085241b7e7e7361");
+        _unbalancedSamples[3].leaves[13] = bytes(hex"510fde4a04ecbe4bf94a514ab836074270959d2b96492a56909706de0b0248be");
+        _unbalancedSamples[3].leaves[14] = bytes(hex"5fa7828c8c661f4243c97527d637bf318e5e801dcf3b488f0f302622abdf61e9");
+        _unbalancedSamples[3].leaves[15] = bytes(hex"6f7fafb63e26bafeb5fd21a76615d0f2b45e6684324e294f10078661c00b4026");
+        _unbalancedSamples[3].leaves[16] = bytes(hex"e80b189bd7a1e5d0a04d9ba778d11f1bbffec4e276f502780a97fc7006715ac5");
+        _unbalancedSamples[3].leaves[17] = bytes(hex"25e66cd82e8fa7b6072d7759d2b49d0979bad20b018c730f4d1fcac7a6959bd4");
+        _unbalancedSamples[3].leaves[18] = bytes(hex"0695e86d43e2c5097acd182042d09cda438fc1f773335ce0f190dbed668ff508");
+        _unbalancedSamples[3].leaves[19] = bytes(hex"783eb995b1a99bb4181dad1c386dff06c5c02469a4893ca8c0a5b5fd82f7747a");
+        _unbalancedSamples[3].leaves[20] = bytes(hex"44fb0e55a04f4d5accd7a9e00fd5bbbf8926083ef169c0711f503ae16b589c20");
+        _unbalancedSamples[3].leaves[21] = bytes(hex"2815ef424dd0613d69f70546821ebddf2fe1b7452510cd21d25f3e438863e8a3");
+
+        _unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[3].proofElements[1] = 0x55f9b393403d39fdf3b35fe8f394e13a272509df05d5562e5da6937c11bb1214;
+        _unbalancedSamples[3].proofElements[2] = 0xa4c0ad363d17cb84be9e3f757278de2b26843e918e48e33e1b00438764fefa2b;
+        _unbalancedSamples[3].proofElements[3] = 0xafee522c5ba27318361dd426e355fa02e5821fbc8b2ba52dea3b36885d76ec94;
+        _unbalancedSamples[3].proofElements[4] = 0x25171fda9d5059c4cc4b8a86af5e0634dd8217cd2167c8d868733eef3f23054f;
+        _unbalancedSamples[3].proofElements[5] = 0x1e3106cd69a1fec956fa702eae218bdba94f7150d87c05210b282e334218d265;
+        _unbalancedSamples[3].proofElements[6] = 0xd2b4a539a1349d2d145fe307c30ce5bd43fa10887ce20a8a71da53300c0a6150;
+    }
+
+    function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
+        sample_.root = bytes32(uint256(sample_.root) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        sample_.root = bytes32(uint256(sample_.root) - 1);
+    }
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Potential overflow & unnecessary state mutation in `_testVerifyWithIncorrectRoot`**

Incrementing `sample_.root` in-place risks:
1. Overflow when `root == type(uint256).max`, causing the subsequent subtract to underflow and revert.
2. Accidental pollution of shared sample data (stateful across tests).

A safer pattern is to keep the original value untouched and pass a tampered copy to `verify`:

```diff
-function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
-    sample_.root = bytes32(uint256(sample_.root) + 1);
-    vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
-    _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
-    sample_.root = bytes32(uint256(sample_.root) - 1);
-}
+function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
+    bytes32 wrongRoot_ = bytes32(uint256(sample_.root) + 1);
+    vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+    _sequentialMerkleProofs.verify(wrongRoot_, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+}
```
This avoids overflow and keeps fixtures pristine.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
        bytes32 wrongRoot_ = bytes32(uint256(sample_.root) + 1);
        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
        _sequentialMerkleProofs.verify(wrongRoot_, sample_.startingIndex, sample_.leaves, sample_.proofElements);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-08T18:35:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    function test_getRoot_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    /* ============ bitCount32 ============ */
+
+    function test_bitCount32_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max) + 1);
+    }
+
+    function test_bitCount32() external view {
+        assertEq(_sequentialMerkleProofs.__bitCount32(0), 0);
+        assertEq(_sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max)), 32);
+
+        for (uint256 i_ = 0; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__bitCount32(1 << i_), 1); // One bit set
+            assertEq(_sequentialMerkleProofs.__bitCount32((1 << i_) - 1), i_); // i_ bits set
+        }
+    }
+
+    /* ============ roundUpToPowerOf2 ============ */
+
+    function test_roundUpToPowerOf2_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max) + 1);
+    }
+
+    function test_roundUpToPowerOf2() external view {
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(0), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(2), 2);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(3), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(4), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(5), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(6), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(7), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getBalancedLeafCount ============ */
+
+    function test_getBalancedLeafCount_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max) + 1);
+    }
+
+    function test_getBalancedLeafCount() external view {
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(0), 0);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(2), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(3), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(4), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(5), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(6), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(7), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getReversedLeafNodesFromLeaves ============ */
+
+    function test_getReversedLeafNodesFromLeaves() external view {
+        bytes[] memory leaves_ = new bytes[](4);
+
+        leaves_[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        leaves_[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        leaves_[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        leaves_[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        bytes32[] memory reversedLeafNodes_ = _sequentialMerkleProofs.__getReversedLeafNodesFromLeaves(leaves_);
+
+        assertEq(reversedLeafNodes_[0], 0x8613e086c6c42ab3b7027723e29bf61497f3629e0fb0c1b0453c93f053e22a18);
+        assertEq(reversedLeafNodes_[1], 0x09a458689dbd19291ce83007725819f0c69504e282d7669b7d08576162bd7f52);
+        assertEq(reversedLeafNodes_[2], 0x569dab69f958322840fb959938fee8dbc2bde84f55584935a8230303db6aaebd);
+        assertEq(reversedLeafNodes_[3], 0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b);
+    }
+
+    /* ============ helpers ============ */
+
+    function _buildBalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 2,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](2),
+                root: 0xeeef536868dc2c030bec2d3602cc13fbe660bd5d63deca6a0a4dfd201eb941c0
+            })
+        );
+
+        _balancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _balancedSamples[0].proofElements[0] = bytes32(_balancedSamples[0].leafCount);
+        _balancedSamples[0].proofElements[1] = bytes32(
+            0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b
+        );
+
+        /* ============ Sample 2 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 1,
+                leafCount: 8,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](4),
+                root: 0x00f8c0ad3c60c727ededce5717c8baa64047b5c3f29e409085df14dc3bfda1a7
+            })
+        );
+
+        _balancedSamples[1].leaves[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        _balancedSamples[1].leaves[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        _balancedSamples[1].leaves[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        _balancedSamples[1].leaves[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        _balancedSamples[1].proofElements[0] = bytes32(_balancedSamples[1].leafCount);
+        _balancedSamples[1].proofElements[1] = 0xb7f40118daf8bbb92b1759e810c6eb4b0b92e04d1b7e4be48a147721ea457d87;
+        _balancedSamples[1].proofElements[2] = 0x06331c9b61be683c1819b2cd20c83f315499477f6253ae8d8bb02cbc6bd93c9f;
+        _balancedSamples[1].proofElements[3] = 0xc3d064e0f1ace0f92e6c822d5346de5330fb23a6c31a0c84a80d9d5c8543cc0e;
+
+        /* ============ Sample 3 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 16,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](5),
+                root: 0x31338b156e26447f0a3a965981b0be87957bf5606b44e0dcdc99eb4646048942
+            })
+        );
+
+        _balancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _balancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _balancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _balancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _balancedSamples[2].proofElements[0] = bytes32(_balancedSamples[2].leafCount);
+        _balancedSamples[2].proofElements[1] = 0x194f8cb59bc03a6e9dfcc3566bed0a85a069f50de609f51af6c74dee673450d4;
+        _balancedSamples[2].proofElements[2] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _balancedSamples[2].proofElements[3] = 0x9d8799ecccbca75f60304876c8426007565302fdb72449fe980917d7847d43e7;
+        _balancedSamples[2].proofElements[4] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+    }
+
+    function _buildUnbalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 1,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](1),
+                root: 0x5b833bdf4f55e39d1838653841d4a2c651a71b5626b7936e1bedb5212cae96e3
+            })
+        );
+
+        _unbalancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _unbalancedSamples[0].proofElements[0] = bytes32(_unbalancedSamples[0].leafCount);
+
+        /* ============ Sample 2 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 4,
+                leafCount: 7,
+                leaves: new bytes[](2),
+                proofElements: new bytes32[](3),
+                root: 0x38631dd8b5081555ec3c51cc8db7918ee90158fa33a70674c1399234d23908b2
+            })
+        );
+
+        _unbalancedSamples[1].leaves[0] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+        _unbalancedSamples[1].leaves[1] = bytes(hex"51b7ae2bab96bd3fbb3b26e1efefb0b9b6a60054ed7ffcfa700374d58f315a31");
+
+        _unbalancedSamples[1].proofElements[0] = bytes32(_unbalancedSamples[1].leafCount);
+        _unbalancedSamples[1].proofElements[1] = 0x6b379ab3dd3b0b8df76cbe09e2a04f21ebf601f2932d3505ecfb89417f3de836;
+        _unbalancedSamples[1].proofElements[2] = 0x52bd35868bccc1b1f4c3ac67dd7e3b3db4a24f60436411162d633e6d1118de89;
+
+        /* ============ Sample 3 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 13,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](3),
+                root: 0xf92d4ca528834b0350cecd9307bec2dd97d0a6bbb58b077ab51cdad36fc5c087
+            })
+        );
+
+        _unbalancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _unbalancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _unbalancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _unbalancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _unbalancedSamples[2].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[2].proofElements[1] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _unbalancedSamples[2].proofElements[2] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+
+        /* ============ Sample 4 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 200,
+                leafCount: 324,
+                leaves: new bytes[](22),
+                proofElements: new bytes32[](7),
+                root: 0x8f28d0f19d1805a3b539bcc2bb0e627ded4bf5b873bebc9199ed179a30ca312c
+            })
+        );
+
+        _unbalancedSamples[3].leaves[0] = bytes(hex"fe0403dd500c862ddbbb4736c071a575d9fd43fbdeea602833234e9cd198bb3f");
+        _unbalancedSamples[3].leaves[1] = bytes(hex"5129d77889ad52ef899ba3d85e01a6df5c230804ccc586faa7e8376b89aa0600");
+        _unbalancedSamples[3].leaves[2] = bytes(hex"f1d6a296210c112eda6ca002273abc7d21b933cf99ca0c8292e344ba2d62f750");
+        _unbalancedSamples[3].leaves[3] = bytes(hex"907f141e1702157a9ca13881b0ecb0cb10be9b588bd6499479496965ce933225");
+        _unbalancedSamples[3].leaves[4] = bytes(hex"c77ccc18e17ec5d50bbbf775f564debfa1f9da8031be2fc7ce618cea8a625226");
+        _unbalancedSamples[3].leaves[5] = bytes(hex"aad8eb018edb0690bbdcebe48db820a6a13e3f8102bb0e5535fdfed79eaa5c39");
+        _unbalancedSamples[3].leaves[6] = bytes(hex"374346003edf10ea2da568e8e973857ec4291bf10a8bf81612b36efaf6e93c23");
+        _unbalancedSamples[3].leaves[7] = bytes(hex"45a3c2e32a3c8ea5e34562806f8279e85cfad69f6015588b599dc2e774d50cee");
+        _unbalancedSamples[3].leaves[8] = bytes(hex"9322c7cbfb342280d72d4eba15c0d9711e3f730072375bc4b0cf538970014e7a");
+        _unbalancedSamples[3].leaves[9] = bytes(hex"b3017e1b1874f68c4508c0b147a4f2b96dd166274c81bb4a86082d391efad6fa");
+        _unbalancedSamples[3].leaves[10] = bytes(hex"76d7f81c6813544dc594db8b5f3d6cef292e91d02a50ad556e6bcddc1eb59f8c");
+        _unbalancedSamples[3].leaves[11] = bytes(hex"320cfee2fb238c49255d102213b395963c2a3ebd7a8ee424e8df02b01361e484");
+        _unbalancedSamples[3].leaves[12] = bytes(hex"697fa39a1e23106c54eeecf38d213bfe2fcf896f55b790815085241b7e7e7361");
+        _unbalancedSamples[3].leaves[13] = bytes(hex"510fde4a04ecbe4bf94a514ab836074270959d2b96492a56909706de0b0248be");
+        _unbalancedSamples[3].leaves[14] = bytes(hex"5fa7828c8c661f4243c97527d637bf318e5e801dcf3b488f0f302622abdf61e9");
+        _unbalancedSamples[3].leaves[15] = bytes(hex"6f7fafb63e26bafeb5fd21a76615d0f2b45e6684324e294f10078661c00b4026");
+        _unbalancedSamples[3].leaves[16] = bytes(hex"e80b189bd7a1e5d0a04d9ba778d11f1bbffec4e276f502780a97fc7006715ac5");
+        _unbalancedSamples[3].leaves[17] = bytes(hex"25e66cd82e8fa7b6072d7759d2b49d0979bad20b018c730f4d1fcac7a6959bd4");
+        _unbalancedSamples[3].leaves[18] = bytes(hex"0695e86d43e2c5097acd182042d09cda438fc1f773335ce0f190dbed668ff508");
+        _unbalancedSamples[3].leaves[19] = bytes(hex"783eb995b1a99bb4181dad1c386dff06c5c02469a4893ca8c0a5b5fd82f7747a");
+        _unbalancedSamples[3].leaves[20] = bytes(hex"44fb0e55a04f4d5accd7a9e00fd5bbbf8926083ef169c0711f503ae16b589c20");
+        _unbalancedSamples[3].leaves[21] = bytes(hex"2815ef424dd0613d69f70546821ebddf2fe1b7452510cd21d25f3e438863e8a3");
+
+        _unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[3].proofElements[1] = 0x55f9b393403d39fdf3b35fe8f394e13a272509df05d5562e5da6937c11bb1214;
+        _unbalancedSamples[3].proofElements[2] = 0xa4c0ad363d17cb84be9e3f757278de2b26843e918e48e33e1b00438764fefa2b;
+        _unbalancedSamples[3].proofElements[3] = 0xafee522c5ba27318361dd426e355fa02e5821fbc8b2ba52dea3b36885d76ec94;
+        _unbalancedSamples[3].proofElements[4] = 0x25171fda9d5059c4cc4b8a86af5e0634dd8217cd2167c8d868733eef3f23054f;
+        _unbalancedSamples[3].proofElements[5] = 0x1e3106cd69a1fec956fa702eae218bdba94f7150d87c05210b282e334218d265;
+        _unbalancedSamples[3].proofElements[6] = 0xd2b4a539a1349d2d145fe307c30ce5bd43fa10887ce20a8a71da53300c0a6150;
+    }
+
+    function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
+        sample_.root = bytes32(uint256(sample_.root) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        sample_.root = bytes32(uint256(sample_.root) - 1);
+    }
path: test/unit/SequentialMerkleProofs.t.sol
user: login: deluca-mike
body: The overflow isn't an issue at all since we're just talking about a root being different. As a matter of fact, you've given me the idea to just make all of this type of tempering unchecked so that any underflow or overflow rolls over, and still produces a different value. Again, all of this overflow and underflow is extremely unlikely given put the exact starting value of the hash would need to be (0 or max).
created_at: 2025-05-09T00:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,595 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test, stdError } from "../../lib/forge-std/src/Test.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { SequentialMerkleProofsHarness } from "../utils/Harnesses.sol";
+
+contract SequentialMerkleProofsTests is Test {
+    struct Sample {
+        uint256 startingIndex;
+        uint256 leafCount;
+        bytes[] leaves;
+        bytes32[] proofElements;
+        bytes32 root;
+    }
+
+    Sample[] internal _balancedSamples;
+    Sample[] internal _unbalancedSamples;
+
+    SequentialMerkleProofsHarness internal _sequentialMerkleProofs;
+
+    function setUp() external {
+        _sequentialMerkleProofs = new SequentialMerkleProofsHarness();
+
+        _buildBalancedSamples();
+        _buildUnbalancedSamples();
+    }
+
+    /* ============ hashLeaf ============ */
+
+    function test_hashLeaf() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Hello"),
+            0x0b8faa8dd08a172ebf9582ef1c73d06a82e28d18c61a8b47b13d01f1740e9a11
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("World"),
+            0x817d5a47d8f38274199af57907aa20a633a5f767439cfc22e11a5dd5a4493f2a
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet"),
+            0xa5dc43044db877acef8181b04b78a81c39a01d8ba78e29a5fe06a99732e626d3
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashLeaf("Lorem ipsum dolor sit amet consectetur adipiscing elit"),
+            0xa5fe16054246ec55b4c4f376e432cce9f5d7e9c6859bf2801c1d05e0a187341c
+        );
+    }
+
+    /* ============ hashNodePair ============ */
+
+    function test_hashNodePair() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x30c08f17fca6d5b183fd798df2c94be55f9191aa17b64e14af24dea51d56a200
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f,
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0xaf45a9d3cd256488928e96458a0fe6bde6182c416dad6228c15900570ad4ce86
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashNodePair(
+                0x0000000000000000000000000000000000000000000000000000000000000000,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0xcf86af75a8a3ad149001943d69c6f6baf87e648e765e365f9aea84c06a306fea
+        );
+    }
+
+    /* ============ hashPairlessNode ============ */
+
+    function test_hashPairlessNode() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+            ),
+            0x4bde7d5c3f4483ac1642c0f72078ce38639cca66529ce401be6200d03194f0a5
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
+            ),
+            0x26960017dbb60afc80dfcc5a6ccbfa17711de9e934a46ae4532a79ac33344a02
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            ),
+            0xeb14eebf6aaf45d22d20a202b833f1d484603044dea188a8eb1585c22d01a41f
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashPairlessNode(
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x2f8f0f26e7f767c7dea71e75bf4f70510db5b05f50e842eeb6dc09d678b87640
+        );
+    }
+
+    /* ============ hashRoot ============ */
+
+    function test_hashRoot() external view {
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(1, 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            0x42bdfcbf85f7e8aafa85a4c4ce995c0f8413225503d8b5aeaadaa7a044083acb
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(78, 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100),
+            0xda04fbfbac2808039eaf34829fbb30df8855c1589785cb5764e23b1b32fd2537
+        );
+
+        assertEq(
+            _sequentialMerkleProofs.__hashRoot(
+                46984,
+                0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+            ),
+            0x6bd1491b7fd13801bace4086092489a8e34ac0c15920d1ad7921e0f402eae63e
+        );
+    }
+
+    /* ============ verify ============ */
+
+    function test_verify_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_verify_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), proofElements_);
+    }
+
+    function test_verify_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.verify(0, 0, leaves_, proofElements_);
+    }
+
+    function test_verify_noLeafs() external {
+        _sequentialMerkleProofs.verify(0, 0, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_verify_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_balancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    function test_verify_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+            _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+        }
+    }
+
+    function test_verify_unbalancedSamples_invalidProofs() external {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[index_];
+
+            _testVerifyWithIncorrectRoot(sample_);
+            _testVerifyWithIncorrectStartingIndex(sample_);
+            _testVerifyWithOutOfBoundsStartingIndex(sample_);
+            _testVerifyWithIncorrectLeaf(sample_);
+            _testVerifyWithIncorrectLeafCount(sample_);
+
+            if (sample_.proofElements.length == 1) continue;
+
+            _testVerifyWithIncorrectProofElement(sample_);
+        }
+    }
+
+    /* ============ getRoot ============ */
+
+    function test_getRoot_noProofElements() external {
+        vm.expectRevert(SequentialMerkleProofs.NoProofElements.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_getRoot_noLeafs_nonZeroStartingIndex() external {
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(1, new bytes[](0), new bytes32[](1));
+    }
+
+    function test_getRoot_noLeafs_nonZeroLeafCount() external {
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(1));
+
+        vm.expectRevert(SequentialMerkleProofs.NoLeaves.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](0), proofElements_);
+    }
+
+    function test_getRoot_invalidProof() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.getRoot(0, new bytes[](1), new bytes32[](1));
+    }
+
+    function test_getRoot_invalidBitCount32Input() external {
+        bytes[] memory leaves_ = new bytes[](1);
+        leaves_[0] = bytes(hex"00");
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(type(uint32).max) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.getRoot(0, leaves_, proofElements_);
+    }
+
+    function test_getRoot_noLeaves() external {
+        assertEq(_sequentialMerkleProofs.getRoot(0, new bytes[](0), new bytes32[](1)), bytes32(0));
+    }
+
+    function test_getRoot_balancedSamples() external view {
+        for (uint256 index_; index_ < _balancedSamples.length; ++index_) {
+            Sample storage sample_ = _balancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    function test_getRoot_unbalancedSamples() external view {
+        for (uint256 index_; index_ < _unbalancedSamples.length; ++index_) {
+            Sample storage sample_ = _unbalancedSamples[0];
+
+            assertEq(
+                _sequentialMerkleProofs.getRoot(sample_.startingIndex, sample_.leaves, sample_.proofElements),
+                sample_.root
+            );
+        }
+    }
+
+    /* ============ bitCount32 ============ */
+
+    function test_bitCount32_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max) + 1);
+    }
+
+    function test_bitCount32() external view {
+        assertEq(_sequentialMerkleProofs.__bitCount32(0), 0);
+        assertEq(_sequentialMerkleProofs.__bitCount32(uint256(type(uint32).max)), 32);
+
+        for (uint256 i_ = 0; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__bitCount32(1 << i_), 1); // One bit set
+            assertEq(_sequentialMerkleProofs.__bitCount32((1 << i_) - 1), i_); // i_ bits set
+        }
+    }
+
+    /* ============ roundUpToPowerOf2 ============ */
+
+    function test_roundUpToPowerOf2_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max) + 1);
+    }
+
+    function test_roundUpToPowerOf2() external view {
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(0), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1), 1);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(2), 2);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(3), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(4), 4);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(5), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(6), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(7), 8);
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__roundUpToPowerOf2((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getBalancedLeafCount ============ */
+
+    function test_getBalancedLeafCount_invalidBitCount32Input() external {
+        vm.expectRevert(SequentialMerkleProofs.InvalidBitCount32Input.selector);
+        _sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max) + 1);
+    }
+
+    function test_getBalancedLeafCount() external view {
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(0), 0);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(2), 2);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(3), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(4), 4);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(5), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(6), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(7), 8);
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(8), 8);
+
+        assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(uint256(type(uint32).max)), 1 << 32);
+
+        for (uint256 i_ = 4; i_ < 32; ++i_) {
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount(1 << i_), 1 << i_); // Exact power of 2
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << i_) - 1), 1 << i_); // 1 less than
+            assertEq(_sequentialMerkleProofs.__getBalancedLeafCount((1 << (i_ - 1)) + 1), 1 << i_); // 1 more than prev
+        }
+    }
+
+    /* ============ getReversedLeafNodesFromLeaves ============ */
+
+    function test_getReversedLeafNodesFromLeaves() external view {
+        bytes[] memory leaves_ = new bytes[](4);
+
+        leaves_[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        leaves_[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        leaves_[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        leaves_[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        bytes32[] memory reversedLeafNodes_ = _sequentialMerkleProofs.__getReversedLeafNodesFromLeaves(leaves_);
+
+        assertEq(reversedLeafNodes_[0], 0x8613e086c6c42ab3b7027723e29bf61497f3629e0fb0c1b0453c93f053e22a18);
+        assertEq(reversedLeafNodes_[1], 0x09a458689dbd19291ce83007725819f0c69504e282d7669b7d08576162bd7f52);
+        assertEq(reversedLeafNodes_[2], 0x569dab69f958322840fb959938fee8dbc2bde84f55584935a8230303db6aaebd);
+        assertEq(reversedLeafNodes_[3], 0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b);
+    }
+
+    /* ============ helpers ============ */
+
+    function _buildBalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 2,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](2),
+                root: 0xeeef536868dc2c030bec2d3602cc13fbe660bd5d63deca6a0a4dfd201eb941c0
+            })
+        );
+
+        _balancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _balancedSamples[0].proofElements[0] = bytes32(_balancedSamples[0].leafCount);
+        _balancedSamples[0].proofElements[1] = bytes32(
+            0xb8283b9a33ca222061b7d8d5f85170289c5c4a7f96997ce2ae5bafc94fc8a59b
+        );
+
+        /* ============ Sample 2 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 1,
+                leafCount: 8,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](4),
+                root: 0x00f8c0ad3c60c727ededce5717c8baa64047b5c3f29e409085df14dc3bfda1a7
+            })
+        );
+
+        _balancedSamples[1].leaves[0] = bytes(hex"a8152e7c56b62d9fcb8af361257a260b2b9481c8683e8df1651a31508cc6ee31");
+        _balancedSamples[1].leaves[1] = bytes(hex"007f47e1c51d53cab18977050347e8e8dc488bdd9590babe3e104fcb9a1ef599");
+        _balancedSamples[1].leaves[2] = bytes(hex"7cbe68a29af312d42c40e6d083bb64fe2ba0ac6bf1cac8e4b10f5356142e3828");
+        _balancedSamples[1].leaves[3] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+
+        _balancedSamples[1].proofElements[0] = bytes32(_balancedSamples[1].leafCount);
+        _balancedSamples[1].proofElements[1] = 0xb7f40118daf8bbb92b1759e810c6eb4b0b92e04d1b7e4be48a147721ea457d87;
+        _balancedSamples[1].proofElements[2] = 0x06331c9b61be683c1819b2cd20c83f315499477f6253ae8d8bb02cbc6bd93c9f;
+        _balancedSamples[1].proofElements[3] = 0xc3d064e0f1ace0f92e6c822d5346de5330fb23a6c31a0c84a80d9d5c8543cc0e;
+
+        /* ============ Sample 3 ============ */
+
+        _balancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 16,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](5),
+                root: 0x31338b156e26447f0a3a965981b0be87957bf5606b44e0dcdc99eb4646048942
+            })
+        );
+
+        _balancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _balancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _balancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _balancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _balancedSamples[2].proofElements[0] = bytes32(_balancedSamples[2].leafCount);
+        _balancedSamples[2].proofElements[1] = 0x194f8cb59bc03a6e9dfcc3566bed0a85a069f50de609f51af6c74dee673450d4;
+        _balancedSamples[2].proofElements[2] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _balancedSamples[2].proofElements[3] = 0x9d8799ecccbca75f60304876c8426007565302fdb72449fe980917d7847d43e7;
+        _balancedSamples[2].proofElements[4] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+    }
+
+    function _buildUnbalancedSamples() internal {
+        /* ============ Sample 1 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 0,
+                leafCount: 1,
+                leaves: new bytes[](1),
+                proofElements: new bytes32[](1),
+                root: 0x5b833bdf4f55e39d1838653841d4a2c651a71b5626b7936e1bedb5212cae96e3
+            })
+        );
+
+        _unbalancedSamples[0].leaves[0] = bytes(hex"6330b989705733cc5c1f7285b8a5b892e08be86ed6fbe9d254713a4277bc5bd2");
+
+        _unbalancedSamples[0].proofElements[0] = bytes32(_unbalancedSamples[0].leafCount);
+
+        /* ============ Sample 2 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 4,
+                leafCount: 7,
+                leaves: new bytes[](2),
+                proofElements: new bytes32[](3),
+                root: 0x38631dd8b5081555ec3c51cc8db7918ee90158fa33a70674c1399234d23908b2
+            })
+        );
+
+        _unbalancedSamples[1].leaves[0] = bytes(hex"4a864e860c0d0247c6aa5ebcb2bc3f15fc4ddf86213258f4bf0b72e51c9d9c69");
+        _unbalancedSamples[1].leaves[1] = bytes(hex"51b7ae2bab96bd3fbb3b26e1efefb0b9b6a60054ed7ffcfa700374d58f315a31");
+
+        _unbalancedSamples[1].proofElements[0] = bytes32(_unbalancedSamples[1].leafCount);
+        _unbalancedSamples[1].proofElements[1] = 0x6b379ab3dd3b0b8df76cbe09e2a04f21ebf601f2932d3505ecfb89417f3de836;
+        _unbalancedSamples[1].proofElements[2] = 0x52bd35868bccc1b1f4c3ac67dd7e3b3db4a24f60436411162d633e6d1118de89;
+
+        /* ============ Sample 3 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 9,
+                leafCount: 13,
+                leaves: new bytes[](4),
+                proofElements: new bytes32[](3),
+                root: 0xf92d4ca528834b0350cecd9307bec2dd97d0a6bbb58b077ab51cdad36fc5c087
+            })
+        );
+
+        _unbalancedSamples[2].leaves[0] = bytes(hex"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2");
+        _unbalancedSamples[2].leaves[1] = bytes(hex"112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00");
+        _unbalancedSamples[2].leaves[2] = bytes(hex"f0e1d2c3b4a5968778695a4b3c2d1e0f1f2e3d4c5b6a79887766554433221100");
+        _unbalancedSamples[2].leaves[3] = bytes(hex"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
+
+        _unbalancedSamples[2].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[2].proofElements[1] = 0x2642cf6153e4ddfd7ea9c1c99d86efe99c03d29bce0ce4adbf7c0162865aa93a;
+        _unbalancedSamples[2].proofElements[2] = 0x1f70e7dd11a042e3868e8b0992118a3d7bd301b029a3b967a5b2042466c5110c;
+
+        /* ============ Sample 4 ============ */
+
+        _unbalancedSamples.push(
+            Sample({
+                startingIndex: 200,
+                leafCount: 324,
+                leaves: new bytes[](22),
+                proofElements: new bytes32[](7),
+                root: 0x8f28d0f19d1805a3b539bcc2bb0e627ded4bf5b873bebc9199ed179a30ca312c
+            })
+        );
+
+        _unbalancedSamples[3].leaves[0] = bytes(hex"fe0403dd500c862ddbbb4736c071a575d9fd43fbdeea602833234e9cd198bb3f");
+        _unbalancedSamples[3].leaves[1] = bytes(hex"5129d77889ad52ef899ba3d85e01a6df5c230804ccc586faa7e8376b89aa0600");
+        _unbalancedSamples[3].leaves[2] = bytes(hex"f1d6a296210c112eda6ca002273abc7d21b933cf99ca0c8292e344ba2d62f750");
+        _unbalancedSamples[3].leaves[3] = bytes(hex"907f141e1702157a9ca13881b0ecb0cb10be9b588bd6499479496965ce933225");
+        _unbalancedSamples[3].leaves[4] = bytes(hex"c77ccc18e17ec5d50bbbf775f564debfa1f9da8031be2fc7ce618cea8a625226");
+        _unbalancedSamples[3].leaves[5] = bytes(hex"aad8eb018edb0690bbdcebe48db820a6a13e3f8102bb0e5535fdfed79eaa5c39");
+        _unbalancedSamples[3].leaves[6] = bytes(hex"374346003edf10ea2da568e8e973857ec4291bf10a8bf81612b36efaf6e93c23");
+        _unbalancedSamples[3].leaves[7] = bytes(hex"45a3c2e32a3c8ea5e34562806f8279e85cfad69f6015588b599dc2e774d50cee");
+        _unbalancedSamples[3].leaves[8] = bytes(hex"9322c7cbfb342280d72d4eba15c0d9711e3f730072375bc4b0cf538970014e7a");
+        _unbalancedSamples[3].leaves[9] = bytes(hex"b3017e1b1874f68c4508c0b147a4f2b96dd166274c81bb4a86082d391efad6fa");
+        _unbalancedSamples[3].leaves[10] = bytes(hex"76d7f81c6813544dc594db8b5f3d6cef292e91d02a50ad556e6bcddc1eb59f8c");
+        _unbalancedSamples[3].leaves[11] = bytes(hex"320cfee2fb238c49255d102213b395963c2a3ebd7a8ee424e8df02b01361e484");
+        _unbalancedSamples[3].leaves[12] = bytes(hex"697fa39a1e23106c54eeecf38d213bfe2fcf896f55b790815085241b7e7e7361");
+        _unbalancedSamples[3].leaves[13] = bytes(hex"510fde4a04ecbe4bf94a514ab836074270959d2b96492a56909706de0b0248be");
+        _unbalancedSamples[3].leaves[14] = bytes(hex"5fa7828c8c661f4243c97527d637bf318e5e801dcf3b488f0f302622abdf61e9");
+        _unbalancedSamples[3].leaves[15] = bytes(hex"6f7fafb63e26bafeb5fd21a76615d0f2b45e6684324e294f10078661c00b4026");
+        _unbalancedSamples[3].leaves[16] = bytes(hex"e80b189bd7a1e5d0a04d9ba778d11f1bbffec4e276f502780a97fc7006715ac5");
+        _unbalancedSamples[3].leaves[17] = bytes(hex"25e66cd82e8fa7b6072d7759d2b49d0979bad20b018c730f4d1fcac7a6959bd4");
+        _unbalancedSamples[3].leaves[18] = bytes(hex"0695e86d43e2c5097acd182042d09cda438fc1f773335ce0f190dbed668ff508");
+        _unbalancedSamples[3].leaves[19] = bytes(hex"783eb995b1a99bb4181dad1c386dff06c5c02469a4893ca8c0a5b5fd82f7747a");
+        _unbalancedSamples[3].leaves[20] = bytes(hex"44fb0e55a04f4d5accd7a9e00fd5bbbf8926083ef169c0711f503ae16b589c20");
+        _unbalancedSamples[3].leaves[21] = bytes(hex"2815ef424dd0613d69f70546821ebddf2fe1b7452510cd21d25f3e438863e8a3");
+
+        _unbalancedSamples[3].proofElements[0] = bytes32(_unbalancedSamples[2].leafCount);
+        _unbalancedSamples[3].proofElements[1] = 0x55f9b393403d39fdf3b35fe8f394e13a272509df05d5562e5da6937c11bb1214;
+        _unbalancedSamples[3].proofElements[2] = 0xa4c0ad363d17cb84be9e3f757278de2b26843e918e48e33e1b00438764fefa2b;
+        _unbalancedSamples[3].proofElements[3] = 0xafee522c5ba27318361dd426e355fa02e5821fbc8b2ba52dea3b36885d76ec94;
+        _unbalancedSamples[3].proofElements[4] = 0x25171fda9d5059c4cc4b8a86af5e0634dd8217cd2167c8d868733eef3f23054f;
+        _unbalancedSamples[3].proofElements[5] = 0x1e3106cd69a1fec956fa702eae218bdba94f7150d87c05210b282e334218d265;
+        _unbalancedSamples[3].proofElements[6] = 0xd2b4a539a1349d2d145fe307c30ce5bd43fa10887ce20a8a71da53300c0a6150;
+    }
+
+    function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
+        sample_.root = bytes32(uint256(sample_.root) + 1);
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
+
+        sample_.root = bytes32(uint256(sample_.root) - 1);
+    }
path: test/unit/SequentialMerkleProofs.t.sol
user: login: coderabbitai[bot]
body: @deluca-mike You're absolutely right! For test purposes, a simple difference is all you need, and the edge cases are extremely unlikely. Your idea to use unchecked blocks is elegant - here's how it might look:

```solidity
function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
    bytes32 originalRoot = sample_.root;
    
    unchecked {
        sample_.root = bytes32(uint256(sample_.root) + 1);
    }
    
    vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
    _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
    
    unchecked {
        sample_.root = originalRoot;
    }
}
```

This ensures any overflow would just roll over as you intended. The approach works well for your testing needs!

🐰 *thoughtfully considers the elegance of unchecked blocks*


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike You're absolutely right! For test purposes, a simple difference is all you need, and the edge cases are extremely unlikely. Your idea to use unchecked blocks is elegant - here's how it might look:

```solidity
function _testVerifyWithIncorrectRoot(Sample storage sample_) internal {
    bytes32 originalRoot = sample_.root;
    
    unchecked {
        sample_.root = bytes32(uint256(sample_.root) + 1);
    }
    
    vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
    _sequentialMerkleProofs.verify(sample_.root, sample_.startingIndex, sample_.leaves, sample_.proofElements);
    
    unchecked {
        sample_.root = originalRoot;
    }
}
```

This ensures any overflow would just roll over as you intended. The approach works well for your testing needs!

🐰 *thoughtfully considers the elegance of unchecked blocks*</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T00:42:04Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-02T18:32:16Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new Solidity library, `SequentialMerkleProofs`, has been introduced to provide low-level utilities for verifying sequential Merkle proofs, including functions for root calculation and proof validation. The library defines custom errors, hashing routines, and bitwise utilities to handle both balanced and unbalanced Merkle trees. Comprehensive unit tests have been added in a dedicated test contract, covering all major functions and edge cases. Additionally, a harness contract is provided to expose internal library functions for testing purposes, ensuring thorough validation of the library's correctness and behavior.

## Changes

| File(s)                          | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| src/libraries/SequentialMerkleProofs.sol | Added new library `SequentialMerkleProofs` with functions for verifying sequential Merkle proofs, calculating Merkle roots, custom errors, hashing utilities, and bitwise helpers.                                                                                                                                                                                                                                                                                             |
| test/unit/SequentialMerkleProofs.t.sol     | Added new test contract `SequentialMerkleProofsTests` with comprehensive unit tests for all public and internal functions of the `SequentialMerkleProofs` library, covering valid and invalid cases, edge conditions, and utility functions.                                                                                                                                                                                                                                   |
| test/utils/Harnesses.sol         | Added new harness contract `SequentialMerkleProofsHarness` to expose internal and public functions of the `SequentialMerkleProofs` library for testing, including root calculation, proof verification, hashing, and bitwise utilities.                                                                                                                                                                                                                                         |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant SequentialMerkleProofs

    User->>SequentialMerkleProofs: verify(root, startingIndex, leaves, proofElements)
    SequentialMerkleProofs->>SequentialMerkleProofs: _getRoot(startingIndex, leaves, proofElements)
    SequentialMerkleProofs->>SequentialMerkleProofs: Hash leaves, calculate balanced leaf count
    SequentialMerkleProofs->>SequentialMerkleProofs: Iteratively combine hashes using proof elements
    SequentialMerkleProofs->>SequentialMerkleProofs: Compute final root
    SequentialMerkleProofs-->>User: Revert on failure or return success

    User->>SequentialMerkleProofs: getRoot(startingIndex, leaves, proofElements)
    SequentialMerkleProofs->>SequentialMerkleProofs: _getRoot(startingIndex, leaves, proofElements)
    SequentialMerkleProofs-->>User: Return computed root
```

## Poem

> In the garden of hashes, a tree grows tall,  
> With proofs in a row, and leaves that fall.  
> Sequential and sturdy, the branches align,  
> Each root now computed, the logic is fine!  
> With tests all a-flutter, and harness in tow,  
> This rabbit says, “Bravo! Let the Merkle roots grow!”  
> 🐇🌳✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNxU3bABsvkCiQBHbGlcABpIcVwvOkgAIgAzEmouREDgjHE0XzYKAGtongp8fHjEUNiUDKLabAZpdEhyAHdGF24CIipuBAZIKQp4ePgGanh8LC94ASoXFGZuaLYMmPgsXFgSSCGCgGU09ngsgFlKfJIrIpLEDUR8Lw0YDchJ6ed5XnwJeCVkeOwMMRjDBZdTyAh9SiDeSpIIHLLPJLxHj4RDqIHIJrqBBYE55Aq4fzScJkFSTDBEPog2ijcb8JH4ChKfz0aJoJEw9J1a6QADSJHkiWo2H8yBKESeLxm8lWDC82CUCn8EIGQxG4lpfwB6owyGBbHo/ShAAoAJToDD0Ui4GzFXCm8JNHqwSCwTC0fFPCRUmlYMUcuG+XFnQrFUoPACCtFoaPGWS8snC602krecwWJCWuGQiG4okOkwAXjFXYhsRTNYDxsh1tRINhUpBo4hxFrCiQhgAPerxBkItBSMqNRRE830S5ZiL4SDMbQZWetWTtfCdNDdYaVGidUEPACSuHQXlulVl8vqan3M4w8E8Xh9dfEk3E9XBaEQqObkEx6wihMYWQY3g+sgbr2Ng3DcAyE6Gqqd49nwAj4N+AhZJgdT0CB/zIbeAIxEG+KEtycCbDOqzKoMwx3l4y7riMWACJsPb/PQpFJmR8SyKaWz/JWWBftikB7LCGT5nh5yXGGtz3I8qBoFGMZYGQro4dWTwjNwKjwI+YJTrmFBwcw/CMpQMT+jhbEUdqlSQKJP4kPUfEsU8iAzhQ+5MFUaBiMg/gQaiBAuBoRgQGAbjqNEXCCpw9j7MJ8I5MGHxXJAkAGAAco49EUFwACs2XJQYACqqRZY2JByiMYDMPAuQkEFYCGAYJhQGQ9BimgeCEKQ5BUDQ9BMKw7BcLw/DCKI4gDpAcgKEyKjnloOj6I14BQO4yAyVg7UEMQZDKL1CgDRkXBUC0DhOLMU1MDNqjqPNuj1UYS2mAYiAUAwAD0KYDNIb2CekmReKJFyhtckkcAYsQQwYFiQOGO7bd11AmY4LnyGKDBKaQiBuAguokC0Ox3N8oLPFMUrhAABr9AaA+JiDk+EHxfD8XFakCIK4NpbHQjF/3WacBSJaUDpOiGUjirWaDRUJdR0n2E2rrmzgKDqoh4JIZXShtfN4psBJ2bu+5KEM5DIO5zaYBOcGQBm7Rgr+46jm2nbdr2rLxOEGDDuEIEOyWZYPERJOvLMSiys49QAc2LDWxQRQUL8varF6kzMRgngTg46PoLqU6sgO4RJ1SIYlB7OnidbizsIOIGFynfZIkw/yaNjxHzqsNB6Z5mwynKzPrL+7eUMCvieEqFbaogXDk9Bsj05A5NWjaiFzyBC8kLgAAyiIAML4E35MPNPkLseTjAbAwuRrUiktEOrWAOzOuDo8+qksOnMQO/ERQGZL5uuasFJVhKA7N7KWnJNhijziOECgsK4ZiruEfw/QWwUg1NoOU/hD6L1tKffq78VKbBsp/b+4pNh/xQZUYBiCSD9FSCyRELpXwbGrhaYuSIyrwIyNyNem8d57wyKfEgHYCSeQnKxAguB4RuwUE3LYJDWKwI4ZmDEWI977hngA0hbDAoGAABJlV0izHia0AS902OTAA+ueXeTcADMAAmU+4JG4ZGivuc8JiGgOLAOeDcJBSAUAphYooTECrcGgPgKw+AmiUAAPLxEcZOPw/D0KNAypQOs3AkmsXIOHD8EEYl8DFLgJo+BvasMsVaAAQihHCtAt5shsQIpJeC8CbEllhVCMRpEuP3FbPWmxOkAgATowOTAlRUVvr0VApFKnryXrgOejphjOn8GbAktQxFPCIbaSa0oO6jCkPGRhpZNHqXgPHWWntma9lROSAo1z6hGitphWpaFbLSDNPWTRCiihMxiIoyuXDHZIMoHQ+uJzmEG2nJgNAmMPmUOGCOWBZtHDtCBOU+gSl3T1DoKQP8qRsy1GdK+acSA7kUlRAIMkRBPGvOwu8gZhEnjjyBHWQlDQGAXIArePgsJgjoHAkUTyzorZsGYAyeQ7ZVTwDIAweQGFuDUhoNWX8CEmLATWheT49QwJaIGQiI51wjCRmjNqeEastJGInseMx88LFIAAKJSAwE4qcz8L5DgVOpAYHMglIC3vEXA4YMCOo7EgFBuimFutKh3Kq5AUBIlYtEINkKrKS3OXwVAX9o6YGtuG5smjWl7T9oZBogLOFhEdn7TRrKqz2r9g0+Ic9LF+1SsOKw2gKAtosX7TtFzohvnbUoFeFTe1MIWafZZWdeAkFzKwnMeZVRO3gF2GQ7YGSbBreSJJTYUHYCQM6N2pclCYuSYhSF0gdEADFVhxgTPaxeNCwV0CbcO6Q17v4NIHLg8YyDgIUHPFKMAqIiz0P7PUduucGGPM8aC+OECjJ8Cth8LkFKdGxP6HGRMEpSaphKIkS5ksigCHrFWqiTQwDRCOWmIFkjLJWw0Tu0yvMbKCxUd+JQkjNIxEoHHRhFoaWO2lcMWVrjxWSvZXCkgOjzCWHDF4A5NrwSsVDryoCsshEQVcjEXsnhqXrgOE+LGBgoAbzw7MWSShaBTyprFAG/MxLA1PrMl671PqysQD9HmIlHNAyuDcO45NTOQGvdxSyVm6BTxnkaOQKqHHntwBY8IB6Mj2OygANnsJI/+5IdwWiEclvZKqADaABdP8PhlWSygYgIrcXpAOPK5Vrw1W2GOro3Vk0p8jSDwoMPHgwoSAmhC2F1mtJIs2fntgxCRpUu4HS1l8hAD8vAPq7IUrFWRhVeoDVpIA51vxfsc17brXdvtc6xY7rkBevLH6/CUew3RvhbZZNqeVom1NLtA1xATWttxja4LDrlaus9b6wNx7I2IaxDqiFAwKrcBvX+OobzQl/o02BhoTQoNwaQ2hrDeGu0kZnVRg3DG0hsaoGaAJQmZqwQhGViIsQ887Po787TaAIQ6a0ZB/tWdGwdTqzrFefcCOE58FYpTHzxx2fOaDlKdlmjJaun69IU24wmf7il2j3zOt/OlCjart8MatMom7ndgbdadTQqNqseoktKZoHmNEU+zYKCbKSbAO49AEeNnOz3eUmjls7qAUI8IPT+Hkf28i8uSiEHCY7LmMQH9dl+xHHxaKPrEboFjmgWQ4vJpvJiBhDAQz3k2QNYgJ36ZmXmNSLgMJp8reVDRCCIsBDGFG+AqwiCN5EYEIbFX537Tc/59Ga/DyzOxeKi3Uw2tL2qxT0bYiFtbaO1dtX0w/tFBB2IHfaO+g5M/aTr2dnYqFCtNjWLEw/geB04F69AMPeJj78ByeEfFUs9rVstQAjmIGfmBvBxB0x7A6hgQn8TETxowd1PY2E4FlFS45Z7IsQhwMAwAiwihstnAKFQ8Owy0I8m4C4MBk5vhJp1AZFXFVh78z0SD6B2MthewEIkIi90JWF6Uul6Ah8a9oUsgjxp9a5SD6CppLxrxAIKFxxBxg9AELQkVBxatw8GFekWE6DY9OtiRE8xolchV8BeBDgaA/Bn1XIY444iUs5SVyZ21v1pAW1219dgdlEW18taCql1AvsHF8t04D954nCqR9cD4W5pt5kcFv9aRUBUQqpeVjk/96ArY+NewRgOUQJxk1lcATZgIiBZwPxWIuCChqs39zEPs+F94QisBf8QgYhYjY5exsUhNaQbYOY4C49gUa5iCi4CCuFvYMQypfBSVkixoIVel81NcgQHgCoHxiYrdkByZrFI8HEW0QkLQwkIkolCl4lHEz0eEakGVX0ijmlw4IhyiYjexaDb9cB79hMqi+AajNFQjWi64qC8BuRTV5J70cMGIF8sB3JkFNFaskl4NwVpFYMUBqxDiGC+A+ixA0idF9EvBDFwd4RJjJoD1WstEy9i82DS8WD7Bq9cjzt68ONnRZ1bdyBwN85Gi1CQVbQni5JzVfB4TfANhYSMlETfdoILI2V4h0EhsZB5AlAXgidjlkiwIKEcjNg2sQJXxz9NFL9k8xxDD3ESBXQvgGRx9dYud2VdYnhr0GR8VzYLRnAWQLMBRexJTKAbUfZ5T80k96N0RwgmBDQd1JZMQFQqByQIEkQHiJwQI8VNgEjnwpwyAHAlRWIITUi1dKTSNmw0jZZJdWddczh9dudPpZAAByZAbdIgBmcuNktUDFRLSrHlH0BQtkCg/cIRYY8YM9S1CYj441B6fHRTXadEbJJ4NTZwDTMUU3HTVqPgfTSYXoIzTzIKSAVKPGRnKgZnN7FnaXBzPXDnLnFzLAcmBHJHEXVHP6eM6IRMrHQLLwKdFAwAxTa8AWbAAzXoX3SYsGZKKAcmevMJTicsoeLIYLa8+eBHcdUsJtB84RJ83wL4PGF83QN8kID82Ad9bfb8juAbf8poQCm898vtLtXfd9SC38voWVWCgwV85ckC4/W0VC+7P8jCuC4C5sCxGeCxT2OwzrAi4eEinCsiiiz2JtOrT2DAAALUoHwB2ByxQVWyEVoufKwqAoYqSyYvwBYsovGE4qKE+0j0Er3OEvgpAoooENoBcNwDcPsQ8LwAUvovfPEpYr0qUtIrEuPlkCsRYJ2BxOkAUvQoApMtEvIvMssu2NoGsuHzqzUsTOMuwoMpcvYLqQ8przspgv0pUoCsxLcuCsHQdTuO+B8rNEfMIvCrIpmySyovEnsKrl8pEvfPSqkskrYpku4t4pWwKw7FyuUrSqCMQkKsRFYukq4rkqbiqtMosQKu8vEjaqcs6vivUtcNmO0rTl0qSp/JSscvytqoyokuj0QB6qmutFtFco4JitsrGqgvhDCsmpAoKsCrQjWvmo2rQu2r8t2vXhaoyCkuopBwWvOt4UaUjzitoMurtGOomrOpqoeviC+1CuIp2rIpmLsXsWeqpA0q0p0reqGN/NSqSyBoyAcT+ocs+qSwWNoCWMiWiTiQSVBpTnBqGshrurIrRoxpWOxvsSRswpRo6vXi2I4Netxu+HxuBsJverooBqS2qRYNespthpputEMLoTfWHEQE/RYCsKOuhsIvsswpC3y02oZIMWZNrPQCjCi0cqsWRPUqspsslvpPos1s0nRqitWt1s4n1o1oRwADVzKAB1LEfLUMhZI0Na7LBkaTbE4fK7PxD6kSixa2u2h2gEBkFIni7A8q4BF2myt2qgXUmy72i27C/2kIG2z/e29YWJPAeJKpFJRAMO3LIgfiyq12qOWOsheOs0ROv2gOtOoO0Mr8ku/yD20UhOi3ISpOmuqEdO2AR2kOsaHmxu92uOr2yutuxSjulOwO9YXu2OMaG6zMKO4fGO5uiun29m6HWHIwFcy1LzQ3NIq9HHDe/HOGLqInTg5GfDMnTATGAI6MeID0qoRQWoe3RoMc9yTXacnXGXOc4GPetXRc0ypHB8Xe5wfekGILN/VAFXaM03DlBRU8/sx2ekwbMeFW4pd/OM7+hM2mU+ZMsEg4wtckHgqMZAZKiHIbT8LoQxRE/pXDYOVM5AEiLAHMu8H2XZbbQsyyNgdYRQZAI0D/KEFtdK8mE0UBanZvHhV6mNfwAkWVMWZNRQyPORaOCtBAx2JoKhsFfB5B6shoy8+1eG3AOYoJEm8JTG1YhJHtLmtyqRoJZfNkHtNfJQbfRxrfJCtXffOxidHBDY/mmwQWnYtkd9UWr9Oag+SAR1EVShhWDJaMFIwUuMDvPonMcYaAikPB5vDPbIp3TYLPbhrRi03AYUHULREUIAxNQVBYCialGTEcgMgtChS6RDZ4aiaZRhxQciOgAAbi0XRmvrITuA1lVtoBUipzHOgfDPfonP3BrnmEgg721w3Kwa3JwflzeFkyhnk0bJ6mbJU1bNEHUxtU7MT0gl017IQcM2EmM2HNHJaCmdEWGai0/qWdnOwd/tAf/qslEqAc0hAa7wPqC0/BQNYh7B8GiWlPGvIdQfGx1CvJEpix+wS3HCK3m0WywILqLsO2kBOwB3O1qyxd+2O3+x20kQuxByu3ovSrm3bjRakMxfCB+xxZJb2wgzqwZY20ayJZa0ByysuzNBkeKb4cRfsUSwpccsKMetauFaZbO1JaBz5YMKKf6z4dRcy3ri+zFaTqsUGuBupbSzVYwG9oFeVZu1Vay0Mc1b9uCRSVJqxooDWL1YWwNaNfXkFdNZpbVYKXJstZvIsX5rprqR5rNfVaev5ddZNcdbRbRPqV2KS26w1tAq/J+25bxcRBdaVZKdiw5cJchR9ftVAvAq7SzaOwRCDXfQJaRfgCIFgFwHLbDYzaFezYSz9jzcsVAu32QuHGLc5a9RIHTbde7ZzZbfja1bwtm2Dbdg1fZZLeRfrYHeFdzZHatafVoUCfiGCbFuYAlsHZlba3xbnYjelYqwk1mH+MCYO3jY3tM3ukemalYTag6kJx6hiH6kzCOjQBOgvvOnkCaaoGuk0G0DukMCan2iqiS2+DqyQQwroAsXISA+MGWkgAAEZ7ESAAB2DD+IbKexAABlsTQ4AE4GARg0ACPcoAAWHD7KAQWxeIAjzk2jnD9D+xDLDLNDvD+DkD+xAjtANAND2xBgWgcj9j2xAQPD+xJDtDuj2xHDtDwTkYAjgQDLcjkgWxcj2xJD+IPD3oRaEDgADgYC0+yhw4ECQ54+U4EDQ7QHsVo5ID0709oAcVsSE4YHY4y14/c/sTQ7sh0/uhA7Y+M9kj0+QnsTqHiD0+yno/c4y3sXI5o7QDY9sU86U4M5GAEAI7Q848Q9kgyz07QEk/o7Q+yiUEo+pD08U7Y5IHsXiFo4YHI+s6UGyns9w5UCy4gEgDQ706Q4YGo4yxw88ko949yns8s6Q9oHs6a707k9sQcVC9oDM5IHI6RF08Q865IHoli+yjU9Q708o9i5GDsnvu4/I9C4YAEFoGw6Q66/iCQ4EGW788Q7Y4YG47I5IAyw0/sTQAM7Q/sWQg04YHe/iAyzu7sloEs5i9y7k/oBW/a+pDE4EHK609UFc9oCK7oHK5++yis8+5IFu4y2pAI/sV+705w/u+A8Q/ojqAI7B4I/o6Q9x4c4I/c6k+yhU4y5m4c7QHI4YCQ5M/O+yjG/g9vdA/UAdRGeCRoWg9oAsRaiA6AA -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=xmtp/smart-contracts&utm_content=55):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-02T18:35:27Z
body: ### [LCOV](https://github.com/marketplace/actions/report-lcov) of commit [<code>bece9db</code>](55/commits/bece9db99f1e18d96a7f54e97338da4c10bbd51d) during [Solidity #186](../actions/runs/14933439777)
<pre>Summary coverage rate:
  lines......: 100.0% (703 of 703 lines)
  functions..: 100.0% (207 of 207 functions)
  branches...: 100.0% (156 of 156 branches)

Files changed coverage rate:
                                                           |Lines       |Functions  |Branches    
  Filename                                                 |Rate     Num|Rate    Num|Rate     Num
  ===============================================================================================
  src/libraries/SequentialMerkleProofs.sol                 |17.7%     79| 100%    14|    -      0</pre>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-02T18:37:18Z
body: # Changes to gas cost

> Generated at commit: [d62d45882ef80ccb0a7a06c9a8501b293d2e85b1](/xmtp/smart-contracts/commit/d62d45882ef80ccb0a7a06c9a8501b293d2e85b1), compared to commit: [905b9fd7b8d266602078750784036e9194c3a9f2](/xmtp/smart-contracts/commit/905b9fd7b8d266602078750784036e9194c3a9f2)

### 🧾 Summary (20% most significant diffs)

| Contract | Method | Avg (+/-) | % |
|:-|:-|-:|-:|
| **GroupMessageBroadcasterHarness** | ___setSequenceId_ | -1,990 ✅ | **-8.86%** |
---

<details>
<summary><strong>Full diff report</strong> 👇</summary>
<br />

| Contract | Deployment Cost (+/-) | Method | Min (+/-) | % | Avg (+/-) | % | Median (+/-) | % | Max (+/-) | % | # Calls (+/-) |
|:-|-:|:-|-:|-:|-:|-:|-:|-:|-:|-:|-:|
| **GroupMessageBroadcasterHarness** | 969,035&nbsp;(0) | ___setSequenceId_<br />_addMessage_ | 2,551&nbsp;(-19,900)<br />2,679&nbsp;(0) | **-88.64%**<br />**0.00%** | 20,461&nbsp;(-1,990)<br />4,164,341&nbsp;(-6,364) | **-8.86%**<br />**-0.15%** | 22,451&nbsp;(0)<br />7,125&nbsp;(0) | **0.00%**<br />**0.00%** | 22,451&nbsp;(0)<br />62,326,764&nbsp;(0) | **0.00%**<br />**0.00%** | 10&nbsp;(0)<br />15&nbsp;(0) |
| **PayerRegistryHarness** | 2,702,853&nbsp;(0) | ___setTotalDebt_<br />_cancelWithdrawal_<br />_deposit(address,uint96)_<br />_deposit(uint96)_<br />_getBalance_<br />_requestWithdrawal_ | 2,555&nbsp;(0)<br />2,387&nbsp;(0)<br />2,644&nbsp;(0)<br />2,557&nbsp;(0)<br />602&nbsp;(0)<br />2,557&nbsp;(0) | **0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%** | 5,309&nbsp;(+2)<br />11,766&nbsp;(+102)<br />13,422&nbsp;(+165)<br />13,333&nbsp;(+165)<br />985&nbsp;(+7)<br />11,020&nbsp;(+3) | **+0.04%**<br />**+0.87%**<br />**+1.24%**<br />**+1.25%**<br />**+0.72%**<br />**+0.03%** | 5,355&nbsp;(0)<br />13,108&nbsp;(0)<br />19,197&nbsp;(0)<br />19,107&nbsp;(0)<br />602&nbsp;(0)<br />11,046&nbsp;(0) | **0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%** | 5,355&nbsp;(0)<br />13,173&nbsp;(0)<br />36,297&nbsp;(0)<br />36,207&nbsp;(0)<br />2,602&nbsp;(0)<br />28,146&nbsp;(0) | **0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%** | 556&nbsp;(+29)<br />259&nbsp;(0)<br />261&nbsp;(0)<br />261&nbsp;(0)<br />757&nbsp;(+2)<br />261&nbsp;(0) |
| **Proxy** | 105,099&nbsp;(+12) | _fallback_ | 471&nbsp;(0) | **0.00%** | 126,454&nbsp;(-192) | **-0.15%** | 31,408&nbsp;(0) | **0.00%** | 169,210,019&nbsp;(0) | **0.00%** | 9,950&nbsp;(+21) |
| **IdentityUpdateBroadcasterHarness** | 971,646&nbsp;(0) | _addIdentityUpdate_ | 2,678&nbsp;(0) | **0.00%** | 8,690,800&nbsp;(-8) | **-0.00%** | 7,135&nbsp;(0) | **0.00%** | 67,928,197&nbsp;(0) | **0.00%** | 15&nbsp;(0) |
| **Factory** | 785,317&nbsp;(+12) | _deployImplementation_<br />_deployProxy_ | 21,842&nbsp;(0)<br />109,157&nbsp;(0) | **0.00%**<br />**0.00%** | 216,050,370&nbsp;(+3)<br />323,069,086&nbsp;(-1) | **+0.00%**<br />**-0.00%** | 850,947&nbsp;(+6)<br />155,172&nbsp;(0) | **+0.00%**<br />**0.00%** | 1,937,981,623&nbsp;(0)<br />1,937,821,154&nbsp;(0) | **0.00%**<br />**0.00%** | 18&nbsp;(0)<br />18&nbsp;(0) |
| **AppChainGatewayHarness** | 833,309&nbsp;(+12) |  |  |  |  |  |  |  |  |  |  |
| **ParameterKeysHarness** | 644,685&nbsp;(+12) |  |  |  |  |  |  |  |  |  |  |
| **MockParameterRegistry** | 183,827&nbsp;(+12) |  |  |  |  |  |  |  |  |  |  |
</details>
<!-- Sticky Pull Request Comment -->
author_association: NONE
user: login: macroscopeapp[bot]
created_at: 2025-05-02T18:48:06Z
body: ### Implement sequential merkle proof verification library to validate ordered leaf positions in merkle trees
The `SequentialMerkleProofs` library provides functionality for verifying sequential merkle proofs through:

* Core verification functions `verify()` and `getRoot()` in [SequentialMerkleProofs.sol](https://github.com/xmtp/smart-contracts/pull/55/files#diff-ff40daaf8a5948d34b0bb31c466d78910cb7381d9e82701848d8b81edf6fe765) for validating leaf ordering in merkle trees
* Specialized hashing functions for leaf, node, and root operations with defined prefix constants
* Helper utilities for bit manipulation and power-of-2 calculations
* Comprehensive test coverage in [SequentialMerkleProofs.t.sol](https://github.com/xmtp/smart-contracts/pull/55/files#diff-7f3137106f5bd398f284df5d28672fdf2d9a80ada5dcdc1f5704f887f9d7d917) with balanced and unbalanced tree scenarios
* Test harness in [Harnesses.sol](https://github.com/xmtp/smart-contracts/pull/55/files#diff-f803a3747dfd9bb6151512e75424166315fd4e6c591066540f3f690ec7093177) exposing internal library functions

#### 📍Where to Start
Start with the main `verify()` function in [SequentialMerkleProofs.sol](https://github.com/xmtp/smart-contracts/pull/55/files#diff-ff40daaf8a5948d34b0bb31c466d78910cb7381d9e82701848d8b81edf6fe765) which implements the core sequential proof verification logic.



#### Changes since #55 opened

- Added `getLeafCount()` function to `SequentialMerkleProofs` library [dab0b89]
- Added constants and error definitions to `SequentialMerkleProofs` library [dab0b89]
- Added test coverage for `getLeafCount()` function [dab0b89]
- Fixed array indexing in `SequentialMerkleProofs` test functions [bece9db]
- Corrected test data initialization in `SequentialMerkleProofs` [bece9db]
- Added unchecked arithmetic blocks in `SequentialMerkleProofs` test helpers [bece9db]
----

_[Macroscope](https://app.macroscope.com) summarized bece9db._
author_association: NONE