// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * @title CheckpointLeaf * @notice Canonical payment leaves for Merkle proofs (versioned). */ library CheckpointLeaf { bytes1 internal constant PAYMENT_LEAF_V1 = 0x01; bytes1 internal constant PAYMENT_LEAF_V2 = 0x02; struct PaymentLeafV1 { bytes32 txHash; address from; address to; uint256 value; uint256 blockNumber; uint64 blockTimestamp; uint256 gasUsed; bool success; } /// @notice V2 adds optional ERC-20 token + log index for transfer-only batches struct PaymentLeafV2 { bytes32 txHash; address from; address to; address token; uint256 value; uint256 blockNumber; uint64 blockTimestamp; uint256 gasUsed; bool success; uint32 logIndex; } function paymentLeafV1( uint64 chainId, PaymentLeafV1 memory leaf ) internal pure returns (bytes32) { return keccak256( abi.encode( PAYMENT_LEAF_V1, chainId, leaf.txHash, leaf.from, leaf.to, leaf.value, leaf.blockNumber, leaf.blockTimestamp, leaf.gasUsed, leaf.success ) ); } function paymentLeafV2(uint64 chainId, PaymentLeafV2 memory leaf) internal pure returns (bytes32) { return keccak256( abi.encode( PAYMENT_LEAF_V2, chainId, leaf.txHash, leaf.from, leaf.to, leaf.token, leaf.value, leaf.blockNumber, leaf.blockTimestamp, leaf.gasUsed, leaf.success, leaf.logIndex ) ); } function buildMerkleRoot(bytes32[] memory leaves) internal pure returns (bytes32) { require(leaves.length > 0, "empty"); if (leaves.length == 1) return leaves[0]; bytes32[] memory layer = leaves; while (layer.length > 1) { uint256 nextLen = (layer.length + 1) / 2; bytes32[] memory next = new bytes32[](nextLen); for (uint256 i = 0; i < layer.length; i += 2) { bytes32 a = layer[i]; bytes32 b = i + 1 < layer.length ? layer[i + 1] : a; next[i / 2] = _hashPair(a, b); } layer = next; } return layer[0]; } function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); } function verifyMerkle(bytes32 root, bytes32 leaf, bytes32[] memory proof) internal pure returns (bool) { bytes32 computed = leaf; for (uint256 i = 0; i < proof.length; i++) { bytes32 p = proof[i]; computed = computed < p ? keccak256(abi.encodePacked(computed, p)) : keccak256(abi.encodePacked(p, computed)); } return computed == root; } }