-
Notifications
You must be signed in to change notification settings - Fork 5
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
18 changed files
with
1,571 additions
and
1 deletion.
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
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,309 @@ | ||
// SPDX-FileCopyrightText: 2023 Lido <[email protected]> | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
pragma solidity 0.8.21; | ||
|
||
import { IForkSelector } from "./interfaces/IForkSelector.sol"; | ||
import { ICSVerifier } from "./interfaces/ICSVerifier.sol"; | ||
import { IGIProvider } from "./interfaces/IGIProvider.sol"; | ||
import { ICSModule } from "./interfaces/ICSModule.sol"; | ||
|
||
import { BeaconBlockHeader, ForkVersion, Slot, Validator, Withdrawal } from "./lib/Types.sol"; | ||
import { GIndex } from "./lib/GIndex.sol"; | ||
import { SSZ } from "./lib/SSZ.sol"; | ||
|
||
contract CSVerifier is ICSVerifier { | ||
using SSZ for BeaconBlockHeader; | ||
using SSZ for Withdrawal; | ||
using SSZ for Validator; | ||
|
||
address public constant BEACON_ROOTS = | ||
0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; | ||
|
||
uint64 public immutable SLOTS_PER_EPOCH; | ||
uint64 public immutable SECONDS_PER_SLOT; | ||
uint64 public immutable GENESIS_TIME; | ||
|
||
IForkSelector public forkSelector; | ||
IGIProvider public gIprovider; | ||
ICSModule public module; | ||
|
||
error RootNotFound(); | ||
error InvalidOffset(); | ||
error InvalidGIndex(); | ||
error InvalidBlockHeader(); | ||
error InvalidChainConfig(); | ||
error ProofTypeNotSupported(); | ||
error ValidatorNotWithdrawn(); | ||
|
||
constructor( | ||
uint64 slotsPerEpoch, | ||
uint64 secondsPerSlot, | ||
uint64 genesisTime | ||
) { | ||
if (secondsPerSlot == 0) revert InvalidChainConfig(); | ||
if (slotsPerEpoch == 0) revert InvalidChainConfig(); | ||
|
||
SECONDS_PER_SLOT = secondsPerSlot; | ||
SLOTS_PER_EPOCH = slotsPerEpoch; | ||
GENESIS_TIME = genesisTime; | ||
} | ||
|
||
function initialize( | ||
address _module, | ||
address _gIprovider, | ||
address _forkSelector | ||
) external { | ||
module = ICSModule(_module); | ||
gIprovider = IGIProvider(_gIprovider); | ||
forkSelector = IForkSelector(_forkSelector); | ||
} | ||
|
||
function processWithdrawalProof( | ||
ProvableBeaconBlockHeader calldata beaconBlock, | ||
WithdrawalProofContext calldata ctx, | ||
uint256 nodeOperatorId, | ||
uint256 keyIndex | ||
) external { | ||
// NOTE: Make as a modifier? | ||
{ | ||
bytes32 trustedHeaderRoot = _getParentBlockRoot( | ||
beaconBlock.rootsTimestamp | ||
); | ||
bytes32 headerRoot = beaconBlock.blockHeader.hashTreeRoot(); | ||
if (trustedHeaderRoot != headerRoot) { | ||
revert InvalidBlockHeader(); | ||
} | ||
} | ||
|
||
bytes32 stateRoot = beaconBlock.blockHeader.stateRoot; | ||
ForkVersion fork = forkSelector.findFork( | ||
Slot.wrap(beaconBlock.blockHeader.slot) | ||
); | ||
|
||
bytes memory pubkey = module.getNodeOperatorSigningKeys( | ||
nodeOperatorId, | ||
keyIndex, | ||
1 | ||
); | ||
|
||
Withdrawal memory withdrawal = _processWithdrawalProof( | ||
ctx, | ||
stateRoot, | ||
fork, | ||
pubkey | ||
); | ||
|
||
module.submitWithdrawal( | ||
nodeOperatorId, | ||
keyIndex, | ||
withdrawal.amountWei() | ||
); | ||
} | ||
|
||
function processHistoricalWithdrawalProof( | ||
ProvableHistoricalBlockHeader calldata beaconBlock, | ||
WithdrawalProofContext calldata ctx, | ||
uint256 nodeOperatorId, | ||
uint256 keyIndex | ||
) external { | ||
{ | ||
bytes32 trustedHeaderRoot = _getParentBlockRoot( | ||
beaconBlock.anchorBlock.rootsTimestamp | ||
); | ||
bytes32 headerRoot = beaconBlock | ||
.anchorBlock | ||
.blockHeader | ||
.hashTreeRoot(); | ||
if (trustedHeaderRoot != headerRoot) { | ||
revert InvalidBlockHeader(); | ||
} | ||
} | ||
|
||
{ | ||
// Check the validity of the historical block root against the anchor block header (accessible from EIP-4788). | ||
bytes32 anchorStateRoot = beaconBlock | ||
.anchorBlock | ||
.blockHeader | ||
.stateRoot; | ||
ForkVersion anchorFork = forkSelector.findFork( | ||
Slot.wrap(beaconBlock.anchorBlock.blockHeader.slot) | ||
); | ||
// solhint-disable-next-line func-named-parameters | ||
_verifyBlockRootProof( | ||
anchorFork, | ||
anchorStateRoot, | ||
beaconBlock.historicalBlock.hashTreeRoot(), | ||
beaconBlock.blockRootGIndex, | ||
beaconBlock.blockRootProof | ||
); | ||
} | ||
|
||
// Fork may get a new value depends on the historical state root. | ||
bytes32 stateRoot = beaconBlock.historicalBlock.stateRoot; | ||
ForkVersion fork = forkSelector.findFork( | ||
Slot.wrap(beaconBlock.historicalBlock.slot) | ||
); | ||
|
||
bytes memory pubkey = module.getNodeOperatorSigningKeys( | ||
nodeOperatorId, | ||
keyIndex, | ||
1 | ||
); | ||
|
||
Withdrawal memory withdrawal = _processWithdrawalProof( | ||
ctx, | ||
stateRoot, | ||
fork, | ||
pubkey | ||
); | ||
|
||
module.submitWithdrawal( | ||
nodeOperatorId, | ||
keyIndex, | ||
withdrawal.amountWei() | ||
); | ||
} | ||
|
||
function _getParentBlockRoot( | ||
uint64 ts | ||
) internal view returns (bytes32 root) { | ||
(bool success, bytes memory data) = BEACON_ROOTS.staticcall( | ||
abi.encode(ts) | ||
); | ||
|
||
if (!success || data.length == 0) { | ||
revert RootNotFound(); | ||
} | ||
|
||
root = abi.decode(data, (bytes32)); | ||
} | ||
|
||
/// @dev It's up to a user to provide a valid generalized index of a historical block root in a summaries list. | ||
function _verifyBlockRootProof( | ||
ForkVersion fork, | ||
bytes32 stateRoot, | ||
bytes32 historicalBlockRoot, | ||
GIndex historicalBlockRootGIndex, | ||
bytes32[] calldata blockRootProof | ||
) internal view { | ||
GIndex anchor = gIprovider.getIndex( | ||
fork, | ||
"BeaconState.historical_summaries" | ||
); | ||
|
||
// Ensuring the provided generalized index is for a node somewhere below the historical_summaries root. | ||
if (!anchor.isParentOf(historicalBlockRootGIndex)) { | ||
revert InvalidGIndex(); | ||
} | ||
|
||
SSZ.verifyProof( | ||
blockRootProof, | ||
stateRoot, | ||
historicalBlockRoot, | ||
historicalBlockRootGIndex | ||
); | ||
} | ||
|
||
// @dev state_root is already validated | ||
function _processWithdrawalProof( | ||
WithdrawalProofContext calldata ctx, | ||
bytes32 stateRoot, | ||
ForkVersion fork, | ||
bytes memory pubkey | ||
) internal view returns (Withdrawal memory withdrawal) { | ||
if (_getEpoch() < ctx.withdrawableEpoch) { | ||
revert ValidatorNotWithdrawn(); | ||
} | ||
|
||
Validator memory validator = Validator({ | ||
pubkey: pubkey, | ||
withdrawalCredentials: ctx.withdrawalCredentials, | ||
effectiveBalance: ctx.effectiveBalance, | ||
slashed: ctx.slashed, | ||
activationEligibilityEpoch: ctx.activationEligibilityEpoch, | ||
activationEpoch: ctx.activationEpoch, | ||
exitEpoch: ctx.exitEpoch, | ||
withdrawableEpoch: ctx.withdrawableEpoch | ||
}); | ||
|
||
SSZ.verifyProof( | ||
ctx.validatorProof, | ||
stateRoot, | ||
validator.hashTreeRoot(), | ||
_getValidatorGI(fork, ctx.validatorIndex) | ||
); | ||
|
||
withdrawal = Withdrawal({ | ||
index: ctx.withdrawalIndex, | ||
validatorIndex: ctx.validatorIndex, | ||
withdrawalAddress: _credentialsToAddress(ctx.withdrawalCredentials), | ||
amount: ctx.amount | ||
}); | ||
|
||
SSZ.verifyProof( | ||
ctx.withdrawalProof, | ||
stateRoot, | ||
withdrawal.hashTreeRoot(), | ||
_getWithdrawalGI(fork, ctx.withdrawalOffset) | ||
); | ||
} | ||
|
||
function _getEpoch() internal view returns (uint256) { | ||
return _computeEpochAtTimestamp(_getTime()); | ||
} | ||
|
||
function _credentialsToAddress( | ||
bytes32 credentials | ||
) internal pure returns (address) { | ||
return address(uint160(uint256(credentials))); | ||
} | ||
|
||
function _getValidatorGI( | ||
ForkVersion fork, | ||
uint256 offset | ||
) internal view returns (GIndex) { | ||
GIndex gI = gIprovider.getIndex(fork, "BeaconState.validators[0]"); | ||
return gI.shr(offset); | ||
} | ||
|
||
function _getWithdrawalGI( | ||
ForkVersion fork, | ||
uint256 offset | ||
) internal view returns (GIndex) { | ||
GIndex gI = gIprovider.getIndex(fork, "BeaconState.withdrawals[0]"); | ||
if (offset == 0) return gI; | ||
return gI.shr(offset); | ||
} | ||
|
||
// ┌─────────────────────────────────────────────────────────┐ | ||
// │ Methods below were copied from HashConsensus contract. │ | ||
// └─────────────────────────────────────────────────────────┘ | ||
|
||
function _computeSlotAtTimestamp( | ||
uint256 timestamp | ||
) internal view returns (Slot) { | ||
return Slot.wrap(uint64((timestamp - GENESIS_TIME) / SECONDS_PER_SLOT)); | ||
} | ||
|
||
function _computeEpochAtSlot(Slot slot) internal view returns (uint256) { | ||
// See: github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_epoch_at_slot | ||
return Slot.unwrap(slot) / SLOTS_PER_EPOCH; | ||
} | ||
|
||
function _computeEpochAtTimestamp( | ||
uint256 timestamp | ||
) internal view returns (uint256) { | ||
return _computeEpochAtSlot(_computeSlotAtTimestamp(timestamp)); | ||
} | ||
|
||
function _getTime() internal view virtual returns (uint256) { | ||
return block.timestamp; // solhint-disable-line not-rely-on-time | ||
} | ||
} | ||
|
||
function amountWei(Withdrawal memory withdrawal) pure returns (uint256) { | ||
return uint256(withdrawal.amount) * 1 gwei; | ||
} | ||
|
||
using { amountWei } for Withdrawal; |
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,57 @@ | ||
// SPDX-FileCopyrightText: 2023 Lido <[email protected]> | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
pragma solidity 0.8.21; | ||
|
||
import { IForkSelector } from "./interfaces/IForkSelector.sol"; | ||
import { ForkVersion, Slot } from "./lib/Types.sol"; | ||
|
||
contract ForkSelector is IForkSelector { | ||
ForkVersion[] public supportedVersions; | ||
Slot[] public versionsLookup; | ||
Slot public terminalSlot = Slot.wrap(type(uint64).max); | ||
|
||
error NoSuitableForkVersion(Slot slot); | ||
error UnexpectedOrder(); | ||
|
||
/// @dev If any fork introduces a changed generalized index, we need to add it here. | ||
/// @dev The list of `versionsLookup` is expected to be sorted in ascending order. | ||
function addForkAtSlot(ForkVersion fork, Slot slot) external onlyDao { | ||
if ( | ||
versionsLookup.length > 0 && | ||
Slot.unwrap(versionsLookup[versionsLookup.length - 1]) >= | ||
Slot.unwrap(slot) | ||
) { | ||
revert UnexpectedOrder(); | ||
} | ||
|
||
supportedVersions.push(fork); | ||
versionsLookup.push(slot); | ||
} | ||
|
||
// TODO: Make it on-shot operation of leave it as is? | ||
function ossifyAtSlot(Slot slot) external onlyDao { | ||
terminalSlot = slot; | ||
} | ||
|
||
/// @dev returns the fork version suitable for the given slot number given the requirements to generalized indices. | ||
function findFork(Slot slot) external view returns (ForkVersion) { | ||
if (Slot.unwrap(slot) > Slot.unwrap(terminalSlot)) { | ||
revert NoSuitableForkVersion(slot); | ||
} | ||
|
||
for (uint256 i = versionsLookup.length; i > 0; i--) { | ||
if (Slot.unwrap(slot) > Slot.unwrap(versionsLookup[i - 1])) { | ||
return supportedVersions[i - 1]; | ||
} | ||
} | ||
|
||
// Basically, too old slot provided. | ||
revert NoSuitableForkVersion(slot); | ||
} | ||
|
||
modifier onlyDao() { | ||
// FIXME: implement. | ||
_; | ||
} | ||
} |
Oops, something went wrong.