Skip to content

Commit

Permalink
feat: validator's withdrawal proof
Browse files Browse the repository at this point in the history
  • Loading branch information
madlabman committed Jan 11, 2024
1 parent 7bdeaed commit 3629fe1
Show file tree
Hide file tree
Showing 6 changed files with 605 additions and 0 deletions.
194 changes: 194 additions & 0 deletions src/CSVerifier.sol
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
}
}
47 changes: 47 additions & 0 deletions src/GIProvider.sol
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)
);
}
}
46 changes: 46 additions & 0 deletions src/interfaces/ICSVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

interface ICSVerifier {
// ┌────────────────────────────────────────────────────────────────────────────────┐
// │ A proof for a `state_root` against a `block_root │
// │ Should accept different types of proofs as described here │
// │ https://research.lido.fi/t/block-roots-permanent-cache-eip-4788-plugin/5706/18 │
// └────────────────────────────────────────────────────────────────────────────────┘
enum StateRootProofType {
WithinBlock,
WithinHistoricalSummaries
}

struct WithdrawalProofContext {
// ── Withdrawal fields ─────────────────────────────────────────────────
uint8 withdrawalOffset; // in the withdrawals list
uint64 withdrawalIndex; // network-wise
uint64 validatorIndex;
uint64 amount;
address withdrawalAddress;
// ── Validator fields ──────────────────────────────────────────────────
bool slashed;
uint64 activationEligibilityEpoch;
uint64 activationEpoch;
uint64 exitEpoch;
uint64 withdrawableEpoch;
// ──────────────────────────────────────────────────────────────────────
uint64 blockTimestamp;
bytes32 stateRoot;
StateRootProofType stateRootProofType;
bytes32 stateRootOffsets; // Includes offsets for the different types of proofs.
// ── Proofs ────────────────────────────────────────────────────────────
bytes32[] withdrawalProof;
bytes32[] validatorProof;
bytes32[] stateRootProof;
}

function processWithdrawalProof(
WithdrawalProofContext calldata ctx,
uint256 nodeOperatorId,
uint256 keyIndex
) external;
}
19 changes: 19 additions & 0 deletions src/interfaces/IGIProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

import { Withdrawal, Validator } from "../lib/SSZ.sol";
import { GIndex } from "../lib/GIndexLib.sol";

interface IGIProvider {
function gIndex(
Withdrawal memory /* self */,
uint64 offset
) external pure returns (GIndex);

function gIndex(
Validator memory /* self */,
uint64 offset
) external pure returns (GIndex);
}
51 changes: 51 additions & 0 deletions src/lib/GIndexLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

type GIndex is uint256;

library GIndexLib {
// See https://github.com/protolambda/remerkleable/blob/91ed092d08ef0ba5ab076f0a34b0b371623db728/remerkleable/tree.py#L46
function concat(GIndex self, GIndex b) internal pure returns (GIndex) {
uint256 stepBitLen = log2(GIndex.unwrap(b));
return
GIndex.wrap(
(GIndex.unwrap(self) << stepBitLen) |
(GIndex.unwrap(b) ^ (1 << stepBitLen))
);
}

/// @dev From solady FixedPointMath.
/// @dev Returns the log2 of `x`.
/// Equivalent to computing the index of the most significant bit (MSB) of `x`.
function log2(uint256 x) internal pure returns (uint256 r) {
/// @solidity memory-safe-assembly
assembly {
if iszero(x) {
// revert Log2Undefined()
mstore(0x00, 0x5be3aa5c)
revert(0x1c, 0x04)
}

r := 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))))

// For the remaining 32 bits, use a De Bruijn lookup.
// See: https://graphics.stanford.edu/~seander/bithacks.html
x := shr(r, x)
x := or(x, shr(1, x))
x := or(x, shr(2, x))
x := or(x, shr(4, x))
x := or(x, shr(8, x))
x := or(x, shr(16, x))

// prettier-ignore
r := or(r, byte(shr(251, mul(x, shl(224, 0x07c4acdd))),
0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f))
}
}
}

using GIndexLib for GIndex global;
Loading

0 comments on commit 3629fe1

Please sign in to comment.