From 50f8045ca2cf8c46e41c23a7c7831f2fe22a915d Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 21 Feb 2025 16:54:17 +0100 Subject: [PATCH] feat: add cl proof verifier --- contracts/0.8.25/CLProofVerifier.sol | 169 ++++++++ contracts/0.8.25/lib/GIndex.sol | 117 ++++++ contracts/0.8.25/lib/SSZ.sol | 262 +++++++++++++ contracts/0.8.25/lib/Types.sol | 42 ++ hardhat.config.ts | 10 + test/0.8.25/beaconBlockRoot.ts | 23 ++ test/0.8.25/clProofVerifier.test.ts | 556 +++++++++++++++++++++++++++ test/0.8.25/contracts/Utilities.sol | 93 +++++ test/0.8.25/lib/GIndex.t.sol | 352 +++++++++++++++++ test/0.8.25/lib/SSZ.t.sol | 370 ++++++++++++++++++ 10 files changed, 1994 insertions(+) create mode 100644 contracts/0.8.25/CLProofVerifier.sol create mode 100644 contracts/0.8.25/lib/GIndex.sol create mode 100644 contracts/0.8.25/lib/SSZ.sol create mode 100644 contracts/0.8.25/lib/Types.sol create mode 100644 test/0.8.25/beaconBlockRoot.ts create mode 100644 test/0.8.25/clProofVerifier.test.ts create mode 100644 test/0.8.25/contracts/Utilities.sol create mode 100644 test/0.8.25/lib/GIndex.t.sol create mode 100644 test/0.8.25/lib/SSZ.t.sol diff --git a/contracts/0.8.25/CLProofVerifier.sol b/contracts/0.8.25/CLProofVerifier.sol new file mode 100644 index 000000000..d3d9892c4 --- /dev/null +++ b/contracts/0.8.25/CLProofVerifier.sol @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +import {BeaconBlockHeader, Slot, Validator} from "./lib/Types.sol"; +import {GIndex} from "./lib/GIndex.sol"; +import {SSZ} from "./lib/SSZ.sol"; + +struct ValidatorWitness { + uint64 validatorIndex; + Validator validator; + bytes32[] validatorProof; +} + +struct ProvableBeaconBlockHeader { + BeaconBlockHeader header; // Header of a block which root is a root at rootsTimestamp. + uint64 rootsTimestamp; // To be passed to the EIP-4788 block roots contract. +} + +// A witness for a block header which root is accessible via `historical_summaries` field. +struct HistoricalHeaderWitness { + BeaconBlockHeader header; + GIndex rootGIndex; + bytes32[] proof; +} + +contract CLProofVerifier { + using SSZ for Validator; + using SSZ for BeaconBlockHeader; + + // See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788. + address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + uint64 public immutable SLOTS_PER_EPOCH; + + /// @dev This index is relative to a state like: `BeaconState.validators[0]`. + GIndex public immutable GI_FIRST_VALIDATOR_PREV; + + /// @dev This index is relative to a state like: `BeaconState.validators[0]`. + GIndex public immutable GI_FIRST_VALIDATOR_CURR; + + /// @dev This index is relative to a state like: `BeaconState.historical_summaries`. + GIndex public immutable GI_HISTORICAL_SUMMARIES_PREV; + + /// @dev This index is relative to a state like: `BeaconState.historical_summaries`. + GIndex public immutable GI_HISTORICAL_SUMMARIES_CURR; + + /// @dev The very first slot the verifier is supposed to accept proofs for. + Slot public immutable FIRST_SUPPORTED_SLOT; + + /// @dev The first slot of the currently compatible fork. + Slot public immutable PIVOT_SLOT; + + error RootNotFound(); + error InvalidGIndex(); + error InvalidBlockHeader(); + error UnsupportedSlot(Slot slot); + error InvalidPivotSlot(); + + /// @dev The previous and current forks can be essentially the same. + constructor( + GIndex gIFirstValidatorPrev, + GIndex gIFirstValidatorCurr, + GIndex gIHistoricalSummariesPrev, + GIndex gIHistoricalSummariesCurr, + Slot firstSupportedSlot, + Slot pivotSlot + ) { + if (firstSupportedSlot > pivotSlot) revert InvalidPivotSlot(); + + GI_FIRST_VALIDATOR_PREV = gIFirstValidatorPrev; + GI_FIRST_VALIDATOR_CURR = gIFirstValidatorCurr; + + GI_HISTORICAL_SUMMARIES_PREV = gIHistoricalSummariesPrev; + GI_HISTORICAL_SUMMARIES_CURR = gIHistoricalSummariesCurr; + + FIRST_SUPPORTED_SLOT = firstSupportedSlot; + PIVOT_SLOT = pivotSlot; + } + + /// @notice Verify withdrawal proof and report withdrawal to the module for valid proofs + /// @param beaconBlock Beacon block header + function verifyValidatorProof( + ProvableBeaconBlockHeader calldata beaconBlock, + ValidatorWitness calldata witness + ) external view { + if (beaconBlock.header.slot < FIRST_SUPPORTED_SLOT) { + revert UnsupportedSlot(beaconBlock.header.slot); + } + + { + bytes32 trustedHeaderRoot = _getParentBlockRoot(beaconBlock.rootsTimestamp); + if (trustedHeaderRoot != beaconBlock.header.hashTreeRoot()) { + revert InvalidBlockHeader(); + } + } + + SSZ.verifyProof({ + proof: witness.validatorProof, + root: beaconBlock.header.stateRoot, + leaf: witness.validator.hashTreeRoot(), + gI: _getValidatorGI(witness.validatorIndex, beaconBlock.header.slot) + }); + } + + /// @notice Verify withdrawal proof against historical summaries data and report withdrawal to the module for valid proofs + /// @param beaconBlock Beacon block header + /// @param oldBlock Historical block header witness + function verifyHistoricalValidatorProof( + ProvableBeaconBlockHeader calldata beaconBlock, + HistoricalHeaderWitness calldata oldBlock, + ValidatorWitness calldata witness + ) external view { + if (beaconBlock.header.slot < FIRST_SUPPORTED_SLOT) { + revert UnsupportedSlot(beaconBlock.header.slot); + } + + if (oldBlock.header.slot < FIRST_SUPPORTED_SLOT) { + revert UnsupportedSlot(oldBlock.header.slot); + } + + { + bytes32 trustedHeaderRoot = _getParentBlockRoot(beaconBlock.rootsTimestamp); + if (trustedHeaderRoot != beaconBlock.header.hashTreeRoot()) { + revert InvalidBlockHeader(); + } + } + + // It's up to a user to provide a valid generalized index of a historical block root in a summaries list. + // Ensuring the provided generalized index is for a node somewhere below the historical_summaries root. + if (!_getHistoricalSummariesGI(beaconBlock.header.slot).isParentOf(oldBlock.rootGIndex)) { + revert InvalidGIndex(); + } + + SSZ.verifyProof({ + proof: oldBlock.proof, + root: beaconBlock.header.stateRoot, + leaf: oldBlock.header.hashTreeRoot(), + gI: oldBlock.rootGIndex + }); + + SSZ.verifyProof({ + proof: witness.validatorProof, + root: oldBlock.header.stateRoot, + leaf: witness.validator.hashTreeRoot(), + gI: _getValidatorGI(witness.validatorIndex, oldBlock.header.slot) + }); + } + + function _getParentBlockRoot(uint64 blockTimestamp) internal view returns (bytes32) { + (bool success, bytes memory data) = BEACON_ROOTS.staticcall(abi.encode(blockTimestamp)); + + if (!success || data.length == 0) { + revert RootNotFound(); + } + + return abi.decode(data, (bytes32)); + } + + function _getValidatorGI(uint256 offset, Slot stateSlot) internal view returns (GIndex) { + GIndex gI = stateSlot < PIVOT_SLOT ? GI_FIRST_VALIDATOR_PREV : GI_FIRST_VALIDATOR_CURR; + return gI.shr(offset); + } + + function _getHistoricalSummariesGI(Slot stateSlot) internal view returns (GIndex) { + return stateSlot < PIVOT_SLOT ? GI_HISTORICAL_SUMMARIES_PREV : GI_HISTORICAL_SUMMARIES_CURR; + } +} diff --git a/contracts/0.8.25/lib/GIndex.sol b/contracts/0.8.25/lib/GIndex.sol new file mode 100644 index 000000000..613e85d3c --- /dev/null +++ b/contracts/0.8.25/lib/GIndex.sol @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +type GIndex is bytes32; + +using {isRoot, isParentOf, index, width, shr, shl, concat, unwrap, pow} for GIndex global; + +error IndexOutOfRange(); + +/// @param gI Is a generalized index of a node in a tree. +/// @param p Is a power of a tree level the node belongs to. +/// @return GIndex +function pack(uint256 gI, uint8 p) pure returns (GIndex) { + if (gI > type(uint248).max) { + revert IndexOutOfRange(); + } + + // NOTE: We can consider adding additional metadata like a fork version. + return GIndex.wrap(bytes32((gI << 8) | p)); +} + +function unwrap(GIndex self) pure returns (bytes32) { + return GIndex.unwrap(self); +} + +function isRoot(GIndex self) pure returns (bool) { + return index(self) == 1; +} + +function index(GIndex self) pure returns (uint256) { + return uint256(unwrap(self)) >> 8; +} + +function width(GIndex self) pure returns (uint256) { + return 1 << pow(self); +} + +function pow(GIndex self) pure returns (uint8) { + return uint8(uint256(unwrap(self))); +} + +/// @return Generalized index of the nth neighbor of the node to the right. +function shr(GIndex self, uint256 n) pure returns (GIndex) { + uint256 i = index(self); + uint256 w = width(self); + + if ((i % w) + n >= w) { + revert IndexOutOfRange(); + } + + return pack(i + n, pow(self)); +} + +/// @return Generalized index of the nth neighbor of the node to the left. +function shl(GIndex self, uint256 n) pure returns (GIndex) { + uint256 i = index(self); + uint256 w = width(self); + + if (i % w < n) { + revert IndexOutOfRange(); + } + + return pack(i - n, pow(self)); +} + +// See https://github.com/protolambda/remerkleable/blob/91ed092d08ef0ba5ab076f0a34b0b371623db728/remerkleable/tree.py#L46 +function concat(GIndex lhs, GIndex rhs) pure returns (GIndex) { + uint256 lhsMSbIndex = fls(index(lhs)); + uint256 rhsMSbIndex = fls(index(rhs)); + + if (lhsMSbIndex + 1 + rhsMSbIndex > 248) { + revert IndexOutOfRange(); + } + + return pack((index(lhs) << rhsMSbIndex) | (index(rhs) ^ (1 << rhsMSbIndex)), pow(rhs)); +} + +function isParentOf(GIndex self, GIndex child) pure returns (bool) { + uint256 parentIndex = index(self); + uint256 childIndex = index(child); + + if (parentIndex >= childIndex) { + return false; + } + + while (childIndex > 0) { + if (childIndex == parentIndex) { + return true; + } + + childIndex = childIndex >> 1; + } + + return false; +} + +/// @dev From Solady LibBit, see https://github.com/Vectorized/solady/blob/main/src/utils/LibBit.sol. +/// @dev Find last set. +/// Returns the index of the most significant bit of `x`, +/// counting from the least significant bit position. +/// If `x` is zero, returns 256. +function fls(uint256 x) pure returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + // prettier-ignore + r := or(shl(8, iszero(x)), shl(7, lt(0xffffffffffffffffffffffffffffffff, x))) + r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffff, shr(r, x)))) + r := or(r, shl(3, lt(0xff, shr(r, x)))) + // prettier-ignore + r := or(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)), + 0x0706060506020504060203020504030106050205030304010505030400000000)) + } +} diff --git a/contracts/0.8.25/lib/SSZ.sol b/contracts/0.8.25/lib/SSZ.sol new file mode 100644 index 000000000..b0c1fe4ff --- /dev/null +++ b/contracts/0.8.25/lib/SSZ.sol @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +import {BeaconBlockHeader, Validator} from "./Types.sol"; +import {GIndex} from "./GIndex.sol"; + +library SSZ { + error BranchHasMissingItem(); + error BranchHasExtraItem(); + error InvalidProof(); + + function hashTreeRoot(BeaconBlockHeader memory header) internal view returns (bytes32 root) { + bytes32[8] memory nodes = [ + toLittleEndian(header.slot.unwrap()), + toLittleEndian(header.proposerIndex), + header.parentRoot, + header.stateRoot, + header.bodyRoot, + bytes32(0), + bytes32(0), + bytes32(0) + ]; + + /// @solidity memory-safe-assembly + assembly { + // Count of nodes to hash + let count := 8 + + // Loop over levels + // prettier-ignore + for { } 1 { } { + // Loop over nodes at the given depth + + // Initialize `offset` to the offset of `proof` elements in memory. + let target := nodes + let source := nodes + let end := add(source, shl(5, count)) + + // prettier-ignore + for { } 1 { } { + // Read next two hashes to hash + mcopy(0x00, source, 0x40) + + // Call sha256 precompile + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + // Store the resulting hash at the target location + mstore(target, mload(0x00)) + + // Advance the pointers + target := add(target, 0x20) + source := add(source, 0x40) + + if iszero(lt(source, end)) { + break + } + } + + count := shr(1, count) + if eq(count, 1) { + root := mload(0x00) + break + } + } + } + } + + function hashTreeRoot(Validator memory validator) internal view returns (bytes32 root) { + bytes32 pubkeyRoot; + + assembly { + // Dynamic data types such as bytes are stored at the specified offset. + let offset := mload(validator) + // Copy the pubkey to the scratch space. + mcopy(0x00, add(offset, 32), 48) + // Clear the last 16 bytes. + mcopy(48, 0x60, 16) + // Call sha256 precompile. + let result := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + pubkeyRoot := mload(0x00) + } + + bytes32[8] memory nodes = [ + pubkeyRoot, + validator.withdrawalCredentials, + toLittleEndian(validator.effectiveBalance), + toLittleEndian(validator.slashed), + toLittleEndian(validator.activationEligibilityEpoch), + toLittleEndian(validator.activationEpoch), + toLittleEndian(validator.exitEpoch), + toLittleEndian(validator.withdrawableEpoch) + ]; + + /// @solidity memory-safe-assembly + assembly { + // Count of nodes to hash + let count := 8 + + // Loop over levels + // prettier-ignore + for { } 1 { } { + // Loop over nodes at the given depth + + // Initialize `offset` to the offset of `proof` elements in memory. + let target := nodes + let source := nodes + let end := add(source, shl(5, count)) + + // prettier-ignore + for { } 1 { } { + // Read next two hashes to hash + mcopy(0x00, source, 0x40) + + // Call sha256 precompile + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + // Store the resulting hash at the target location + mstore(target, mload(0x00)) + + // Advance the pointers + target := add(target, 0x20) + source := add(source, 0x40) + + if iszero(lt(source, end)) { + break + } + } + + count := shr(1, count) + if eq(count, 1) { + root := mload(0x00) + break + } + } + } + } + + /// @notice Modified version of `verify` from Solady `MerkleProofLib` to support generalized indices and sha256 precompile. + /// @dev Reverts if `leaf` doesn't exist in the Merkle tree with `root`, given `proof`. + function verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, GIndex gI) internal view { + uint256 index = gI.index(); + + /// @solidity memory-safe-assembly + assembly { + // Check if `proof` is empty. + if iszero(proof.length) { + // revert InvalidProof() + mstore(0x00, 0x09bde339) + revert(0x1c, 0x04) + } + // Left shift by 5 is equivalent to multiplying by 0x20. + let end := add(proof.offset, shl(5, proof.length)) + // Initialize `offset` to the offset of `proof` in the calldata. + let offset := proof.offset + // Iterate over proof elements to compute root hash. + // prettier-ignore + for { } 1 { } { + // Slot of `leaf` in scratch space. + // If the condition is true: 0x20, otherwise: 0x00. + let scratch := shl(5, and(index, 1)) + index := shr(1, index) + if iszero(index) { + // revert BranchHasExtraItem() + mstore(0x00, 0x5849603f) + // 0x1c = 28 => offset in 32-byte word of a slot 0x00 + revert(0x1c, 0x04) + } + // Store elements to hash contiguously in scratch space. + // Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes. + mstore(scratch, leaf) + mstore(xor(scratch, 0x20), calldataload(offset)) + // Call sha256 precompile. + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompile returns no data on OutOfGas error. + revert(0, 0) + } + + // Reuse `leaf` to store the hash to reduce stack operations. + leaf := mload(0x00) + offset := add(offset, 0x20) + if iszero(lt(offset, end)) { + break + } + } + + if iszero(eq(index, 1)) { + // revert BranchHasMissingItem() + mstore(0x00, 0x1b6661c3) + revert(0x1c, 0x04) + } + + if iszero(eq(leaf, root)) { + // revert InvalidProof() + mstore(0x00, 0x09bde339) + revert(0x1c, 0x04) + } + } + } + + // See https://github.com/succinctlabs/telepathy-contracts/blob/5aa4bb7/src/libraries/SimpleSerialize.sol#L17-L28 + function toLittleEndian(uint256 v) internal pure returns (bytes32) { + v = + ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + v = + ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + v = + ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + v = + ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) | + ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + v = (v >> 128) | (v << 128); + return bytes32(v); + } + + function toLittleEndian(bool v) internal pure returns (bytes32) { + return bytes32(v ? 1 << 248 : 0); + } +} diff --git a/contracts/0.8.25/lib/Types.sol b/contracts/0.8.25/lib/Types.sol new file mode 100644 index 000000000..e69565cbc --- /dev/null +++ b/contracts/0.8.25/lib/Types.sol @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +// As defined in phase0/beacon-chain.md:159 +type Slot is uint64; + +function unwrap(Slot slot) pure returns (uint64) { + return Slot.unwrap(slot); +} + +function gt(Slot lhs, Slot rhs) pure returns (bool) { + return lhs.unwrap() > rhs.unwrap(); +} + +function lt(Slot lhs, Slot rhs) pure returns (bool) { + return lhs.unwrap() < rhs.unwrap(); +} + +using {unwrap, lt as <, gt as >} for Slot global; + +// As defined in phase0/beacon-chain.md:356 +struct Validator { + bytes pubkey; + bytes32 withdrawalCredentials; + uint64 effectiveBalance; + bool slashed; + uint64 activationEligibilityEpoch; + uint64 activationEpoch; + uint64 exitEpoch; + uint64 withdrawableEpoch; +} + +// As defined in phase0/beacon-chain.md:436 +struct BeaconBlockHeader { + Slot slot; + uint64 proposerIndex; + bytes32 parentRoot; + bytes32 stateRoot; + bytes32 bodyRoot; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index e046e8fdb..06736592e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -111,6 +111,16 @@ const config: HardhatUserConfig = { evmVersion: "istanbul", }, }, + { + version: "0.8.25", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + evmVersion: "cancun", + }, + }, ], }, tracer: { diff --git a/test/0.8.25/beaconBlockRoot.ts b/test/0.8.25/beaconBlockRoot.ts new file mode 100644 index 000000000..6e71f8163 --- /dev/null +++ b/test/0.8.25/beaconBlockRoot.ts @@ -0,0 +1,23 @@ +import { impersonate } from "lib"; + +// Address of the Beacon Block Storage contract, which exposes beacon chain roots. +// This corresponds to `BEACON_ROOTS_ADDRESS` as specified in EIP-4788. +export const BEACON_BLOCK_STORAGE_CONTRACT = "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02"; + +export const updateBeaconBlockRoot = async (root: string): Promise => { + const beaconRootUpdater = await impersonate( + "0xfffffffffffffffffffffffffffffffffffffffe", + 999999999999999999999999999n, + ); + + const transaction = await beaconRootUpdater.sendTransaction({ + to: BEACON_BLOCK_STORAGE_CONTRACT, + value: 0, + data: root, + }); + + const blockDetails = await transaction.getBlock(); + if (!blockDetails) throw new Error("Failed to retrieve block details."); + + return blockDetails.timestamp; +}; diff --git a/test/0.8.25/clProofVerifier.test.ts b/test/0.8.25/clProofVerifier.test.ts new file mode 100644 index 000000000..bbb64cef9 --- /dev/null +++ b/test/0.8.25/clProofVerifier.test.ts @@ -0,0 +1,556 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { CLProofVerifier } from "typechain-types"; + +import { Snapshot } from "test/suite"; + +import { updateBeaconBlockRoot } from "./beaconBlockRoot"; + +describe("CLProofVerifier.sol", () => { + let originalState: string; + + beforeEach(async () => { + originalState = await Snapshot.take(); + }); + + afterEach(async () => { + await Snapshot.restore(originalState); + }); + + describe("CLProofVerifier Constructor", () => { + const GI_FIRST_VALIDATOR_PREV = `0x${"1".repeat(64)}`; + const GI_FIRST_VALIDATOR_CURR = `0x${"2".repeat(64)}`; + const GI_HISTORICAL_SUMMARIES_PREV = `0x${"3".repeat(64)}`; + const GI_HISTORICAL_SUMMARIES_CURR = `0x${"4".repeat(64)}`; + const FIRST_SUPPORTED_SLOT = 1; + const PIVOT_SLOT = 2; + + let clProofVerifier: CLProofVerifier; + + before(async () => { + clProofVerifier = await ethers.deployContract("CLProofVerifier", [ + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_HISTORICAL_SUMMARIES_PREV, + GI_HISTORICAL_SUMMARIES_CURR, + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + ]); + }); + + it("sets all parameters correctly correctly", async () => { + expect(await clProofVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); + expect(await clProofVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); + expect(await clProofVerifier.GI_HISTORICAL_SUMMARIES_PREV()).to.equal(GI_HISTORICAL_SUMMARIES_PREV); + expect(await clProofVerifier.GI_HISTORICAL_SUMMARIES_CURR()).to.equal(GI_HISTORICAL_SUMMARIES_CURR); + expect(await clProofVerifier.FIRST_SUPPORTED_SLOT()).to.equal(FIRST_SUPPORTED_SLOT); + expect(await clProofVerifier.PIVOT_SLOT()).to.equal(PIVOT_SLOT); + }); + + it("reverts with 'InvalidPivotSlot' if firstSupportedSlot > pivotSlot", async () => { + await expect( + ethers.deployContract("CLProofVerifier", [ + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_HISTORICAL_SUMMARIES_PREV, + GI_HISTORICAL_SUMMARIES_CURR, + 200_000, // firstSupportedSlot + 100_000, // pivotSlot < firstSupportedSlot + ]), + ).to.be.revertedWithCustomError(clProofVerifier, "InvalidPivotSlot"); + }); + }); + + describe("verifyValidatorProof method", () => { + const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000056000000000028"; + const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000003b00"; + + const VALIDATOR_PROOF = { + blockRoot: "0x56073a5bf24e8a3ea2033ad10a5039a7a7a6884086b67053c90d38f104ae89cf", + + beaconBlockHeader: { + slot: 1743359, + proposerIndex: 1337, + parentRoot: "0x5db6dfb2b5e735bafb437a76b9e525e958d2aef589649e862bfbc02964edf5ab", + stateRoot: "0x21205c716572ae05692c0f8a4c64fd84e504cbb1a16fa0371701adbab756dd72", + bodyRoot: "0x459390eed4479eb49b71efadcc3b540bbc60073f196e0409588d6cc9eafbe5fa", + }, + witness: { + validatorIndex: 1551477n, + validator: { + pubkey: "0xa5b3dfbe60eb74b9224ec56bb253e18cf032c999818f10bc51fc13a9c5584eb66624796a400c2047ac248146f58a2d3d", + withdrawalCredentials: "0x010000000000000000000000c93c3e1c11037f5bd50f21cfc1a02aba5427b2f3", + effectiveBalance: 0n, + activationEligibilityEpoch: 21860n, + activationEpoch: 21866n, + exitEpoch: 41672n, + withdrawableEpoch: 41928n, + slashed: true, + }, + validatorProof: [ + "0x3efdddf56d4e2f27814f3c7a33242b208eba5496d4375ae1354189cb45022265", + "0xa80637a489bc503b27c5b8667d7147ed1c52f945d52aae090d1911941ba3bc0a", + "0x55437fead4a169949a4686ee6d0d7777d0006000439d01e8f1ff86ed3b944555", + "0x1ded2cca8f4b1667158ee2db6c5bc13488283921d0bc19ee870e9e96182e8ab9", + "0x6e8978026de507444dff6c59d0159f56ac57bc0d838b0060c81547de5e4c57b8", + "0x3a01de7f6c7c3840419cf3fcf7910d791e0d7ef471288331d5fe56398b7f1b3f", + "0x1bfe62a72cfbcef5a057464913e141d625ecf04eaa34c3c85e047a32a7b28ec8", + "0x31129869b19b584b2032d8b3fa901ec86ca3213983620a2e085b14506a53b9b6", + "0xb010816d1a36de59273332db53d2c20eb91a07b8c5327790a1d2c6cdbe9cdeba", + "0x9acaa36e34da8ba19c54d7b9f6d9e5740febc1b30b61cb19d0891e79c2642243", + "0x43c6392e38689b6666857ed9dba67b486421dce3824878abd891107ff2b62757", + "0xe38fab163d8350d6ffd316794bfb000d97a72c85eccc4062e80308e94a9939d8", + "0x96428f8477bf31469220152f22fb9c321e74aa08774dd5b4de6d11e8fc23d272", + "0x384a25acafbec9f1c547eb89766051cf16cb4fd4d49a7ddadf7bd32e01ef4489", + "0x4c82fe5eca765bbd31dae8cb40b2229526d89c64205a5d5048551dfd9f0215c6", + "0x552980838151f3db4e1e3e69689b481f784f947a147ec9b2df4f6d9d1eaf1147", + "0xa527b49b664e1311993cb4d5d77c8e3ef9bbe06b142e76f1035a5768b1443c79", + "0x889f02af50613a82f8e1ed3f654bf1f829c58e4cd1d67bf608793cfe80ec6165", + "0xbc676437f6c3c377e4aac6eb1a73c19e6a35db70a44604d791172912b23e2b8e", + "0x06a06bbdd7f1700337393726ed1ca6e63a5a591607dcacf1766119753ec81292", + "0xef1b63eac20336d5cd32028b1963f7c80869ae34ba13ece0965c51540abc1709", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0xcb2c1a0000000000000000000000000000000000000000000000000000000000", + "0xbc36040000000000000000000000000000000000000000000000000000000000", + "0x0ed6189bc73badc7cf2cd2f0e54551a3b1d2192ee26bbb58d670d069b31b148e", + "0x80eb44447d4f078e878a8b5fd2e3d3833a368e1d12239503e9f7b4605a0d782a", + "0xbb2952772995323016b98233c26e96e5c54955fda62e643cb56981da6aab7365", + "0xda5ca7afba0d19d345e85d2825fc3078eefdd76ead776b108fe0eac9aa96e5e6", + ], + }, + }; + + let clProofVerifier: CLProofVerifier; + + before(async () => { + clProofVerifier = await ethers.deployContract("CLProofVerifier", [ + GI_FIRST_VALIDATOR_INDEX, // GI_FIRST_VALIDATOR_PREV + GI_FIRST_VALIDATOR_INDEX, // GI_FIRST_VALIDATOR_CURR + GI_HISTORICAL_SUMMARIES_INDEX, // GI_HISTORICAL_SUMMARIES_PREV + GI_HISTORICAL_SUMMARIES_INDEX, // GI_HISTORICAL_SUMMARIES_CURR + 100_500, // FIRST_SUPPORTED_SLOT + 100_501, // PIVOT_SLOT + ]); + }); + + it("accepts a valid proof and does not revert", async () => { + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + + await expect( + clProofVerifier.verifyValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + VALIDATOR_PROOF.witness, + ), + ).not.to.be.reverted; + }); + + it("reverts with 'UnsupportedSlot' when slot < FIRST_SUPPORTED_SLOT", async () => { + // Use a slot smaller than 100_500 + const invalidHeader = { + ...VALIDATOR_PROOF.beaconBlockHeader, + slot: 99_999, + }; + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + + await expect( + clProofVerifier.verifyValidatorProof( + { + rootsTimestamp: timestamp, + header: invalidHeader, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'RootNotFound' if the staticcall to the block roots contract fails/returns empty", async () => { + const badTimestamp = 999_999_999; + await expect( + clProofVerifier.verifyValidatorProof( + { + rootsTimestamp: badTimestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "RootNotFound"); + }); + + it("reverts with 'InvalidBlockHeader' if the block root from contract doesn't match the header root", async () => { + const bogusBlockRoot = "0xbadbadbad0000000000000000000000000000000000000000000000000000000"; + const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); + + await expect( + clProofVerifier.verifyValidatorProof( + { + rootsTimestamp: mismatchTimestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "InvalidBlockHeader"); + }); + + it("reverts if the validator proof is incorrect", async () => { + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + + // Mutate one proof entry to break it + const badWitness = { + ...VALIDATOR_PROOF.witness, + validatorProof: [ + ...VALIDATOR_PROOF.witness.validatorProof.slice(0, -1), + "0xbadbadbad0000000000000000000000000000000000000000000000000000000", // corrupt last entry + ], + }; + + await expect( + clProofVerifier.verifyValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + badWitness, + ), + ).to.be.reverted; + }); + }); + + describe("verifyHistoricalValidatorProof method", () => { + const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000056000000000028"; + const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000003b00"; + + const VALIDATOR_PROOF = { + blockRoot: "0x657c451abdfbae5c4a18c699a87ecdb76e00d7ef9af9c7fcf3d7c4a700dad12d", + beaconBlockHeader: { + slot: 6073654, + proposerIndex: 31415, + parentRoot: "0x24265b525422ca972dfb33372a20a8ce241e4726d6920028b409841664fba54c", + stateRoot: "0x5d5c80b9b03018142083eac6da1433da370037d0c4f9ba6dfa586c689ce270dc", + bodyRoot: "0xe14a6474295a5cafbd30acec9347e21ba4c03fe96c5add4b96b468b4b1e69154", + }, + oldBlock: { + beaconBlockHeader: { + slot: 1743359, + proposerIndex: 1337, + parentRoot: "0x5db6dfb2b5e735bafb437a76b9e525e958d2aef589649e862bfbc02964edf5ab", + stateRoot: "0x7872be65d584621635a0192bd4424a5b30a24e1a9096f1aac5e8e6f046bf49bd", + bodyRoot: "0xe14a6474295a5cafbd30acec9347e21ba4c03fe96c5add4b96b468b4b1e69154", + }, + rootGIndex: "0x000000000000000000000000000000000000000000000000000000ec00000000", + proof: [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0100000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x2658397f87f190d84814e4595b3ec8eb0110ab5be675d59434d5a3dfd5ef760d", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xe537052d30df4f0436cd5a3c5debd331c770d9df46da47e0e3db74906186fa09", + "0x4616e1d9312a92eb228e8cd5483fa1fca64d99781d62129bc53718d194b98c45", + ], + }, + witness: { + validatorIndex: 673610, + validator: { + pubkey: "0xa6e2ebcef8e8aa149ee3a0d4cfafdfb0e592914038c38d81b174cab83ba3f9c3dcf4d10776cd8c25e7729204db5f145f", + withdrawalCredentials: "0x010000000000000000000000b3e29c46ee1745724417c0c51eb2351a1c01cf36", + effectiveBalance: 32000000000, + activationEligibilityEpoch: 21860, + activationEpoch: 21866, + exitEpoch: 41672, + withdrawableEpoch: 41928, + slashed: false, + }, + validatorProof: [ + "0x9a38ebbd300b757b903b0508c0319a24a2085e6ea32477b0ccf0fcfdaae57ffa", + "0x9a80eeb8748d92854d659b31862663afd95a7d0b5c58784e390b96cc5cf8a050", + "0x894ecc352a325a00ef1b4607bdfe20c811f894ebba68b428294c60d701f56adf", + "0xfb5ac36fdbd1aa8d349bc3ab269835ecace76787882f276e1188923a7e97aa15", + "0x5ec5527e0e52856e679ffa5a3055cba1ceafcddf295eb0c62e6972d4388e7472", + "0x7da3ff199f7e66789fc0acedda61e3b9ba4d444770e8c962736bba32e121c9da", + "0x47540c45db936f74c0c0ad745588b465fa26eca7d6d6d44782dd3da7b28bc1e9", + "0x86ed60c17e59e0fadb5287b21f735dad27d1a4c8f8fe0b0e178c9b0b9db4e629", + "0xcf048e4d5f1099beef7cec38a8e7b7390592c9d6d295a4d53520e6b94989c80a", + "0xc9f5d836a2d0b701948097bc0ba77f48b3f4c7d19d6af14fb61ae75033f6529f", + "0x7cf412a36fdcd86ecc6c4af72f8e1b02c766cafca07d6589f95136231bce32c3", + "0x264c4483d374885d55c21f1902f86f9fd85c3c4381522e1e4a3a61e6a3fd700c", + "0xce5d7735c68dae371b4d72732cdb5ea1349f099d20f234193a27229dda008945", + "0x593fb43e4d9b4e42766a9caca516a924daaad074145562fe207711b419e617e2", + "0x12d47e17c665da9a0c9cda0fd030924285138901f4f9834c566b9c015a0a1344", + "0x72f9ff98530f077ee309ed7e2b1d6cc0403521ac0ee52aadee3ad4d6599d24b5", + "0xc8d966c9cc238f1c1f47f381f8395cf071ab7bd2107735040df853eb9ce644c7", + "0xda8d7b1ca5594f8b0bd8b61722c230a9a5b3832e71c7f0747fd7db63e5ed9adb", + "0xce27e7bd7b49d82e19110527914bdf8c6206ea517ca6acd1694810c03a1abee1", + "0x3adb047395ba4b2d70ccc75bceffc288d7eb3fcb4bf7e2e15ed0c604d794ba4b", + "0x275e385218e4241e9714c0e5da831c375c82630d4723be82544acaa4f598ee4d", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0xcb2c1a0000000000000000000000000000000000000000000000000000000000", + "0xbc36040000000000000000000000000000000000000000000000000000000000", + "0x0ed6189bc73badc7cf2cd2f0e54551a3b1d2192ee26bbb58d670d069b31b148e", + "0x80eb44447d4f078e878a8b5fd2e3d3833a368e1d12239503e9f7b4605a0d782a", + "0xbb2952772995323016b98233c26e96e5c54955fda62e643cb56981da6aab7365", + "0x2b790b51060aad6c32c0e5a8e95e4ea6b0c2c0171bd54ceae9ce10a7ef144fa2", + ], + }, + }; + + let clProofVerifier: CLProofVerifier; + + before(async () => { + clProofVerifier = await ethers.deployContract("CLProofVerifier", [ + GI_FIRST_VALIDATOR_INDEX, // GI_FIRST_VALIDATOR_PREV + GI_FIRST_VALIDATOR_INDEX, // GI_FIRST_VALIDATOR_CURR + GI_HISTORICAL_SUMMARIES_INDEX, // GI_HISTORICAL_SUMMARIES_PREV + GI_HISTORICAL_SUMMARIES_INDEX, // GI_HISTORICAL_SUMMARIES_CURR + 100_500, // FIRST_SUPPORTED_SLOT + 100_501, // PIVOT_SLOT + ]); + }); + + it("accepts a valid proof and does not revert", async () => { + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + rootGIndex: VALIDATOR_PROOF.oldBlock.rootGIndex, + proof: VALIDATOR_PROOF.oldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).not.to.be.reverted; + }); + + it("reverts with 'UnsupportedSlot' if beaconBlock slot < FIRST_SUPPORTED_SLOT", async () => { + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + const invalidHeader = { + ...VALIDATOR_PROOF.beaconBlockHeader, + slot: 50_000, + }; + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: timestamp, + header: invalidHeader, + }, + { + header: VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + rootGIndex: VALIDATOR_PROOF.oldBlock.rootGIndex, + proof: VALIDATOR_PROOF.oldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'UnsupportedSlot' if oldBlock slot < FIRST_SUPPORTED_SLOT", async () => { + const oldBlock = { + ...VALIDATOR_PROOF.oldBlock, + beaconBlockHeader: { + ...VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + slot: 99_999, + }, + }; + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: oldBlock.beaconBlockHeader, + rootGIndex: oldBlock.rootGIndex, + proof: oldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'RootNotFound' if block root contract call fails", async () => { + const badTimestamp = 999_999_999; + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: badTimestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + rootGIndex: VALIDATOR_PROOF.oldBlock.rootGIndex, + proof: VALIDATOR_PROOF.oldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "RootNotFound"); + }); + + it("reverts with 'InvalidBlockHeader' if returned root doesn't match the new block header root", async () => { + // Deploy a mismatch root in the mock + const bogusBlockRoot = "0xbadbadbad0000000000000000000000000000000000000000000000000000000"; + const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: mismatchTimestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + rootGIndex: VALIDATOR_PROOF.oldBlock.rootGIndex, + proof: VALIDATOR_PROOF.oldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "InvalidBlockHeader"); + }); + + it("reverts with 'InvalidGIndex' if oldBlock.rootGIndex is not under the historicalSummaries root", async () => { + // Provide an obviously wrong rootGIndex that won't match the parent's + const invalidRootGIndex = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + rootGIndex: invalidRootGIndex, + proof: VALIDATOR_PROOF.oldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.revertedWithCustomError(clProofVerifier, "InvalidGIndex"); + }); + + it("reverts if the oldBlock proof is corrupted", async () => { + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + // Mutate one proof entry to break the historical block proof + const badOldBlock = { + ...VALIDATOR_PROOF.oldBlock, + proof: [ + ...VALIDATOR_PROOF.oldBlock.proof.slice(0, -1), + "0xbadbadbad0000000000000000000000000000000000000000000000000000000", + ], + }; + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: badOldBlock.beaconBlockHeader, + rootGIndex: badOldBlock.rootGIndex, + proof: badOldBlock.proof, + }, + VALIDATOR_PROOF.witness, + ), + ).to.be.reverted; + }); + + it("reverts if the validatorProof in the witness is corrupted", async () => { + const timestamp = await updateBeaconBlockRoot(VALIDATOR_PROOF.blockRoot); + // Mutate the validator proof + const badWitness = { + ...VALIDATOR_PROOF.witness, + validatorProof: [ + ...VALIDATOR_PROOF.witness.validatorProof.slice(0, -1), + "0xbadbadbad0000000000000000000000000000000000000000000000000000000", + ], + }; + + await expect( + clProofVerifier.verifyHistoricalValidatorProof( + { + rootsTimestamp: timestamp, + header: VALIDATOR_PROOF.beaconBlockHeader, + }, + { + header: VALIDATOR_PROOF.oldBlock.beaconBlockHeader, + rootGIndex: VALIDATOR_PROOF.oldBlock.rootGIndex, + proof: VALIDATOR_PROOF.oldBlock.proof, + }, + badWitness, + ), + ).to.be.reverted; + }); + }); +}); diff --git a/test/0.8.25/contracts/Utilities.sol b/test/0.8.25/contracts/Utilities.sol new file mode 100644 index 000000000..8f0c2c091 --- /dev/null +++ b/test/0.8.25/contracts/Utilities.sol @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +contract Utilities { + error FreeMemoryPointerOverflowed(); + error ZeroSlotIsNotZero(); + + /// See https://github.com/Vectorized/solady - MIT licensed. + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + function _brutalizeMemory() private view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(offset, zero, calldatasize()) + + // Fill the 64 bytes of scratch space with garbage. + mstore(zero, add(caller(), gas())) + mstore(0x20, keccak256(offset, calldatasize())) + mstore(zero, keccak256(zero, 0x40)) + + let r0 := mload(zero) + let r1 := mload(0x20) + + let cSize := add(codesize(), iszero(codesize())) + if iszero(lt(cSize, 32)) { + cSize := sub(cSize, and(mload(0x02), 0x1f)) + } + let start := mod(mload(0x10), cSize) + let size := mul(sub(cSize, start), gt(cSize, start)) + let times := div(0x7ffff, cSize) + if iszero(lt(times, 128)) { + times := 128 + } + + // Occasionally offset the offset by a pseudorandom large amount. + // Can't be too large, or we will easily get out-of-gas errors. + offset := add(offset, mul(iszero(and(r1, 0xf)), and(r0, 0xfffff))) + + // Fill the free memory with garbage. + // prettier-ignore + for { let w := not(0) } 1 {} { + mstore(offset, r0) + mstore(add(offset, 0x20), r1) + offset := add(offset, 0x40) + // We use codecopy instead of the identity precompile + // to avoid polluting the `forge test -vvvv` output with tons of junk. + codecopy(offset, start, size) + codecopy(add(offset, size), 0, start) + offset := add(offset, cSize) + times := add(times, w) // `sub(times, 1)`. + if iszero(times) { break } + } + } + } + + /// See https://github.com/Vectorized/solady - MIT licensed. + /// @dev Check if the free memory pointer and the zero slot are not contaminated. + /// Useful for cases where these slots are used for temporary storage. + function _checkMemory() internal pure { + bool zeroSlotIsNotZero; + bool freeMemoryPointerOverflowed; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + // Test at a lower, but reasonable limit for more safety room. + if gt(mload(0x40), 0xffffffff) { + freeMemoryPointerOverflowed := 1 + } + // Check the value of the zero slot. + zeroSlotIsNotZero := mload(0x60) + } + if (freeMemoryPointerOverflowed) revert FreeMemoryPointerOverflowed(); + if (zeroSlotIsNotZero) revert ZeroSlotIsNotZero(); + } + + /// See https://github.com/Vectorized/solady - MIT licensed. + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeMemory() { + _brutalizeMemory(); + _; + _checkMemory(); + } +} diff --git a/test/0.8.25/lib/GIndex.t.sol b/test/0.8.25/lib/GIndex.t.sol new file mode 100644 index 000000000..be2e40801 --- /dev/null +++ b/test/0.8.25/lib/GIndex.t.sol @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {GIndex, pack, IndexOutOfRange, fls} from "../../../contracts/0.8.25/lib/GIndex.sol"; +import {SSZ} from "../../../contracts/0.8.25/lib/SSZ.sol"; + +// Wrap the library internal methods to make an actual call to them. +// Supposed to be used with `expectRevert` cheatcode. +contract Library { + function concat(GIndex lhs, GIndex rhs) public pure returns (GIndex) { + return lhs.concat(rhs); + } + + function shr(GIndex self, uint256 n) public pure returns (GIndex) { + return self.shr(n); + } + + function shl(GIndex self, uint256 n) public pure returns (GIndex) { + return self.shl(n); + } +} + +contract GIndexTest is Test { + GIndex internal ZERO = GIndex.wrap(bytes32(0)); + GIndex internal ROOT = GIndex.wrap(0x0000000000000000000000000000000000000000000000000000000000000100); + GIndex internal MAX = GIndex.wrap(bytes32(type(uint256).max)); + + Library internal lib; + + error Log2Undefined(); + + function setUp() public { + lib = new Library(); + } + + function test_pack() public { + GIndex gI; + + gI = pack(0x7b426f79504c6a8e9d31415b722f696e705c8a3d9f41, 42); + assertEq( + gI.unwrap(), + 0x0000000000000000007b426f79504c6a8e9d31415b722f696e705c8a3d9f412a, + "Invalid gindex encoded" + ); + + assertEq(MAX.unwrap(), bytes32(type(uint256).max), "Invalid gindex encoded"); + } + + function test_isRootTrue() public { + assertTrue(ROOT.isRoot(), "ROOT is not root gindex"); + } + + function test_isRootFalse() public { + GIndex gI; + + gI = pack(0, 0); + assertFalse(gI.isRoot(), "Expected [0,0].isRoot() to be false"); + + gI = pack(42, 0); + assertFalse(gI.isRoot(), "Expected [42,0].isRoot() to be false"); + + gI = pack(42, 4); + assertFalse(gI.isRoot(), "Expected [42,4].isRoot() to be false"); + + gI = pack(2048, 4); + assertFalse(gI.isRoot(), "Expected [2048,4].isRoot() to be false"); + + gI = pack(type(uint248).max, type(uint8).max); + assertFalse(gI.isRoot(), "Expected [uint248.max,uint8.max].isRoot() to be false"); + } + + function test_isParentOf_Truthy() public { + assertTrue(pack(1024, 0).isParentOf(pack(2048, 0))); + assertTrue(pack(1024, 0).isParentOf(pack(2049, 0))); + assertTrue(pack(1024, 9).isParentOf(pack(2048, 0))); + assertTrue(pack(1024, 9).isParentOf(pack(2049, 0))); + assertTrue(pack(1024, 0).isParentOf(pack(2048, 9))); + assertTrue(pack(1024, 0).isParentOf(pack(2049, 9))); + assertTrue(pack(1023, 0).isParentOf(pack(4094, 0))); + assertTrue(pack(1024, 0).isParentOf(pack(4098, 0))); + } + + function testFuzz_ROOT_isParentOfAnyChild(GIndex rhs) public { + vm.assume(rhs.index() > 1); + assertTrue(ROOT.isParentOf(rhs)); + } + + function testFuzz_isParentOf_LessThanAnchor(GIndex lhs, GIndex rhs) public { + vm.assume(rhs.index() < lhs.index()); + assertFalse(lhs.isParentOf(rhs)); + } + + function test_isParentOf_OffTheBranch() public { + assertFalse(pack(1024, 0).isParentOf(pack(2050, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2051, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2047, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2046, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2050, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2051, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2047, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2046, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2050, 9))); + assertFalse(pack(1024, 0).isParentOf(pack(2051, 9))); + assertFalse(pack(1024, 0).isParentOf(pack(2047, 9))); + assertFalse(pack(1024, 0).isParentOf(pack(2046, 9))); + assertFalse(pack(1023, 0).isParentOf(pack(2048, 0))); + assertFalse(pack(1023, 0).isParentOf(pack(2049, 0))); + assertFalse(pack(1023, 9).isParentOf(pack(2048, 0))); + assertFalse(pack(1023, 9).isParentOf(pack(2049, 0))); + assertFalse(pack(1023, 0).isParentOf(pack(4098, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(4094, 0))); + } + + function test_concat() public { + assertEq(pack(2, 99).concat(pack(3, 99)).unwrap(), pack(5, 99).unwrap()); + assertEq(pack(31, 99).concat(pack(3, 99)).unwrap(), pack(63, 99).unwrap()); + assertEq(pack(31, 99).concat(pack(6, 99)).unwrap(), pack(126, 99).unwrap()); + assertEq(ROOT.concat(pack(2, 1)).concat(pack(5, 1)).concat(pack(9, 1)).unwrap(), pack(73, 1).unwrap()); + assertEq(ROOT.concat(pack(2, 9)).concat(pack(5, 1)).concat(pack(9, 4)).unwrap(), pack(73, 4).unwrap()); + + assertEq(ROOT.concat(MAX).unwrap(), MAX.unwrap()); + } + + function test_concat_RevertsIfZeroGIndex() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(ZERO, pack(1024, 1)); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(1024, 1), ZERO); + } + + function test_concat_BigIndicesBorderCases() public view { + lib.concat(pack(2 ** 9, 0), pack(2 ** 238, 0)); + lib.concat(pack(2 ** 47, 0), pack(2 ** 200, 0)); + lib.concat(pack(2 ** 199, 0), pack(2 ** 48, 0)); + } + + function test_concat_RevertsIfTooBigIndices() public { + vm.expectRevert(IndexOutOfRange.selector); + MAX.concat(MAX); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(2 ** 48, 0), pack(2 ** 200, 0)); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(2 ** 200, 0), pack(2 ** 48, 0)); + } + + function testFuzz_concat_WithRoot(GIndex rhs) public { + vm.assume(rhs.index() > 0); + assertEq(ROOT.concat(rhs).unwrap(), rhs.unwrap(), "`concat` with a root should return right-hand side value"); + } + + function testFuzz_concat_isParentOf(GIndex lhs, GIndex rhs) public { + // Left-hand side value can be a root. + vm.assume(lhs.index() > 0); + // But root.concat(root) will result in a root value again, and root is not a parent for itself. + vm.assume(rhs.index() > 1); + // Overflow check. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + + assertTrue(lhs.isParentOf(lhs.concat(rhs)), "Left-hand side value should be a parent of `concat` result"); + assertFalse(lhs.concat(rhs).isParentOf(lhs), "`concat` result can't be a parent for the left-hand side value"); + assertFalse(lhs.concat(rhs).isParentOf(rhs), "`concat` result can't be a parent for the right-hand side value"); + } + + function testFuzz_unpack(uint248 index, uint8 pow) public { + GIndex gI = pack(index, pow); + assertEq(gI.index(), index); + assertEq(gI.width(), 2 ** pow); + } + + function test_shr() public { + GIndex gI; + + gI = pack(1024, 4); + assertEq(gI.shr(0).unwrap(), pack(1024, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(1025, 4).unwrap()); + assertEq(gI.shr(15).unwrap(), pack(1039, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gI.shr(0).unwrap(), pack(1031, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(1032, 4).unwrap()); + assertEq(gI.shr(8).unwrap(), pack(1039, 4).unwrap()); + + gI = pack(2049, 4); + assertEq(gI.shr(0).unwrap(), pack(2049, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(2050, 4).unwrap()); + assertEq(gI.shr(14).unwrap(), pack(2063, 4).unwrap()); + } + + function test_shr_AfterConcat() public { + GIndex gI; + GIndex gIParent = pack(5, 4); + + gI = pack(1024, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5120, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5121, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(15).unwrap(), pack(5135, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5127, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5128, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(8).unwrap(), pack(5135, 4).unwrap()); + + gI = pack(2049, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(10241, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(10242, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(14).unwrap(), pack(10255, 4).unwrap()); + } + + function test_shr_OffTheWidth() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(ROOT, 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1024, 4), 16); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1031, 4), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1023, 4), 1); + } + + function test_shr_OffTheWidth_AfterConcat() public { + GIndex gIParent = pack(154, 4); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(ROOT), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1024, 4)), 16); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1031, 4)), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1023, 4)), 1); + } + + function testFuzz_shr_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { + // Indices concatenation overflow protection. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + vm.assume(rhs.index() >= rhs.width()); + unchecked { + vm.assume(rhs.width() + shift > rhs.width()); + vm.assume(lhs.concat(rhs).index() + shift > lhs.concat(rhs).index()); + } + + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(lhs.concat(rhs), rhs.width() + shift); + } + + function test_shl() public { + GIndex gI; + + gI = pack(1023, 4); + assertEq(gI.shl(0).unwrap(), pack(1023, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(1022, 4).unwrap()); + assertEq(gI.shl(15).unwrap(), pack(1008, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gI.shl(0).unwrap(), pack(1031, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(1030, 4).unwrap()); + assertEq(gI.shl(7).unwrap(), pack(1024, 4).unwrap()); + + gI = pack(2063, 4); + assertEq(gI.shl(0).unwrap(), pack(2063, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(2062, 4).unwrap()); + assertEq(gI.shl(15).unwrap(), pack(2048, 4).unwrap()); + } + + function test_shl_AfterConcat() public { + GIndex gI; + GIndex gIParent = pack(5, 4); + + gI = pack(1023, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(3071, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(3070, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(3056, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(5127, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(5126, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(7).unwrap(), pack(5120, 4).unwrap()); + + gI = pack(2063, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(10255, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(10254, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(10240, 4).unwrap()); + } + + function test_shl_OffTheWidth() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(ROOT, 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1024, 4), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1031, 4), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1023, 4), 16); + } + + function test_shl_OffTheWidth_AfterConcat() public { + GIndex gIParent = pack(154, 4); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(ROOT), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1024, 4)), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1031, 4)), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1023, 4)), 16); + } + + function testFuzz_shl_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { + // Indices concatenation overflow protection. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + vm.assume(rhs.index() >= rhs.width()); + vm.assume(shift > rhs.index() % rhs.width()); + + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(lhs.concat(rhs), shift); + } + + function testFuzz_shl_shr_Idempotent(GIndex gI, uint256 shift) public { + vm.assume(gI.index() > 0); + vm.assume(gI.index() >= gI.width()); + vm.assume(shift < gI.index() % gI.width()); + + assertEq(lib.shr(lib.shl(gI, shift), shift).unwrap(), gI.unwrap()); + } + + function testFuzz_shr_shl_Idempotent(GIndex gI, uint256 shift) public { + vm.assume(gI.index() > 0); + vm.assume(gI.index() >= gI.width()); + vm.assume(shift < gI.width() - (gI.index() % gI.width())); + + assertEq(lib.shl(lib.shr(gI, shift), shift).unwrap(), gI.unwrap()); + } + + function test_fls() public { + for (uint256 i = 1; i < 255; i++) { + assertEq(fls((1 << i) - 1), i - 1); + assertEq(fls((1 << i)), i); + assertEq(fls((1 << i) + 1), i); + } + + assertEq(fls(3), 1); // 0011 + assertEq(fls(7), 2); // 0101 + assertEq(fls(10), 3); // 1010 + assertEq(fls(300), 8); // 0001 0010 1100 + assertEq(fls(0), 256); + } +} diff --git a/test/0.8.25/lib/SSZ.t.sol b/test/0.8.25/lib/SSZ.t.sol new file mode 100644 index 000000000..4d65ca6ea --- /dev/null +++ b/test/0.8.25/lib/SSZ.t.sol @@ -0,0 +1,370 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {BeaconBlockHeader, Validator} from "../../../contracts/0.8.25/lib/Types.sol"; +import {GIndex, pack} from "../../../contracts/0.8.25/lib/GIndex.sol"; +import {Utilities} from "../contracts/Utilities.sol"; +import {Slot} from "../../../contracts/0.8.25/lib/Types.sol"; +import {SSZ} from "../../../contracts/0.8.25/lib/SSZ.sol"; + +// Wrap the library internal methods to make an actual call to them. +// Supposed to be used with `expectRevert` cheatcode and to pass +// calldata arguments. +contract Library { + function verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, GIndex gI) external view { + SSZ.verifyProof(proof, root, leaf, gI); + } +} + +contract SSZTest is Utilities, Test { + Library internal lib; + + function setUp() public { + lib = new Library(); + } + + function test_toLittleEndianUint() public pure { + uint256 v = 0x1234567890ABCDEF; + bytes32 expected = bytes32(bytes.concat(hex"EFCDAB9078563412", bytes24(0))); + bytes32 actual = SSZ.toLittleEndian(v); + assertEq(actual, expected); + } + + function test_toLittleEndianUintZero() public pure { + bytes32 actual = SSZ.toLittleEndian(0); + assertEq(actual, bytes32(0)); + } + + function test_toLittleEndianFalse() public pure { + bool v = false; + bytes32 expected = 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 actual = SSZ.toLittleEndian(v); + assertEq(actual, expected); + } + + function test_toLittleEndianTrue() public pure { + bool v = true; + bytes32 expected = 0x0100000000000000000000000000000000000000000000000000000000000000; + bytes32 actual = SSZ.toLittleEndian(v); + assertEq(actual, expected); + } + + function testFuzz_toLittleEndian_Idempotent(uint256 v) public pure { + uint256 n = v; + n = uint256(SSZ.toLittleEndian(n)); + n = uint256(SSZ.toLittleEndian(n)); + assertEq(n, v); + } + + function test_ValidatorRootExitedSlashed() public view { + Validator memory v = Validator({ + pubkey: hex"91760f8a17729cfcb68bfc621438e5d9dfa831cd648e7b2b7d33540a7cbfda1257e4405e67cd8d3260351ab3ff71b213", + withdrawalCredentials: 0x01000000000000000000000006676e8584342cc8b6052cfdf381c3a281f00ac8, + effectiveBalance: 30000000000, + slashed: true, + activationEligibilityEpoch: 242529, + activationEpoch: 242551, + exitEpoch: 242556, + withdrawableEpoch: 250743 + }); + + bytes32 expected = 0xe4674dc5c27e7d3049fcd298745c00d3e314f03d33c877f64bf071d3b77eb942; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRootActive() public view { + Validator memory v = Validator({ + pubkey: hex"8fb78536e82bcec34e98fff85c907f0a8e6f4b1ccdbf1e8ace26b59eb5a06d16f34e50837f6c490e2ad6a255db8d543b", + withdrawalCredentials: 0x0023b9d00bf66e7f8071208a85afde59b3148dea046ee3db5d79244880734881, + effectiveBalance: 32000000000, + slashed: false, + activationEligibilityEpoch: 2593, + activationEpoch: 5890, + exitEpoch: type(uint64).max, + withdrawableEpoch: type(uint64).max + }); + + bytes32 expected = 0x60fb91184416404ddfc62bef6df9e9a52c910751daddd47ea426aabaf19dfa09; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRootExtraBytesInPubkey() public view { + Validator memory v = Validator({ + pubkey: hex"8fb78536e82bcec34e98fff85c907f0a8e6f4b1ccdbf1e8ace26b59eb5a06d16f34e50837f6c490e2ad6a255db8d543bDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + withdrawalCredentials: 0x0023b9d00bf66e7f8071208a85afde59b3148dea046ee3db5d79244880734881, + effectiveBalance: 32000000000, + slashed: false, + activationEligibilityEpoch: 2593, + activationEpoch: 5890, + exitEpoch: type(uint64).max, + withdrawableEpoch: type(uint64).max + }); + + bytes32 expected = 0x60fb91184416404ddfc62bef6df9e9a52c910751daddd47ea426aabaf19dfa09; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRoot_AllZeroes() public view { + Validator memory v = Validator({ + pubkey: hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + withdrawalCredentials: 0x0000000000000000000000000000000000000000000000000000000000000000, + effectiveBalance: 0, + slashed: false, + activationEligibilityEpoch: 0, + activationEpoch: 0, + exitEpoch: 0, + withdrawableEpoch: 0 + }); + + bytes32 expected = 0xfa324a462bcb0f10c24c9e17c326a4e0ebad204feced523eccaf346c686f06ee; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRoot_AllOnes() public view { + Validator memory v = Validator({ + pubkey: hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + withdrawalCredentials: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + effectiveBalance: type(uint64).max, + slashed: true, + activationEligibilityEpoch: type(uint64).max, + activationEpoch: type(uint64).max, + exitEpoch: type(uint64).max, + withdrawableEpoch: type(uint64).max + }); + + bytes32 expected = 0x29c03a7cc9a8047ff05619a04bb6e60440a791e6ac3fe7d72e6fe9037dd3696f; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function testFuzz_validatorRoot_memory(Validator memory v) public view brutalizeMemory { + SSZ.hashTreeRoot(v); + } + + function test_BeaconBlockHeaderRoot() public view { + // Can be obtained via /eth/v1/beacon/headers/{block_id}. + BeaconBlockHeader memory h = BeaconBlockHeader({ + slot: Slot.wrap(7472518), + proposerIndex: 152834, + parentRoot: 0x4916af1ff31b06f1b27125d2d20cd26e123c425a4b34ebd414e5f0120537e78d, + stateRoot: 0x76ca64f3732754bc02c7966271fb6356a9464fe5fce85be8e7abc403c8c7b56b, + bodyRoot: 0x6d858c959f1c95f411dba526c4ae9ab8b2690f8b1e59ed1b79ad963ab798b01a + }); + + bytes32 expected = 0x26631ee28ab4dd44a39c3756e03714d6a35a256560de5e2885caef9c3efd5516; + bytes32 actual = SSZ.hashTreeRoot(h); + assertEq(actual, expected); + } + + function test_BeaconBlockHeaderRoot_AllZeroes() public view { + BeaconBlockHeader memory h = BeaconBlockHeader({ + slot: Slot.wrap(0), + proposerIndex: 0, + parentRoot: 0x0000000000000000000000000000000000000000000000000000000000000000, + stateRoot: 0x0000000000000000000000000000000000000000000000000000000000000000, + bodyRoot: 0x0000000000000000000000000000000000000000000000000000000000000000 + }); + + bytes32 expected = 0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c; + bytes32 actual = SSZ.hashTreeRoot(h); + assertEq(actual, expected); + } + + function test_BeaconBlockHeaderRoot_AllOnes() public view { + BeaconBlockHeader memory h = BeaconBlockHeader({ + slot: Slot.wrap(type(uint64).max), + proposerIndex: type(uint64).max, + parentRoot: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + stateRoot: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + bodyRoot: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + }); + + bytes32 expected = 0x5ebe9f2b0267944bd80dd5cde20317a91d07225ff12e9cd5ba1e834c05cc2b05; + bytes32 actual = SSZ.hashTreeRoot(h); + assertEq(actual, expected); + } + + function testFuzz_BeaconBlockHeaderRoot_memory(BeaconBlockHeader memory h) public view brutalizeMemory { + SSZ.hashTreeRoot(h); + } + + // For the tests below, assume there's the following tree from the bottom up: + // -- + // 0x0000000000000000000000000000000000000000000000000000000000000000 + // 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5 + // 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30 + // 0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85 + // -- + // 0x0a4b105f69a6f41c3b3efc9bb5ac525b5b557a524039a13c657a916d8eb04451 + // 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124 + // -- + // 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c + + function test_verifyProof_HappyPath() public view { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0x0000000000000000000000000000000000000000000000000000000000000000, + pack(4, 0) + ); + + // prettier-ignore + { + proof[0] = 0x0000000000000000000000000000000000000000000000000000000000000000; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(5, 0) + ); + } + + function test_verifyProof_OneItem() public view brutalizeMemory { + bytes32[] memory proof = new bytes32[](1); + + // prettier-ignore + proof[0] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0x0a4b105f69a6f41c3b3efc9bb5ac525b5b557a524039a13c657a916d8eb04451, + pack(2, 0) + ); + } + + function test_verifyProof_RevertWhen_NoProof() public brutalizeMemory { + vm.expectRevert(SSZ.InvalidProof.selector); + + // bytes32(0) is a valid proof for the inputs. + lib.verifyProof( + new bytes32[](0), + 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, + 0x0000000000000000000000000000000000000000000000000000000000000000, + pack(2, 0) + ); + } + + function test_verifyProof_RevertWhen_ProvingRoot() public brutalizeMemory { + vm.expectRevert(SSZ.InvalidProof.selector); + + lib.verifyProof( + new bytes32[](0), + 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, + 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, + pack(1, 0) + ); + } + + function test_verifyProof_RevertWhen_InvalidProof() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.InvalidProof.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(4, 0) + ); + } + + function test_verifyProof_RevertWhen_WrongGIndex() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.InvalidProof.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0x0000000000000000000000000000000000000000000000000000000000000000, + pack(5, 0) + ); + } + + function test_verifyProof_RevertWhen_BranchHasExtraItem() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.BranchHasExtraItem.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(2, 0) + ); + } + + function test_verifyProof_RevertWhen_BranchHasMissingItem() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.BranchHasMissingItem.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(8, 0) + ); + } + + function testFuzz_verifyProof_MemorySafe( + bytes32[] calldata proof, + bytes32 root, + bytes32 leaf, + GIndex gI + ) public view { + try this.verifyProofCallJunkMemory(proof, root, leaf, gI) {} catch {} + } + + function verifyProofCallJunkMemory( + bytes32[] calldata proof, + bytes32 root, + bytes32 leaf, + GIndex gI + ) external view brutalizeMemory { + SSZ.verifyProof(proof, root, leaf, gI); + } +}