Skip to content

feat: pectra compatibility #1053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ae1ebdd
feat: update beacon tree height
ypatil12 Jan 28, 2025
dc19e76
feat: add hardcoded fork timestamp; tests pass for deneb container
ypatil12 Jan 28, 2025
911e71a
test: add upgrade tests
ypatil12 Jan 30, 2025
025b45b
test: add additional pre-fork case
ypatil12 Jan 30, 2025
3da3598
chore: simplify fork logic
ypatil12 Jan 30, 2025
bf23115
chore: pending 0x02 credentials
ypatil12 Feb 5, 2025
9511a61
chore: push initial impl (#1069)
ypatil12 Feb 6, 2025
b446eea
feat: add timestamp setter
ypatil12 Feb 6, 2025
be313ff
feat: update basic integration tests; pending some debugging
ypatil12 Feb 7, 2025
d50e8da
chore: update comments
ypatil12 Feb 7, 2025
bc16e4a
chore: format
ypatil12 Feb 7, 2025
a88ac5d
chore: cleanup interface
ypatil12 Feb 16, 2025
799cfc6
test: push update, still debugging
ypatil12 Feb 17, 2025
4b1ba92
test: passing tests
ypatil12 Feb 18, 2025
21c91af
test: fix upgrade tests
ypatil12 Feb 18, 2025
7d0c583
chore: update BC mock to use max EB
ypatil12 Feb 18, 2025
233379d
fix: tests
ypatil12 Feb 18, 2025
a767aad
fix: pectra beacon max eb
ypatil12 Feb 18, 2025
9cd8980
chore: update all tests to create validators up to maxEB
ypatil12 Feb 18, 2025
9e3f5b4
fix: out of gas error
ypatil12 Feb 18, 2025
3d46db5
feat: add require statement for nonzero fork timestamp
ypatil12 Feb 22, 2025
30f0d19
fix: storage gap
ypatil12 Feb 22, 2025
c101620
feat: use versioned enum; bring fork logic into pod
ypatil12 Feb 22, 2025
06ba161
chore: update upgradeable bc mock; deduplicate func
ypatil12 Feb 22, 2025
c6a665d
chore: addressn nits
ypatil12 Feb 22, 2025
3f57319
chore: format
ypatil12 Feb 22, 2025
8658df5
chore: update upgrade script to pause all actions
ypatil12 Feb 22, 2025
b7053d2
chore: typos
ypatil12 Feb 22, 2025
d93e6c1
chore: compile errors
ypatil12 Feb 22, 2025
62f2943
chore: fix fork timestmap; only do pectra forking in prooftra upgrade…
ypatil12 Feb 23, 2025
a8dd876
test: add tests for proving deneb against pectra
ypatil12 Feb 24, 2025
36093f7
chore: cleanup using deneb on all non-prooftra upgrade tests
ypatil12 Feb 24, 2025
f0c68e1
fix: storage gap
ypatil12 Feb 24, 2025
494cb19
fix: set fork timestamp for fork tests
ypatil12 Feb 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/contracts/interfaces/IEigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ interface IEigenPodErrors {
error MsgValueNot32ETH();
/// @dev Thrown when provided `beaconTimestamp` is too far in the past.
error BeaconTimestampTooFarInPast();
/// @dev Thrown when the pectraForkTimestamp returned from the EigenPodManager is zero
error ForkTimestampZero();
}

interface IEigenPodTypes {
Expand Down Expand Up @@ -146,6 +148,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin {
) external;

/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable;

/**
Expand Down
22 changes: 22 additions & 0 deletions src/contracts/interfaces/IEigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ 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 caller is not the proof timestamp setter
error OnlyProofTimestampSetter();
}

interface IEigenPodManagerEvents {
Expand Down Expand Up @@ -58,6 +60,12 @@ interface IEigenPodManagerEvents {

/// @notice Emitted when an operator is slashed and shares to be burned are increased
event BurnableETHSharesIncreased(uint256 shares);

/// @notice Emitted when the Pectra fork timestamp is updated
event PectraForkTimestampSet(uint64 newPectraForkTimestamp);

/// @notice Emitted when the proof timestamp setter is updated
event ProofTimestampSetterSet(address newProofTimestampSetter);
}

interface IEigenPodManagerTypes {
Expand Down Expand Up @@ -122,6 +130,16 @@ interface IEigenPodManager is
int256 balanceDeltaWei
) external;

/// @notice Sets the address that can set proof timestamps
function setProofTimestampSetter(
address newProofTimestampSetter
) external;

/// @notice Sets the Pectra fork timestamp, only callable by `proofTimestampSetter`
function setPectraForkTimestamp(
uint64 timestamp
) external;

/// @notice Returns the address of the `podOwner`'s EigenPod if it has been deployed.
function ownerToPod(
address podOwner
Expand Down Expand Up @@ -171,4 +189,8 @@ interface IEigenPodManager is

/// @notice Returns the accumulated amount of beacon chain ETH Strategy shares
function burnableETHShares() external view returns (uint256);

/// @notice Returns the timestamp of the Pectra hard fork
/// @dev Specifically, this returns the timestamp of the first non-missed slot at or after the Pectra hard fork
function pectraForkTimestamp() external view returns (uint64);
}
35 changes: 29 additions & 6 deletions src/contracts/libraries/BeaconChainProofs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ 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;
uint256 internal constant BALANCE_TREE_HEIGHT = 38;
uint256 internal constant VALIDATOR_TREE_HEIGHT = 40;

Expand Down Expand Up @@ -71,6 +72,12 @@ library BeaconChainProofs {
uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max;
bytes8 internal constant UINT64_MASK = 0xffffffffffffffff;

/// @notice The beacon chain version to validate against
enum ProofVersion {
DENEB,
PECTRA
}

/// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root
struct StateRootProof {
bytes32 beaconStateRoot;
Expand Down Expand Up @@ -134,17 +141,20 @@ library BeaconChainProofs {
/// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot`
/// @param validatorIndex the validator's unique index
function verifyValidatorFields(
ProofVersion proofVersion,
bytes32 beaconStateRoot,
bytes32[] calldata validatorFields,
bytes calldata validatorFieldsProof,
uint40 validatorIndex
) internal view {
require(validatorFields.length == VALIDATOR_FIELDS_LENGTH, InvalidValidatorFieldsLength());

uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofVersion);

/// 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,10 +195,15 @@ 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 {
function verifyBalanceContainer(
ProofVersion proofVersion,
bytes32 beaconBlockRoot,
BalanceContainerProof calldata proof
) internal view {
uint256 beaconStateTreeHeight = getBeaconStateTreeHeight(proofVersion);

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

/// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees:
Expand All @@ -197,7 +212,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 +327,12 @@ library BeaconChainProofs {
) internal pure returns (uint64) {
return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]);
}

/// @dev We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
function getBeaconStateTreeHeight(
ProofVersion proofVersion
) internal pure returns (uint256) {
return proofVersion == ProofVersion.DENEB ? DENEB_BEACON_STATE_TREE_HEIGHT : PECTRA_BEACON_STATE_TREE_HEIGHT;
}
}
29 changes: 28 additions & 1 deletion src/contracts/pods/EigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ contract EigenPod is

// Verify `balanceContainerProof` against `beaconBlockRoot`
BeaconChainProofs.verifyBalanceContainer({
proofVersion: _getProofVersion(checkpointTimestamp),
beaconBlockRoot: checkpoint.beaconBlockRoot,
proof: balanceContainerProof
});
Expand Down Expand Up @@ -267,6 +268,7 @@ contract EigenPod is
for (uint256 i = 0; i < validatorIndices.length; i++) {
// forgefmt: disable-next-item
totalAmountToBeRestakedWei += _verifyWithdrawalCredentials(
beaconTimestamp,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I wonder about the usage of getParentBlockRoot above.

Can beaconTimestamp belong to the block after the fork, but the proof itself is over the parent block (aka pre-fork)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record I think I'm correct here. 2 scenarios:

  1. I try to verify withdrawal credentials the block after the hard fork. getParentBlockRoot grabs a block from before the hard fork; that's what my proof needs to be against.
  2. I start a checkpoint just after the hard fork. The checkpoint block root used for proofs is from a block just before the hard fork.

Naive solution: have the fork selector logic in the proofs library check proofTimestamp - 12. However, this runs into potential issues with skipped slots -- I might have a proof timestamp 2 blocks after the hard fork, but there was a skipped slot right at the fork, so the proof block is from before the fork.

I think, unfortunately, we need to pause proofs just before the fork, and unpause just after the fork (once we see the first valid block). Although I'm not sure this entirely fixes the issue... will think more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to pause + upgrade just after the fork + unpause? ugh.

Copy link
Collaborator Author

@ypatil12 ypatil12 Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synced offline, here is what we need to do:

  • Prevent deneb proofs from being submitted to pectra blocks
  • Ensure that the PECTRA_FORK_TIMESTAMP is the first timestamp at or after the pectra hard fork for which there is a non-missed slot. Checkpoint proofs store the proof timestamp. If there are missed slots at the hard fork timestamp, it's possible, like Alex mentions above, that the beaconTimestamp is post pectra but the block header is deneb.

To do this, here is the process:

  1. Pause checkpoint starting & credential proofs
  2. Upgrade after fork is hit
  3. Set pectra fork timestamp to the first timestamp at which there is a pectra block header
  4. Unpause

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the image above, we want to determine what proof type to use, given the proof timestamp tp. Because tp is used to look up a parent block in the EIP-4788 oracle, its proof type corresponds to the last non-skipped block. So, if:

  • tp > t1, use pectra logic
  • tp <= t1, use dencun logic

Given our beacon state tree height getter: https://github.com/layr-labs/eigenlayer-contracts/blob/b4852c74cdbe43fea2f7330ea0dc752fcf10b6e9/src/contracts/libraries/BeaconChainProofs.sol#L327-L334

... pectraForkTimestamp should be set to the first valid, non-skipped block after the Pectra hard fork.

stateRootProof.beaconStateRoot,
validatorIndices[i],
validatorFieldsProofs[i],
Expand Down Expand Up @@ -354,6 +356,7 @@ contract EigenPod is

// Verify Validator container proof against `beaconStateRoot`
BeaconChainProofs.verifyValidatorFields({
proofVersion: _getProofVersion(beaconTimestamp),
beaconStateRoot: stateRootProof.beaconStateRoot,
validatorFields: proof.validatorFields,
validatorFieldsProof: proof.proof,
Expand Down Expand Up @@ -391,6 +394,7 @@ contract EigenPod is
}

/// @notice Called by EigenPodManager when the owner wants to create another ETH validator.
/// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract.
function stake(
bytes calldata pubkey,
bytes calldata signature,
Expand Down Expand Up @@ -432,6 +436,7 @@ contract EigenPod is
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
*/
function _verifyWithdrawalCredentials(
uint64 beaconTimestamp,
bytes32 beaconStateRoot,
uint40 validatorIndex,
bytes calldata validatorFieldsProof,
Expand Down Expand Up @@ -486,7 +491,8 @@ contract EigenPod is

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

Expand All @@ -497,6 +503,7 @@ contract EigenPod is

// Verify passed-in validatorFields against verified beaconStateRoot:
BeaconChainProofs.verifyValidatorFields({
proofVersion: _getProofVersion(beaconTimestamp),
beaconStateRoot: beaconStateRoot,
validatorFields: validatorFields,
validatorFieldsProof: validatorFieldsProof,
Expand Down Expand Up @@ -678,6 +685,10 @@ contract EigenPod is
return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this));
}

function _podCompoundingWithdrawalCredentials() internal view returns (bytes memory) {
return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this));
}

///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec
function _calculateValidatorPubkeyHash(
bytes memory validatorPubkey
Expand Down Expand Up @@ -744,4 +755,20 @@ contract EigenPod is
require(success && result.length > 0, InvalidEIP4788Response());
return abi.decode(result, (bytes32));
}

/// @notice Returns the PROOF_TYPE depending on the `proofTimestamp` in relation to the fork timestamp.
function _getProofVersion(
uint64 proofTimestamp
) internal view returns (BeaconChainProofs.ProofVersion) {
/// Get the timestamp of the Pectra fork, read from the `EigenPodManager`
/// This returns the timestamp of the first non-missed slot at or after the Pectra hard fork
uint64 forkTimestamp = eigenPodManager.pectraForkTimestamp();
require(forkTimestamp != 0, ForkTimestampZero());

/// We check if the proofTimestamp is <= pectraForkTimestamp because a `proofTimestamp` at the `pectraForkTimestamp`
/// is considered to be Pre-Pectra given the EIP-4788 oracle returns the parent block.
return proofTimestamp <= forkTimestamp
? BeaconChainProofs.ProofVersion.DENEB
: BeaconChainProofs.ProofVersion.PECTRA;
}
}
21 changes: 21 additions & 0 deletions src/contracts/pods/EigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ contract EigenPodManager is
_;
}

modifier onlyProofTimestampSetter() {
require(msg.sender == proofTimestampSetter, OnlyProofTimestampSetter());
_;
}

constructor(
IETHPOSDeposit _ethPOS,
IBeacon _eigenPodBeacon,
Expand Down Expand Up @@ -231,6 +236,22 @@ contract EigenPodManager is
emit BurnableETHSharesIncreased(addedSharesToBurn);
}

/// @notice Sets the address that can set proof timestamps
function setProofTimestampSetter(
address newProofTimestampSetter
) external onlyOwner {
proofTimestampSetter = newProofTimestampSetter;
emit ProofTimestampSetterSet(newProofTimestampSetter);
}

/// @notice Sets the pectra fork timestamp
function setPectraForkTimestamp(
uint64 timestamp
) external onlyProofTimestampSetter {
pectraForkTimestamp = timestamp;
emit PectraForkTimestampSet(timestamp);
}

// INTERNAL FUNCTIONS

function _deployPod() internal returns (IEigenPod) {
Expand Down
8 changes: 7 additions & 1 deletion src/contracts/pods/EigenPodManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
/// @notice Returns the amount of `shares` that have been slashed on EigenLayer but not burned yet.
uint256 public burnableETHShares;

/// @notice The address that can set proof timestamps
address public proofTimestampSetter;

/// @notice The timestamp of the Pectra proof
uint64 public pectraForkTimestamp;

constructor(IETHPOSDeposit _ethPOS, IBeacon _eigenPodBeacon, IDelegationManager _delegationManager) {
ethPOS = _ethPOS;
eigenPodBeacon = _eigenPodBeacon;
Expand All @@ -102,5 +108,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[42] private __gap;
uint256[41] private __gap;
}
2 changes: 2 additions & 0 deletions src/test/harnesses/EigenPodHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ contract EigenPodHarness is EigenPod {
}

function verifyWithdrawalCredentials(
uint64 beaconTimestamp,
bytes32 beaconStateRoot,
uint40 validatorIndex,
bytes calldata validatorFieldsProof,
bytes32[] calldata validatorFields
) public returns (uint256) {
return _verifyWithdrawalCredentials(
beaconTimestamp,
beaconStateRoot,
validatorIndex,
validatorFieldsProof,
Expand Down
20 changes: 16 additions & 4 deletions src/test/integration/IntegrationDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
cheats.warp(BEACON_GENESIS_TIME);
timeMachine = new TimeMachine();
beaconChain = new BeaconChainMock(eigenPodManager, BEACON_GENESIS_TIME);

// Set the `pectraForkTimestamp` on the EigenPodManager. Use pectra state
cheats.startPrank(executorMultisig);
eigenPodManager.setProofTimestampSetter(executorMultisig);
eigenPodManager.setPectraForkTimestamp(BEACON_GENESIS_TIME);
cheats.stopPrank();
}

/// Parse existing contracts from mainnet
Expand Down Expand Up @@ -251,6 +257,12 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
// prior to test. `isUpgraded` is true by default, but is set to false in `UpgradeTest.t.sol`
if (isUpgraded) {
_upgradeMainnetContracts();

// Set the `pectraForkTimestamp` on the EigenPodManager. Use pectra state
cheats.startPrank(executorMultisig);
eigenPodManager.setProofTimestampSetter(executorMultisig);
eigenPodManager.setPectraForkTimestamp(BEACON_GENESIS_TIME);
cheats.stopPrank();
}
}

Expand Down Expand Up @@ -692,8 +704,8 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
tokenBalances = new uint[](1);

// Award the user with a random amount of ETH
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 5)
uint amount = 32 ether * _randUint({min: 1, max: 5});
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 2080)
uint amount = 32 ether * _randUint({min: 1, max: 65});
cheats.deal(address(user), amount);

strategies[0] = BEACONCHAIN_ETH_STRAT;
Expand All @@ -716,8 +728,8 @@ abstract contract IntegrationDeployer is ExistingDeploymentParser {
}

// Award the user with a random amount of ETH
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 5)
uint amount = 32 ether * _randUint({min: 1, max: 5});
// This guarantees a multiple of 32 ETH (at least 1, up to/incl 2080)
uint amount = 32 ether * _randUint({min: 1, max: 65});
cheats.deal(address(user), amount);

// Add BEACONCHAIN_ETH_STRAT and eth balance
Expand Down
Loading
Loading