-
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
8 changed files
with
661 additions
and
11 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
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,194 @@ | ||
// SPDX-FileCopyrightText: 2023 Lido <[email protected]> | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
pragma solidity 0.8.21; | ||
|
||
import { ICSVerifier } from "./interfaces/ICSVerifier.sol"; | ||
import { IGIProvider } from "./interfaces/IGIProvider.sol"; | ||
import { ICSModule } from "./interfaces/ICSModule.sol"; | ||
|
||
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; | ||
|
||
import { SSZ, Withdrawal, Validator } from "./lib/SSZ.sol"; | ||
import { GIndex } from "./lib/GIndexLib.sol"; | ||
|
||
contract CSVerifier is ICSVerifier { | ||
using SafeCast for uint256; | ||
|
||
using SSZ for Withdrawal; | ||
using SSZ for Validator; | ||
|
||
uint64 internal immutable SLOTS_PER_EPOCH; | ||
uint64 public immutable SECONDS_PER_SLOT; | ||
uint64 public immutable GENESIS_TIME; | ||
|
||
IGIProvider public giProvider; | ||
ICSModule public module; | ||
|
||
error InvalidChainConfig(); | ||
|
||
error ProofTypeNotSupported(); | ||
error ValidatorNotWithdrawn(); | ||
error BLSPubkeyMismatch(); | ||
|
||
constructor( | ||
uint256 slotsPerEpoch, | ||
uint256 secondsPerSlot, | ||
uint256 genesisTime, | ||
address _giProvider, | ||
address _module | ||
) { | ||
if (secondsPerSlot == 0) revert InvalidChainConfig(); | ||
if (slotsPerEpoch == 0) revert InvalidChainConfig(); | ||
|
||
SECONDS_PER_SLOT = secondsPerSlot.toUint64(); | ||
SLOTS_PER_EPOCH = slotsPerEpoch.toUint64(); | ||
GENESIS_TIME = genesisTime.toUint64(); | ||
// TODO: Move to initialize function. | ||
giProvider = IGIProvider(_giProvider); | ||
module = ICSModule(_module); | ||
} | ||
|
||
function processWithdrawalProof( | ||
WithdrawalProofContext calldata ctx, | ||
uint256 nodeOperatorId, | ||
uint256 keyIndex | ||
) external { | ||
bytes memory pubkey = module.getNodeOperatorSigningKeys( | ||
nodeOperatorId, | ||
keyIndex, | ||
1 | ||
); | ||
|
||
Validator memory validator = Validator({ | ||
pubkey: pubkey, | ||
withdrawalCredentials: _toWithdrawalCredentials( | ||
ctx.withdrawalAddress | ||
), | ||
effectiveBalance: 0, | ||
slashed: ctx.slashed, | ||
activationEligibilityEpoch: ctx.activationEligibilityEpoch, | ||
activationEpoch: ctx.activationEpoch, | ||
exitEpoch: ctx.exitEpoch, | ||
withdrawableEpoch: ctx.withdrawableEpoch | ||
}); | ||
|
||
if (keccak256(validator.pubkey) != keccak256(pubkey)) { | ||
revert BLSPubkeyMismatch(); | ||
} | ||
|
||
// Compare validator struct's withdrawable epoch and balance, otherwise | ||
// it's a skimmed reward withdrawn | ||
if ( | ||
_getEpoch() < validator.withdrawableEpoch || | ||
validator.effectiveBalance != 0 | ||
) { | ||
revert ValidatorNotWithdrawn(); | ||
} | ||
|
||
Withdrawal memory withdrawal = Withdrawal({ | ||
index: ctx.withdrawalIndex, | ||
validatorIndex: ctx.validatorIndex, | ||
withdrawalAddress: ctx.withdrawalAddress, | ||
amount: ctx.amount | ||
}); | ||
|
||
bytes32 blockRoot = _getBlockRoot(ctx.blockTimestamp); | ||
// solhint-disable-next-line func-named-parameters | ||
_verifyStateProof( | ||
ctx.stateRootProofType, | ||
blockRoot, | ||
ctx.stateRoot, | ||
ctx.stateRootOffsets, | ||
ctx.stateRootProof | ||
); | ||
|
||
SSZ.verifyProof( | ||
ctx.validatorProof, | ||
ctx.stateRoot, | ||
validator.hashTreeRoot(), | ||
giProvider.gIndex(validator, ctx.validatorIndex) | ||
); | ||
|
||
SSZ.verifyProof( | ||
ctx.withdrawalProof, | ||
ctx.stateRoot, | ||
withdrawal.hashTreeRoot(), | ||
giProvider.gIndex(withdrawal, ctx.withdrawalOffset) | ||
); | ||
|
||
module.submitWithdrawal( | ||
nodeOperatorId, | ||
keyIndex, | ||
withdrawal.amount * 1 gwei | ||
); | ||
} | ||
|
||
function _verifyStateProof( | ||
StateRootProofType proofType, | ||
bytes32 blockRoot, | ||
bytes32 stateRoot, | ||
bytes32 offsets, | ||
bytes32[] calldata proof | ||
) internal view { | ||
// TODO: Add support for the additional types of proofs. | ||
if (proofType != StateRootProofType.WithinBlock) { | ||
revert ProofTypeNotSupported(); | ||
} | ||
|
||
GIndex gI = _stateGIndex(proofType, offsets); | ||
|
||
SSZ.verifyProof(proof, blockRoot, stateRoot, gI); | ||
} | ||
|
||
function _stateGIndex( | ||
StateRootProofType proofType, | ||
bytes32 /* offsets */ | ||
) internal pure returns (GIndex) { | ||
if (proofType == StateRootProofType.WithinBlock) { | ||
return GIndex.wrap(0xdeadbeef); | ||
} | ||
|
||
// TODO: Add support for the additional types of proofs. | ||
revert ProofTypeNotSupported(); | ||
} | ||
|
||
function _toWithdrawalCredentials( | ||
address withdrawalAddress | ||
) internal pure returns (bytes32) { | ||
return bytes32((1 << 248) + uint160(withdrawalAddress)); | ||
} | ||
|
||
function _getBlockRoot(uint64 /* ts */) internal pure returns (bytes32) { | ||
return bytes32(hex"deadbeef"); | ||
} | ||
|
||
function _getEpoch() internal view returns (uint256) { | ||
return _computeEpochAtTimestamp(_getTime()); | ||
} | ||
|
||
// ┌─────────────────────────────────────────────────────────┐ | ||
// │ Methods below were copied from HashConsensus contract. │ | ||
// └─────────────────────────────────────────────────────────┘ | ||
|
||
function _computeSlotAtTimestamp( | ||
uint256 timestamp | ||
) internal view returns (uint256) { | ||
return (timestamp - GENESIS_TIME) / SECONDS_PER_SLOT; | ||
} | ||
|
||
function _computeEpochAtSlot(uint256 slot) internal view returns (uint256) { | ||
// See: github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_epoch_at_slot | ||
return 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 | ||
} | ||
} |
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,47 @@ | ||
// SPDX-FileCopyrightText: 2023 Lido <[email protected]> | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
pragma solidity 0.8.21; | ||
|
||
import { IGIProvider } from "./interfaces/IGIProvider.sol"; | ||
|
||
import { Withdrawal, Validator } from "./lib/SSZ.sol"; | ||
import { GIndex } from "./lib/GIndexLib.sol"; | ||
|
||
contract GIProvider is IGIProvider { | ||
// FIXME: These constants are not constants from a hardfork to a hardfork. We can implement a dedicated provider for | ||
// these values. | ||
GIndex public constant GI_STATE_WITHDRAWAL_ROOT = GIndex.wrap(0xdeadbeef); | ||
GIndex public constant GI_STATE_VALIDATORS_ROOT = GIndex.wrap(0xdeadbeef); | ||
GIndex public constant GI_HISTORICAL_SUMMARIES_ROOT = | ||
GIndex.wrap(0xdeadbeef); | ||
|
||
uint64 public constant MAX_WITHDRAWALS = 2 ** 4; // See spec/capella/beacon-chain.md:87. | ||
uint64 public constant MAX_VALIDATORS = 2 ** 40; // See spec/phase0/beacon-chain.md:258. | ||
uint64 public constant MAX_STATE_SUMMARY_ROOTS = 2 ** 13; // See spec/phase0/beacon-chain.md:249. | ||
uint64 public constant MAX_HISTORICAL_ROOTS = 2 ** 24; // See spec/phase0/beacon-chain.md:257. | ||
|
||
function gIndex( | ||
Withdrawal memory /* self */, | ||
uint64 offset | ||
) external pure returns (GIndex) { | ||
// NOTE: Shifting MAX_WITHDRAWALS because of mix_in_length during merkleization. | ||
return | ||
// TODO: This call can be inlined, because MAX_VALIDATORS is a constant and we don't have to compute log2. | ||
GI_STATE_WITHDRAWAL_ROOT.concat( | ||
GIndex.wrap((MAX_WITHDRAWALS << 1) | offset) | ||
); | ||
} | ||
|
||
function gIndex( | ||
Validator memory /* self */, | ||
uint64 offset | ||
) external pure returns (GIndex) { | ||
// NOTE: Shifting MAX_VALIDATORS because of mix_in_length during merkleization. | ||
return | ||
// TODO: This call can be inlined, because MAX_WITHDRAWALS is a constant and we don't have to compute log2. | ||
GI_STATE_VALIDATORS_ROOT.concat( | ||
GIndex.wrap((MAX_VALIDATORS << 1) | offset) | ||
); | ||
} | ||
} |
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
Oops, something went wrong.