Skip to content

Commit

Permalink
feat: add cl proof verifier
Browse files Browse the repository at this point in the history
  • Loading branch information
mkurayan committed Feb 24, 2025
1 parent ad2158d commit 50f8045
Show file tree
Hide file tree
Showing 10 changed files with 1,994 additions and 0 deletions.
169 changes: 169 additions & 0 deletions contracts/0.8.25/CLProofVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: 2024 Lido <info@lido.fi>
// 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;
}
}
117 changes: 117 additions & 0 deletions contracts/0.8.25/lib/GIndex.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2024 Lido <info@lido.fi>
// 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))
}
}
Loading

0 comments on commit 50f8045

Please sign in to comment.