Skip to content

Commit

Permalink
feat: basic pectra fork timestamp functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ypatil12 committed Nov 15, 2024
1 parent 725d3df commit 2992704
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 62 deletions.
20 changes: 20 additions & 0 deletions src/contracts/interfaces/IEigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ interface IEigenPodManagerErrors {
/// @dev Thrown when the pods shares are negative and a beacon chain balance update is attempted.
/// The podOwner should complete legacy withdrawal first.
error LegacyWithdrawalsNotCompleted();
/// @dev Thrown when the fork timestamp is invalid
error InvalidForkTimestamp();
/// @dev Thrown when the fork timestamp is not set
error ForkTimestampAlreadySet();
}

interface IEigenPodManagerEvents {
Expand All @@ -49,6 +53,9 @@ interface IEigenPodManagerEvents {
address withdrawer,
bytes32 withdrawalRoot
);

/// @notice Emitted when the pectra fork timestamp is set
event PectraForkTimestampSet(uint64 newPectraForkTimestamp);
}

/**
Expand Down Expand Up @@ -129,4 +136,17 @@ interface IEigenPodManager is IEigenPodManagerErrors, IEigenPodManagerEvents, IS

/// @notice returns canonical, virtual beaconChainETH strategy
function beaconChainETHStrategy() external view returns (IStrategy);


/**
* @notice The Pectra hard fork timestamp used to determine which proof config to use for a checkpoint proof.
* @dev This function returns type(uint64).max if the fork timestamp has not been set. The timestamp can never be set to 0.
*/
function getPectraForkTimestamp() external view returns (uint64);

/**
* @notice Sets the pectra hard fork timestamp by the eigenPodManager owner
* @dev This function is callable only by the owner of the eigenPodManager only once
*/
function setPectraForkTimestamp(uint64 pectraForkTimestamp) external;
}
37 changes: 31 additions & 6 deletions src/contracts/libraries/BeaconChainProofs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ library BeaconChainProofs {
/// | HEIGHT: VALIDATOR_TREE_HEIGHT
/// individual validators
uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3;
uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5;

uint256 internal constant DENEB_BEACON_STATE_TREE_HEIGHT = 5;
uint256 internal constant PECTRA_BEACON_STATE_TREE_HEIGHT = 6; // There are now 38 validator fields, so tree height is 2^6 = 64

uint256 internal constant BALANCE_TREE_HEIGHT = 38;
uint256 internal constant VALIDATOR_TREE_HEIGHT = 40;

Expand Down Expand Up @@ -133,18 +136,24 @@ library BeaconChainProofs {
/// which is used as the leaf to prove against `beaconStateRoot`
/// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot`
/// @param validatorIndex the validator's unique index
/// @param proofTimestamp the timestamp at which the proof is being verified
/// @param pectraForkTimestamp the timestamp of the Pectra hard fork
function verifyValidatorFields(
bytes32 beaconStateRoot,
bytes32[] calldata validatorFields,
bytes calldata validatorFieldsProof,
uint40 validatorIndex
uint40 validatorIndex,
uint64 proofTimestamp,
uint64 pectraForkTimestamp
) internal view {
require(validatorFields.length == VALIDATOR_FIELDS_LENGTH, InvalidValidatorFieldsLength());

uint256 beaconstateTreeHeight = getBeaconStateTreeHeight(proofTimestamp, pectraForkTimestamp);

/// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for
/// this container includes hashing the root of the validator tree with the length of the validator list
require(
validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT),
validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + beaconstateTreeHeight),
InvalidProofLength()
);

Expand Down Expand Up @@ -185,9 +194,18 @@ library BeaconChainProofs {
/// against the same balance container root.
/// @param beaconBlockRoot merkle root of the beacon block
/// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot`
function verifyBalanceContainer(bytes32 beaconBlockRoot, BalanceContainerProof calldata proof) internal view {
/// @param proofTimestamp the timestamp at which the checkpoint proof is being verified
/// @param pectraForkTimestamp the timestamp of the Pectra hard fork
function verifyBalanceContainer(
bytes32 beaconBlockRoot,
BalanceContainerProof calldata proof,
uint64 proofTimestamp,
uint64 pectraForkTimestamp
) internal view {
uint256 beaconstateTreeHeight = getBeaconStateTreeHeight(proofTimestamp, pectraForkTimestamp);

require(
proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT),
proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + beaconstateTreeHeight),
InvalidProofLength()
);

Expand All @@ -197,7 +215,7 @@ library BeaconChainProofs {
/// -- beaconStateRoot
/// | HEIGHT: BEACON_STATE_TREE_HEIGHT
/// ---- balancesContainerRoot
uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX;
uint256 index = (STATE_ROOT_INDEX << (beaconstateTreeHeight)) | BALANCE_CONTAINER_INDEX;

require(
Merkle.verifyInclusionSha256({
Expand Down Expand Up @@ -312,4 +330,11 @@ library BeaconChainProofs {
) internal pure returns (uint64) {
return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]);
}

/// @dev Gets the height of the beacon state tree based on the pectraForkTimestamp
/// @dev We subtract one from the pectraForkTimestamp to ensure that a `proofTimestamp` at the `pectraForkTimestamp`
/// is considered to be Pre-Pectra given the EIP-4788 returns the parent block.
function getBeaconStateTreeHeight(uint64 proofTimestamp, uint64 pectraForkTimestamp) internal pure returns (uint256) {
return proofTimestamp < pectraForkTimestamp - 1 ? DENEB_BEACON_STATE_TREE_HEIGHT : PECTRA_BEACON_STATE_TREE_HEIGHT;
}
}
119 changes: 64 additions & 55 deletions src/contracts/pods/EigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import "./EigenPodStorage.sol";
* @title The implementation contract used for restaking beacon chain ETH on EigenLayer
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs
* @notice This EigenPod Beacon Proxy implementation adheres to the current Pectra consensus specs
* @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose
* to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts
*/
Expand Down Expand Up @@ -160,7 +160,9 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
// Verify `balanceContainerProof` against `beaconBlockRoot`
BeaconChainProofs.verifyBalanceContainer({
beaconBlockRoot: checkpoint.beaconBlockRoot,
proof: balanceContainerProof
proof: balanceContainerProof,
proofTimestamp: checkpointTimestamp,
pectraForkTimestamp: eigenPodManager.getPectraForkTimestamp()
});

// Process each checkpoint proof submitted
Expand Down Expand Up @@ -254,6 +256,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
for (uint256 i = 0; i < validatorIndices.length; i++) {
// forgefmt: disable-next-item
totalAmountToBeRestakedWei += _verifyWithdrawalCredentials(
beaconTimestamp,
stateRootProof.beaconStateRoot,
validatorIndices[i],
validatorFieldsProofs[i],
Expand Down Expand Up @@ -344,7 +347,9 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
beaconStateRoot: stateRootProof.beaconStateRoot,
validatorFields: proof.validatorFields,
validatorFieldsProof: proof.proof,
validatorIndex: uint40(validatorInfo.validatorIndex)
validatorIndex: uint40(validatorInfo.validatorIndex),
proofTimestamp: beaconTimestamp,
pectraForkTimestamp: eigenPodManager.getPectraForkTimestamp()
});

// Validator verified to be stale - start a checkpoint
Expand Down Expand Up @@ -419,63 +424,65 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
*/
function _verifyWithdrawalCredentials(
uint64 beaconTimestamp,
bytes32 beaconStateRoot,
uint40 validatorIndex,
bytes calldata validatorFieldsProof,
bytes32[] calldata validatorFields
) internal returns (uint256) {
bytes32 pubkeyHash = validatorFields.getPubkeyHash();
ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash];

// Withdrawal credential proofs should only be processed for "INACTIVE" validators
require(validatorInfo.status == VALIDATOR_STATUS.INACTIVE, CredentialsAlreadyVerified());

// Validator should be active on the beacon chain, or in the process of activating.
// This implies the validator has reached the minimum effective balance required
// to become active on the beacon chain.
//
// This check is important because the Pectra upgrade will move any validators that
// do NOT have an activation epoch to a "pending deposit queue," temporarily resetting
// their current and effective balances to 0. This balance can be restored if a deposit
// is made to bring the validator's balance above the minimum activation balance.
// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/fork.md#upgrading-the-state)
//
// In the context of EigenLayer slashing, this temporary reset would allow pod shares
// to temporarily decrease, then be restored later. This would effectively prevent these
// shares from being slashable on EigenLayer for a short period of time.
require(
validatorFields.getActivationEpoch() != BeaconChainProofs.FAR_FUTURE_EPOCH, ValidatorInactiveOnBeaconChain()
);
// TODO: Can we handle stack too deep cleaner?
{
ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorFields.getPubkeyHash()];
// Withdrawal credential proofs should only be processed for "INACTIVE" validators
require(validatorInfo.status == VALIDATOR_STATUS.INACTIVE, CredentialsAlreadyVerified());
// Validator should be active on the beacon chain, or in the process of activating.
// This implies the validator has reached the minimum effective balance required
// to become active on the beacon chain.
//
// This check is important because the Pectra upgrade will move any validators that
// do NOT have an activation epoch to a "pending deposit queue," temporarily resetting
// their current and effective balances to 0. This balance can be restored if a deposit
// is made to bring the validator's balance above the minimum activation balance.
// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/fork.md#upgrading-the-state)
//
// In the context of EigenLayer slashing, this temporary reset would allow pod shares
// to temporarily decrease, then be restored later. This would effectively prevent these
// shares from being slashable on EigenLayer for a short period of time.
require(
validatorFields.getActivationEpoch() != BeaconChainProofs.FAR_FUTURE_EPOCH, ValidatorInactiveOnBeaconChain()
);

// Validator should not already be in the process of exiting. This is an important property
// this method needs to enforce to ensure a validator cannot be already-exited by the time
// its withdrawal credentials are verified.
//
// Note that when a validator initiates an exit, two values are set:
// - exit_epoch
// - withdrawable_epoch
//
// The latter of these two values describes an epoch after which the validator's ETH MIGHT
// have been exited to the EigenPod, depending on the state of the beacon chain withdrawal
// queue.
//
// Requiring that a validator has not initiated exit by the time the EigenPod sees their
// withdrawal credentials guarantees that the validator has not fully exited at this point.
//
// This is because:
// - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest
// slot available in the EIP-4788 oracle, which keeps the last 8192 slots.
// - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to
// 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots).
//
// (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit)
require(validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, ValidatorIsExitingBeaconChain());
// Validator should not already be in the process of exiting. This is an important property
// this method needs to enforce to ensure a validator cannot be already-exited by the time
// its withdrawal credentials are verified.
//
// Note that when a validator initiates an exit, two values are set:
// - exit_epoch
// - withdrawable_epoch
//
// The latter of these two values describes an epoch after which the validator's ETH MIGHT
// have been exited to the EigenPod, depending on the state of the beacon chain withdrawal
// queue.
//
// Requiring that a validator has not initiated exit by the time the EigenPod sees their
// withdrawal credentials guarantees that the validator has not fully exited at this point.
//
// This is because:
// - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest
// slot available in the EIP-4788 oracle, which keeps the last 8192 slots.
// - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to
// 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots).
//
// (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit)
require(validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, ValidatorIsExitingBeaconChain());

// Ensure the validator's withdrawal credentials are pointed at this pod
require(
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()),
WithdrawalCredentialsNotForEigenPod()
);
// Ensure the validator's withdrawal credentials are pointed at this pod
require(
validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()),
WithdrawalCredentialsNotForEigenPod()
);
}

// Get the validator's effective balance. Note that this method uses effective balance, while
// `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's
Expand All @@ -487,7 +494,9 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
beaconStateRoot: beaconStateRoot,
validatorFields: validatorFields,
validatorFieldsProof: validatorFieldsProof,
validatorIndex: validatorIndex
validatorIndex: validatorIndex,
proofTimestamp: beaconTimestamp,
pectraForkTimestamp: eigenPodManager.getPectraForkTimestamp()
});

// Account for validator in future checkpoints. Note that if this pod has never started a
Expand All @@ -499,7 +508,7 @@ contract EigenPod is Initializable, ReentrancyGuardUpgradeable, EigenPodPausingC
currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp;

// Proofs complete - create the validator in state
_validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({
_validatorPubkeyHashToInfo[validatorFields.getPubkeyHash()] = ValidatorInfo({
validatorIndex: validatorIndex,
restakedBalanceGwei: restakedBalanceGwei,
lastCheckpointedAt: lastCheckpointedAt,
Expand Down
20 changes: 20 additions & 0 deletions src/contracts/pods/EigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,26 @@ contract EigenPodManager is
}
}

/// @inheritdoc IEigenPodManager
function setPectraForkTimestamp(uint64 newPectraForkTimestamp) external onlyOwner {
uint64 currentPectraForkTimestamp = getPectraForkTimestamp();
require(currentPectraForkTimestamp == type(uint64).max, ForkTimestampAlreadySet());
require(newPectraForkTimestamp != 0, InvalidForkTimestamp());

_pectraForkTimestamp = newPectraForkTimestamp;
emit PectraForkTimestampSet(newPectraForkTimestamp);
}

/// @inheritdoc IEigenPodManager
function getPectraForkTimestamp() public view returns (uint64) {
/// Initial value is 0, return type(uint64).max if not set
if (_pectraForkTimestamp == 0) {
return type(uint64).max;
} else {
return _pectraForkTimestamp;
}
}

/// @notice Returns the current shares of `user` in `strategy`
/// @dev strategy must be beaconChainETH when talking to the EigenPodManager
/// @dev returns 0 if the user has negative shares
Expand Down
4 changes: 3 additions & 1 deletion src/contracts/pods/EigenPodManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {

uint64 internal __deprecated_denebForkTimestamp;

uint64 internal _pectraForkTimestamp;

constructor(
IETHPOSDeposit _ethPOS,
IBeacon _eigenPodBeacon,
Expand All @@ -94,5 +96,5 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[44] private __gap;
uint256[43] private __gap;
}

0 comments on commit 2992704

Please sign in to comment.