-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
1,994 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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)) | ||
} | ||
} |
Oops, something went wrong.