From 4257dad9b84941a15e4849ce95ef3f5fbb5b968a Mon Sep 17 00:00:00 2001 From: wadealexc Date: Tue, 22 Apr 2025 18:23:54 +0000 Subject: [PATCH 1/8] feat: basic pectra compatibility --- src/contracts/interfaces/IEigenPod.sol | 69 +++++++++ src/contracts/pods/EigenPod.sol | 139 +++++++++++++++--- .../pods/EigenPodPausingConstants.sol | 4 + 3 files changed, 195 insertions(+), 17 deletions(-) diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 2192864015..42e65c795b 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -59,6 +59,19 @@ interface IEigenPodErrors { /// @dev Thrown when a validator has not been slashed on the beacon chain. error ValidatorNotSlashedOnBeaconChain(); + /// Consolidation and Withdrawal Requests + + /// @dev Thrown when a consolidation request is initiated where src == target + error SourceEqualsTarget(); + /// @dev Thrown when a predeploy request is initiated with insufficient msg.value + error InsufficientFunds(); + /// @dev Thrown when refunding excess fees from a predeploy fails + error RefundFailed(); + /// @dev Thrown when calling the predeploy fails + error PredeployFailed(); + /// @dev Thrown when querying a predeploy for its current fee fails + error FeeQueryFailed(); + /// Misc /// @dev Thrown when an invalid block root is returned by the EIP-4788 oracle. @@ -97,6 +110,16 @@ interface IEigenPodTypes { int64 balanceDeltasGwei; uint64 prevBeaconBalanceGwei; } + + struct ConsolidationRequest { + bytes srcPubkey; + bytes targetPubkey; + } + + struct WithdrawalRequest { + bytes pubkey; + uint64 amount; + } } interface IEigenPodEvents is IEigenPodTypes { @@ -249,6 +272,42 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { BeaconChainProofs.ValidatorProof calldata proof ) external; + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// consolidate their validators on the beacon chain. + /// @param requests An array of requests consisting of the source and target pubkeys + /// of the validators to be consolidated + /// @dev Both the source and target validator MUST have active withdrawal credentials + /// pointed at the pod + /// @dev The consolidation request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getConsolidationRequestFee` to query the current fee for + /// a single request. If submitting multiple requests, be aware that the predeploy uses + /// an exponential to calculate subsequent fees. You will have to calculate the total cost + /// for all requests offchain. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for reference) + function requestConsolidation( + ConsolidationRequest[] calldata requests + ) external payable; + + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// withdraw funds from validators on the beacon chain. + /// @param requests An array of requests consisting of the source validator and an + /// amount to withdraw + /// @dev The withdrawal request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getWithdrawalRequestFee` to query the current fee for + /// a single request. If submitting multiple requests, be aware that the predeploy uses + /// an exponential to calculate subsequent fees. You will have to calculate the total cost + /// for all requests offchain. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for reference) + function requestWithdrawal( + WithdrawalRequest[] calldata requests + ) external payable; + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; @@ -355,4 +414,14 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { function getParentBlockRoot( uint64 timestamp ) external view returns (bytes32); + + /// @notice Returns the current fee required to add a consolidation request to the EIP-7251 predeploy. + /// @dev Note that this getter only returns the fee required to perform a single request. For multiple + /// requests, see https://eips.ethereum.org/EIPS/eip-7251#fee-calculation + function getConsolidationRequestFee() external view returns (uint256); + + /// @notice Returns the current fee required to add a withdrawal request to the EIP-7002 predeploy. + /// @dev Note that this getter only returns the fee required to perform a single request. For multiple + /// requests, see https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule + function getWithdrawalRequestFee() external view returns (uint256); } diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index a7c10359e7..df2e07b8c2 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -47,6 +47,14 @@ contract EigenPod is /// (See https://eips.ethereum.org/EIPS/eip-4788) address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + /// @notice The address of the EIP-7002 withdrawal request predeploy + /// (See https://eips.ethereum.org/EIPS/eip-7002) + address internal constant WITHDRAWAL_REQUEST_ADDRESS = 0x00000961Ef480Eb55e80D19ad83579A64c007002; + + /// @notice The address of the EIP-7251 consolidation request predeploy + /// (See https://eips.ethereum.org/EIPS/eip-7251) + address internal constant CONSOLIDATION_REQUEST_ADDRESS = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; + /// @notice The length of the EIP-4788 beacon block root ring buffer uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; @@ -365,6 +373,62 @@ contract EigenPod is _startCheckpoint(false); } + /// @dev Input is the concatenated src and target pubkeys: [srcPubkey | targetPubkey] + /// Since each pubkey is 48 bytes, total length should be 96 bytes + function requestConsolidation( + ConsolidationRequest[] calldata requests + ) external payable onlyWhenNotPaused(PAUSED_CONSOLIDATIONS) onlyOwnerOrProofSubmitter { + uint256 fundsAvailable = msg.value; + + for (uint256 i = 0; i < requests.length; i++) { + ConsolidationRequest calldata request = requests[i]; + /// Validate pubkeys are unique and well-formed + require(request.srcPubkey.length == 48, InvalidPubKeyLength()); + require(request.targetPubkey.length == 48, InvalidPubKeyLength()); + require(keccak256(request.srcPubkey) != keccak256(request.targetPubkey), SourceEqualsTarget()); + + /// Ensure both src and target have verified withdrawal credentials pointed at this pod + ValidatorInfo memory src = validatorPubkeyToInfo(request.srcPubkey); + ValidatorInfo memory target = validatorPubkeyToInfo(request.targetPubkey); + require(src.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + + fundsAvailable = _callPredeploy({ + predeploy: CONSOLIDATION_REQUEST_ADDRESS, + callData: bytes.concat(request.srcPubkey, request.targetPubkey), + fundsAvailable: fundsAvailable + }); + } + + /// Refund remainder of fundsAvailable + (bool ok,) = msg.sender.call{value: fundsAvailable}(""); + require(ok, RefundFailed()); + } + + function requestWithdrawal( + WithdrawalRequest[] calldata requests + ) external payable onlyWhenNotPaused(PAUSED_WITHDRAWAL_REQUESTS) onlyOwnerOrProofSubmitter { + uint256 fundsAvailable = msg.value; + + /// Initiate each withdrawal request, decrementing available funds each time + for (uint256 i = 0; i < requests.length; i++) { + WithdrawalRequest calldata request = requests[i]; + /// Validate pubkey is well-formed. It's not necessary to perform any additional validation, + /// as the worst-case scenario is just that the request does not go through. + require(request.pubkey.length == 48, InvalidPubKeyLength()); + + fundsAvailable = _callPredeploy({ + predeploy: WITHDRAWAL_REQUEST_ADDRESS, + callData: abi.encodePacked(request.pubkey, request.amount), + fundsAvailable: fundsAvailable + }); + } + + /// Refund remainder of fundsAvailable + (bool ok,) = msg.sender.call{value: fundsAvailable}(""); + require(ok, RefundFailed()); + } + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens( IERC20[] memory tokenList, @@ -679,6 +743,29 @@ contract EigenPod is }); } + /// @dev Call a predeploy with the given calldata + /// @param predeploy the address of either the EIP-7002 or EIP-7251 predeploy + /// @param callData the data to send to the predeploy + /// @param fundsAvailable the amount of funds available to cover the predeploy fee + /// @return remainder the amount of funds left over after paying the predeploy fee + function _callPredeploy( + address predeploy, + bytes memory callData, + uint256 fundsAvailable + ) internal returns (uint256 remainder) { + /// Ensure fundsAvailable is enough to satisfy the current predeploy fee + uint256 fee = _getFee(predeploy); + require(fundsAvailable >= fee, InsufficientFunds()); + remainder = fundsAvailable - fee; + + /// Call predeploy + (bool ok,) = predeploy.call{value: fee}(callData); + require(ok, PredeployFailed()); + + /// Return unspent funds + return remainder; + } + function _podWithdrawalCredentials() internal view returns (bytes memory) { return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } @@ -688,13 +775,39 @@ contract EigenPod is } ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec - function _calculateValidatorPubkeyHash( + function _calcPubkeyHash( bytes memory validatorPubkey ) internal pure returns (bytes32) { require(validatorPubkey.length == 48, InvalidPubKeyLength()); return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); } + /// @dev Returns the current fee required to query either the EIP-7002 or EIP-7251 predeploy + function _getFee( + address predeploy + ) internal view returns (uint256) { + (bool success, bytes memory result) = predeploy.staticcall(""); + require(success && result.length == 32, FeeQueryFailed()); + + return uint256(bytes32(result)); + } + + /// @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; + } + /** * * VIEW FUNCTIONS @@ -716,8 +829,8 @@ contract EigenPod is /// @notice Returns the validatorInfo for a given validatorPubkey function validatorPubkeyToInfo( bytes calldata validatorPubkey - ) external view returns (ValidatorInfo memory) { - return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + ) public view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calcPubkeyHash(validatorPubkey)]; } function validatorStatus( @@ -730,7 +843,7 @@ contract EigenPod is function validatorStatus( bytes calldata validatorPubkey ) external view returns (VALIDATOR_STATUS) { - bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + bytes32 validatorPubkeyHash = _calcPubkeyHash(validatorPubkey); return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; } @@ -754,19 +867,11 @@ contract EigenPod is 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()); + function getConsolidationRequestFee() external view returns (uint256) { + return _getFee(CONSOLIDATION_REQUEST_ADDRESS); + } - /// 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; + function getWithdrawalRequestFee() external view returns (uint256) { + return _getFee(WITHDRAWAL_REQUEST_ADDRESS); } } diff --git a/src/contracts/pods/EigenPodPausingConstants.sol b/src/contracts/pods/EigenPodPausingConstants.sol index 0c1d337c80..afec0903a4 100644 --- a/src/contracts/pods/EigenPodPausingConstants.sol +++ b/src/contracts/pods/EigenPodPausingConstants.sol @@ -31,4 +31,8 @@ abstract contract EigenPodPausingConstants { uint8 internal constant PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS = 7; uint8 internal constant PAUSED_VERIFY_STALE_BALANCE = 8; + + uint8 internal constant PAUSED_CONSOLIDATIONS = 9; + + uint8 internal constant PAUSED_WITHDRAWAL_REQUESTS = 10; } From c40144f88a2d5e227236b20f4baa5f33687713fc Mon Sep 17 00:00:00 2001 From: wadealexc Date: Tue, 22 Apr 2025 20:00:06 +0000 Subject: [PATCH 2/8] wip: pectra compatibility tests --- ...G_deploy_from_scratch_deployment_data.json | 52 ------ src/contracts/interfaces/IEigenPod.sol | 14 +- src/contracts/pods/EigenPod.sol | 71 +++----- .../integration/mocks/EIP_7002_Mock.t.sol | 156 ++++++++++++++++++ .../integration/mocks/EIP_7251_Mock.t.sol | 31 ++++ src/test/mocks/EigenPodMock.sol | 21 +-- 6 files changed, 224 insertions(+), 121 deletions(-) delete mode 100644 script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json create mode 100644 src/test/integration/mocks/EIP_7002_Mock.t.sol create mode 100644 src/test/integration/mocks/EIP_7251_Mock.t.sol diff --git a/script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json b/script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json deleted file mode 100644 index b83fcb7d80..0000000000 --- a/script/output/devnet/SLASHING_deploy_from_scratch_deployment_data.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "addresses": { - "allocationManager": "0xAbD5Dd30CaEF8598d4EadFE7D45Fd582EDEade15", - "allocationManagerImplementation": "0xBFF7154bAa41e702E78Fb082a8Ce257Ce13E6f55", - "avsDirectory": "0xCa839541648D3e23137457b1Fd4A06bccEADD33a", - "avsDirectoryImplementation": "0x1362e9Cb37831C433095f1f1568215B7FDeD37Ef", - "baseStrategyImplementation": "0x61C6A250AEcAbf6b5e4611725b4f99C4DC85DB34", - "delegationManager": "0x3391eBafDD4b2e84Eeecf1711Ff9FC06EF9Ed182", - "delegationManagerImplementation": "0x4073a9B0fb0f31420C2A2263fB6E9adD33ea6F2A", - "eigenLayerPauserReg": "0xBb02ACE793e921D6a454062D2933064F31Fae0B2", - "eigenLayerProxyAdmin": "0xBf0c97a7df334BD83e0912c1218E44FD7953d122", - "eigenPodBeacon": "0x8ad244c2a986e48862c5bE1FdCA27cef0aaa6E15", - "eigenPodImplementation": "0x93cecf40F05389E99e163539F8d1CCbd4267f9A7", - "eigenPodManager": "0x8C9781FD55c67CE4DC08e3035ECbdB2B67a07307", - "eigenPodManagerImplementation": "0x3013B13BF3a464ff9078EFa40b7dbfF8fA67138d", - "emptyContract": "0x689CEE9134e4234caEF6c15Bf1D82779415daFAe", - "rewardsCoordinator": "0xa7DB7B0E63B5B75e080924F9C842758711177c07", - "rewardsCoordinatorImplementation": "0x0e93df1A21CA53F93160AbDee19A92A20f8b397B", - "strategies": [ - { - "strategy_address": "0x4f812633943022fA97cb0881683aAf9f318D5Caa", - "token_address": "0x94373a4919B3240D86eA41593D5eBa789FEF3848", - "token_symbol": "WETH" - } - ], - "strategyBeacon": "0x957c04A5666079255fD75220a15918ecBA6039c6", - "strategyFactory": "0x09F8f1c1ca1815083a8a05E1b4A0c65EFB509141", - "strategyFactoryImplementation": "0x8b1F09f8292fd658Da35b9b3b1d4F7d1C0F3F592", - "strategyManager": "0x70f8bC2Da145b434de66114ac539c9756eF64fb3", - "strategyManagerImplementation": "0x1562BfE7Cb4644ff030C1dE4aA5A9aBb88a61aeC", - "token": { - "tokenProxyAdmin": "0x0000000000000000000000000000000000000000", - "EIGEN": "0x0000000000000000000000000000000000000000", - "bEIGEN": "0x0000000000000000000000000000000000000000", - "EIGENImpl": "0x0000000000000000000000000000000000000000", - "bEIGENImpl": "0x0000000000000000000000000000000000000000", - "eigenStrategy": "0x0000000000000000000000000000000000000000", - "eigenStrategyImpl": "0x0000000000000000000000000000000000000000" - } - }, - "chainInfo": { - "chainId": 17000, - "deploymentBlock": 2548240 - }, - "parameters": { - "communityMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "executorMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "operationsMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "pauserMultisig": "0xBB37b72F67A410B76Ce9b9aF9e37aa561B1C5B07", - "timelock": "0x0000000000000000000000000000000000000000" - } -} \ No newline at end of file diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 42e65c795b..54ebee11d5 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -282,11 +282,10 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// this is pulled from msg.value. After submitting all requests, any remaining fee is /// refunded to the caller by calling its fallback function. /// @dev This contract exposes `getConsolidationRequestFee` to query the current fee for - /// a single request. If submitting multiple requests, be aware that the predeploy uses - /// an exponential to calculate subsequent fees. You will have to calculate the total cost - /// for all requests offchain. + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to fee * requests.length. This fee is updated at the end of each block. /// - /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for reference) + /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for details) function requestConsolidation( ConsolidationRequest[] calldata requests ) external payable; @@ -299,11 +298,10 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// this is pulled from msg.value. After submitting all requests, any remaining fee is /// refunded to the caller by calling its fallback function. /// @dev This contract exposes `getWithdrawalRequestFee` to query the current fee for - /// a single request. If submitting multiple requests, be aware that the predeploy uses - /// an exponential to calculate subsequent fees. You will have to calculate the total cost - /// for all requests offchain. + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to fee * requests.length. This fee is updated at the end of each block. /// - /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for reference) + /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for details) function requestWithdrawal( WithdrawalRequest[] calldata requests ) external payable; diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index df2e07b8c2..003a523468 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -373,12 +373,13 @@ contract EigenPod is _startCheckpoint(false); } - /// @dev Input is the concatenated src and target pubkeys: [srcPubkey | targetPubkey] - /// Since each pubkey is 48 bytes, total length should be 96 bytes + /// @inheritdoc: IEigenPod function requestConsolidation( ConsolidationRequest[] calldata requests ) external payable onlyWhenNotPaused(PAUSED_CONSOLIDATIONS) onlyOwnerOrProofSubmitter { - uint256 fundsAvailable = msg.value; + uint256 fee = getConsolidationRequestFee(); + require(msg.value >= fee * requests.length, InsufficientFunds()); + uint256 remainder = msg.value - (fee * requests.length); for (uint256 i = 0; i < requests.length; i++) { ConsolidationRequest calldata request = requests[i]; @@ -393,22 +394,25 @@ contract EigenPod is require(src.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); - fundsAvailable = _callPredeploy({ - predeploy: CONSOLIDATION_REQUEST_ADDRESS, - callData: bytes.concat(request.srcPubkey, request.targetPubkey), - fundsAvailable: fundsAvailable - }); + /// Call the predeploy + bytes memory callData = bytes.concat(request.srcPubkey, request.targetPubkey); + (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); + require(ok, PredeployFailed()); } - /// Refund remainder of fundsAvailable - (bool ok,) = msg.sender.call{value: fundsAvailable}(""); - require(ok, RefundFailed()); + /// Refund remainder of msg.value + if (remainder > 0) { + Address.sendValue(payable(msg.sender), remainder); + } } + /// @inheritdoc: IEigenPod function requestWithdrawal( WithdrawalRequest[] calldata requests ) external payable onlyWhenNotPaused(PAUSED_WITHDRAWAL_REQUESTS) onlyOwnerOrProofSubmitter { - uint256 fundsAvailable = msg.value; + uint256 fee = getWithdrawalRequestFee(); + require(msg.value >= fee * requests.length, InsufficientFunds()); + uint256 remainder = msg.value - (fee * requests.length); /// Initiate each withdrawal request, decrementing available funds each time for (uint256 i = 0; i < requests.length; i++) { @@ -417,16 +421,16 @@ contract EigenPod is /// as the worst-case scenario is just that the request does not go through. require(request.pubkey.length == 48, InvalidPubKeyLength()); - fundsAvailable = _callPredeploy({ - predeploy: WITHDRAWAL_REQUEST_ADDRESS, - callData: abi.encodePacked(request.pubkey, request.amount), - fundsAvailable: fundsAvailable - }); + /// Call the predeploy + bytes memory callData = abi.encodePacked(request.pubkey, request.amount); + (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); + require(ok, PredeployFailed()); } - /// Refund remainder of fundsAvailable - (bool ok,) = msg.sender.call{value: fundsAvailable}(""); - require(ok, RefundFailed()); + /// Refund remainder of msg.value + if (remainder > 0) { + Address.sendValue(payable(msg.sender), remainder); + } } /// @notice called by owner of a pod to remove any ERC20s deposited in the pod @@ -743,29 +747,6 @@ contract EigenPod is }); } - /// @dev Call a predeploy with the given calldata - /// @param predeploy the address of either the EIP-7002 or EIP-7251 predeploy - /// @param callData the data to send to the predeploy - /// @param fundsAvailable the amount of funds available to cover the predeploy fee - /// @return remainder the amount of funds left over after paying the predeploy fee - function _callPredeploy( - address predeploy, - bytes memory callData, - uint256 fundsAvailable - ) internal returns (uint256 remainder) { - /// Ensure fundsAvailable is enough to satisfy the current predeploy fee - uint256 fee = _getFee(predeploy); - require(fundsAvailable >= fee, InsufficientFunds()); - remainder = fundsAvailable - fee; - - /// Call predeploy - (bool ok,) = predeploy.call{value: fee}(callData); - require(ok, PredeployFailed()); - - /// Return unspent funds - return remainder; - } - function _podWithdrawalCredentials() internal view returns (bytes memory) { return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } @@ -867,11 +848,11 @@ contract EigenPod is return abi.decode(result, (bytes32)); } - function getConsolidationRequestFee() external view returns (uint256) { + function getConsolidationRequestFee() public view returns (uint256) { return _getFee(CONSOLIDATION_REQUEST_ADDRESS); } - function getWithdrawalRequestFee() external view returns (uint256) { + function getWithdrawalRequestFee() public view returns (uint256) { return _getFee(WITHDRAWAL_REQUEST_ADDRESS); } } diff --git a/src/test/integration/mocks/EIP_7002_Mock.t.sol b/src/test/integration/mocks/EIP_7002_Mock.t.sol new file mode 100644 index 0000000000..6d77c91379 --- /dev/null +++ b/src/test/integration/mocks/EIP_7002_Mock.t.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +struct WithdrawalRequest { + address source; + bytes validatorPubkey; + uint64 amount; +} + +contract EIP_7002_Mock { + + address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + uint constant EXCESS_INHIBITOR = type(uint).max; + uint constant MIN_WITHDRAWAL_REQUEST_FEE = 1; + uint constant WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION = 17; + + uint MAX_WITHDRAWAL_REQUESTS_PER_BLOCK = 16; + uint TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK = 2; + + uint excessRequests; + uint withdrawalRequestCount; + + uint queueHead; + uint queueTail; + mapping(uint => WithdrawalRequest) requestQueue; + + fallback() external payable { + if (msg.sender == SYSTEM_ADDRESS) { + _doReadRequests(); + } else if (msg.data.length == 0) { + _doGetFee(); + } else if (msg.data.length == 56) { + _doAddRequest(); + } else { + revert("EIP_7002_Mock: unknown function"); + } + } + + /** + * + * "PUBLIC" METHODS + * + */ + + function _doAddRequest() internal { + uint fee = _getFee(); + require(msg.value >= fee, "EIP_7002_Mock: insuffient value for fee"); + + withdrawalRequestCount++; + + bytes memory pubkey = new bytes(48); + uint64 amount; + assembly { + calldatacopy(add(32, pubkey), 0, 48) // copy bytes48 pubkey into memory + calldatacopy(24, 48, 8) // copy uint64 amount to memory[0] + amount := mload(0) + } + + WithdrawalRequest storage request = requestQueue[queueTail]; + request.source = msg.sender; + request.validatorPubkey = pubkey; + request.amount = amount; + + queueTail++; + } + + function _doGetFee() internal view { + uint fee = _getFee(); + + assembly { + mstore(0, fee) + return(0, 32) + } + } + + /// (see https://eips.ethereum.org/EIPS/eip-7002#system-call) + function _doReadRequests() internal { + WithdrawalRequest[] memory reqs = _dequeueWithdrawalRequests(); + _updateExcessWithdrawalRequests(); + _resetWithdrawalRequestCount(); + + bytes memory returnData = abi.encode(reqs); + assembly { return(add(32, returnData), mload(returnData)) } + } + + /** + * + * PRIVATE METHODS + * + */ + + function _dequeueWithdrawalRequests() internal returns (WithdrawalRequest[] memory reqs) { + uint numInQueue = queueTail - queueHead; + uint numToDequeue = + numInQueue > MAX_WITHDRAWAL_REQUESTS_PER_BLOCK ? MAX_WITHDRAWAL_REQUESTS_PER_BLOCK : numInQueue; + + reqs = new WithdrawalRequest[](numToDequeue); + for (uint i = 0; i < numToDequeue; i++) { + reqs[i] = requestQueue[queueHead + i]; + } + + uint newQueueHead = queueHead + numToDequeue; + if (newQueueHead == queueTail) { + queueHead = 0; + queueTail = 0; + } else { + queueHead = newQueueHead; + } + + return reqs; + } + + function _updateExcessWithdrawalRequests() internal { + uint prevExcess = excessRequests; + if (prevExcess == EXCESS_INHIBITOR) { + prevExcess = 0; + } + + uint newExcess = 0; + if (prevExcess + withdrawalRequestCount > TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK) { + newExcess = prevExcess + withdrawalRequestCount - TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK; + } + + excessRequests = newExcess; + } + + function _resetWithdrawalRequestCount() internal { + withdrawalRequestCount = 0; + } + + function _getFee() private view returns (uint) { + require(excessRequests != EXCESS_INHIBITOR, "EIP_7002_Mock: excess inhibitor reached"); + + return _fakeExponential({ + factor: MIN_WITHDRAWAL_REQUEST_FEE, + numerator: excessRequests, + denominator: WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION + }); + } + + /// (see https://eips.ethereum.org/EIPS/eip-7002#fee-calculation) + function _fakeExponential(uint factor, uint numerator, uint denominator) private pure returns (uint) { + uint i = 1; + uint output = 0; + uint numeratorAccum = factor * denominator; + + while (numeratorAccum > 0) { + output += numeratorAccum; + numeratorAccum = (numeratorAccum * numerator) / (denominator * i); + i++; + } + + return output / denominator; + } +} diff --git a/src/test/integration/mocks/EIP_7251_Mock.t.sol b/src/test/integration/mocks/EIP_7251_Mock.t.sol new file mode 100644 index 0000000000..a7f77dd52a --- /dev/null +++ b/src/test/integration/mocks/EIP_7251_Mock.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +contract EIP_7251_Mock { + mapping(uint => bytes32) blockRoots; + + uint constant HISTORY_BUFFER_LENGTH = 8191; + + fallback() external { + require(msg.data.length == 32, "4788OracleMock.fallback: malformed msg.data"); + + uint timestamp = abi.decode(msg.data, (uint)); + require(timestamp != 0, "4788OracleMock.fallback: timestamp is 0"); + + bytes32 blockRoot = blockRoots[timestamp]; + require(blockRoot != 0, "4788OracleMock.fallback: no block root found. DID YOU USE CHEATS.WARP?"); + + assembly { + mstore(0, blockRoot) + return(0, 32) + } + } + + function timestampToBlockRoot(uint timestamp) public view returns (bytes32) { + return blockRoots[uint64(timestamp)]; + } + + function setBlockRoot(uint64 timestamp, bytes32 blockRoot) public { + blockRoots[timestamp] = blockRoot; + } +} diff --git a/src/test/mocks/EigenPodMock.sol b/src/test/mocks/EigenPodMock.sol index 42fdb4d526..89b08fa63a 100644 --- a/src/test/mocks/EigenPodMock.sol +++ b/src/test/mocks/EigenPodMock.sol @@ -8,8 +8,6 @@ import "../../contracts/mixins/SemVerMixin.sol"; contract EigenPodMock is IEigenPod, SemVerMixin, Test { constructor() SemVerMixin("v9.9.9") {} - function nonBeaconChainETHBalanceWei() external view returns (uint) {} - /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer), function withdrawableRestakedExecutionLayerGwei() external view returns (uint64) {} @@ -33,12 +31,6 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { /// @notice The owner of this EigenPod function podOwner() external view returns (address) {} - /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. - function hasRestaked() external view returns (bool) {} - - /// @notice block timestamp of the most recent withdrawal - function mostRecentWithdrawalTimestamp() external view returns (uint64) {} - /// @notice Returns the validatorInfo struct for the provided pubkeyHash function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) {} @@ -80,14 +72,8 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { bytes32[][] calldata validatorFields ) external {} - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false - function activateRestaking() external {} - - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false - function withdrawBeforeRestaking() external {} - - /// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei - function withdrawNonBeaconChainETHBalanceWei(address recipient, uint amountToWithdraw) external {} + function requestConsolidation(ConsolidationRequest[] calldata) external payable {} + function requestWithdrawal(WithdrawalRequest[] calldata) external payable {} /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint[] memory amountsToWithdraw, address recipient) external {} @@ -106,4 +92,7 @@ contract EigenPodMock is IEigenPod, SemVerMixin, Test { function getParentBlockRoot(uint64 timestamp) external view returns (bytes32) {} function getPectraForkTimestamp() external view returns (uint64) {} + + function getConsolidationRequestFee() external view returns (uint) {} + function getWithdrawalRequestFee() external view returns (uint) {} } From 105faf1c37ac673dfb914facad8b882790bd7e2e Mon Sep 17 00:00:00 2001 From: wadealexc Date: Thu, 24 Apr 2025 20:30:04 +0000 Subject: [PATCH 3/8] wip: update documentation and start mocking predeploys --- src/contracts/interfaces/IEigenPod.sol | 62 +++++++- src/contracts/pods/EigenPod.sol | 20 +-- .../integration/mocks/BeaconChainMock.t.sol | 10 +- .../integration/mocks/EIP_7002_Mock.t.sol | 3 +- .../integration/mocks/EIP_7251_Mock.t.sol | 146 ++++++++++++++++-- 5 files changed, 214 insertions(+), 27 deletions(-) diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 54ebee11d5..1bf9471fd5 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -283,9 +283,39 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// refunded to the caller by calling its fallback function. /// @dev This contract exposes `getConsolidationRequestFee` to query the current fee for /// a single request. If submitting multiple requests in a single block, the total fee - /// is equal to fee * requests.length. This fee is updated at the end of each block. + /// is equal to (fee * requests.length). This fee is updated at the end of each block. /// /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for details) + /// + /// @dev Note on beacon chain behavior: + /// - If request.srcPubkey == request.targetPubkey, this is a "switch" consolidation. Once + /// processed on the beacon chain, the validator's withdrawal credentials will be changed + /// to compounding (0x02). + /// - The rest of the notes assume src != target. + /// - The target validator MUST already have 0x02 credentials. The source validator can have either. + /// - Consoldiation sets the source validator's exit_epoch and withdrawable_epoch, similar to an exit. + /// When the exit epoch is reached, an epoch sweep will process the consolidation and transfer balance + /// from the source to the target validator. + /// - Consolidation transfers min(srcValidator.effective_balance, state.balance[srcIndex]) to the target. + /// This may not be the entirety of the source validator's balance; any remainder will be moved to the + /// pod when hit by a subsequent withdrawal sweep. + /// + /// @dev Note that consolidation requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - If request.srcPubkey == request.targetPubkey, the validator MUST have 0x01 credentials + /// - If request.srcPubkey != request.targetPubkey, the target validator MUST have 0x02 credentials + /// - Both the source and target validators MUST be active and MUST NOT have initiated exits + /// - The source validator MUST NOT have pending partial withdrawal requests (via `requestWithdrawal`) + /// - If the source validator is slashed after requesting consolidation (but before processing), + /// the consolidation will be skipped. + /// + /// For further reference, see consolidation processing at block and epoch boundaries: + /// - Block: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_consolidation_request + /// - Epoch: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_pending_consolidations function requestConsolidation( ConsolidationRequest[] calldata requests ) external payable; @@ -299,9 +329,37 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// refunded to the caller by calling its fallback function. /// @dev This contract exposes `getWithdrawalRequestFee` to query the current fee for /// a single request. If submitting multiple requests in a single block, the total fee - /// is equal to fee * requests.length. This fee is updated at the end of each block. + /// is equal to (fee * requests.length). This fee is updated at the end of each block. /// /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for details) + /// + /// @dev Note on beacon chain behavior: + /// - Withdrawal requests have two types: full exit requests, and partial exit requests. + /// Partial exit requests will be skipped if the validator has 0x01 withdrawal credentials. + /// If you want your validators to have access to partial exits, use `requestConsolidation` + /// to change their withdrawal credentials to compounding (0x02). + /// - If request.amount == 0, this is a FULL exit request. A full exit request initiates a + /// standard validator exit. + /// - Other amounts are treated as PARTIAL exit requests. A partial exit request will NOT result + /// in a validator with less than 32 ETH balance. Any requested amount above this is ignored. + /// - The actual amount withdrawn for a partial exit is given by the formula: + /// min(request.amount, state.balances[vIdx] - 32 ETH - pending_balance_to_withdraw) + /// (where `pending_balance_to_withdraw` is the sum of any outstanding partial exit requests) + /// (Note that this means you may request more than is actually withdrawn!) + /// + /// @dev Note that withdrawal requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - request.pubkey MUST be a valid validator pubkey + /// - request.pubkey MUST belong to a validator whose withdrawal credentials are this pod + /// - If request.amount is for a partial exit, the validator MUST have 0x02 withdrawal credentials + /// - If request.amount is for a full exit, the validator MUST NOT have any pending partial exits + /// - The validator MUST be active and MUST NOT have initiated exit + /// + /// For further reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_withdrawal_request function requestWithdrawal( WithdrawalRequest[] calldata requests ) external payable; diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 003a523468..742817a44c 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -373,7 +373,7 @@ contract EigenPod is _startCheckpoint(false); } - /// @inheritdoc: IEigenPod + /// @inheritdoc IEigenPod function requestConsolidation( ConsolidationRequest[] calldata requests ) external payable onlyWhenNotPaused(PAUSED_CONSOLIDATIONS) onlyOwnerOrProofSubmitter { @@ -383,15 +383,17 @@ contract EigenPod is for (uint256 i = 0; i < requests.length; i++) { ConsolidationRequest calldata request = requests[i]; - /// Validate pubkeys are unique and well-formed + /// Validate pubkeys are well-formed require(request.srcPubkey.length == 48, InvalidPubKeyLength()); require(request.targetPubkey.length == 48, InvalidPubKeyLength()); - require(keccak256(request.srcPubkey) != keccak256(request.targetPubkey), SourceEqualsTarget()); /// Ensure both src and target have verified withdrawal credentials pointed at this pod - ValidatorInfo memory src = validatorPubkeyToInfo(request.srcPubkey); + /// TODO - I'm fairly certain we don't need this check. verifyWC rejects validators + /// who have a nonzero exit epoch, so it shouldn't be possible to double-count shares + /// using a checkpoint + verifyWC. + // ValidatorInfo memory src = validatorPubkeyToInfo(request.srcPubkey); + // require(src.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); ValidatorInfo memory target = validatorPubkeyToInfo(request.targetPubkey); - require(src.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); /// Call the predeploy @@ -406,7 +408,7 @@ contract EigenPod is } } - /// @inheritdoc: IEigenPod + /// @inheritdoc IEigenPod function requestWithdrawal( WithdrawalRequest[] calldata requests ) external payable onlyWhenNotPaused(PAUSED_WITHDRAWAL_REQUESTS) onlyOwnerOrProofSubmitter { @@ -414,11 +416,11 @@ contract EigenPod is require(msg.value >= fee * requests.length, InsufficientFunds()); uint256 remainder = msg.value - (fee * requests.length); - /// Initiate each withdrawal request, decrementing available funds each time for (uint256 i = 0; i < requests.length; i++) { WithdrawalRequest calldata request = requests[i]; - /// Validate pubkey is well-formed. It's not necessary to perform any additional validation, - /// as the worst-case scenario is just that the request does not go through. + /// Validate pubkey is well-formed. + /// It's not necessary to perform any additional validation; the worst-case + /// as the worst-case scenario is just that the request require(request.pubkey.length == 48, InvalidPubKeyLength()); /// Call the predeploy diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index 09cd3642cd..a6d8a7bd9d 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -9,6 +9,8 @@ import "src/contracts/pods/EigenPodManager.sol"; import "src/test/mocks/ETHDepositMock.sol"; import "src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol"; +import "src/test/integration/mocks/EIP_7002_Mock.t.sol"; +import "src/test/integration/mocks/EIP_7251_Mock.t.sol"; import "src/test/utils/Logger.t.sol"; struct ValidatorFieldsProof { @@ -99,9 +101,13 @@ contract BeaconChainMock is Logger { uint64 public nextTimestamp; EigenPodManager eigenPodManager; + + /// Canonical beacon chain predeploy addresses IETHPOSDeposit constant DEPOSIT_CONTRACT = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); EIP_4788_Oracle_Mock constant EIP_4788_ORACLE = EIP_4788_Oracle_Mock(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02); - + EIP_7002_Mock constant WITHDRAWAL_PREDEPLOY = EIP_7002_Mock(payable(0x0000BBdDc7CE488642fb579F8B00f3a590007251)); + EIP_7251_Mock constant CONSOLIDATION_PREDEPLOY = EIP_7251_Mock(payable(0x00000961Ef480Eb55e80D19ad83579A64c007002)); + /** * BeaconState containers, used for proofgen: * https://eth2book.info/capella/part3/containers/state/#beaconstate @@ -149,6 +155,8 @@ contract BeaconChainMock is Logger { // Create mock 4788 oracle cheats.etch(address(DEPOSIT_CONTRACT), type(ETHPOSDepositMock).runtimeCode); cheats.etch(address(EIP_4788_ORACLE), type(EIP_4788_Oracle_Mock).runtimeCode); + cheats.etch(address(CONSOLIDATION_PREDEPLOY), type(EIP_7251_Mock).runtimeCode); + cheats.etch(address(WITHDRAWAL_PREDEPLOY), type(EIP_7002_Mock).runtimeCode); // Calculate nodes of empty merkle tree bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); diff --git a/src/test/integration/mocks/EIP_7002_Mock.t.sol b/src/test/integration/mocks/EIP_7002_Mock.t.sol index 6d77c91379..9c1f6b33a7 100644 --- a/src/test/integration/mocks/EIP_7002_Mock.t.sol +++ b/src/test/integration/mocks/EIP_7002_Mock.t.sol @@ -49,10 +49,9 @@ contract EIP_7002_Mock { withdrawalRequestCount++; - bytes memory pubkey = new bytes(48); + bytes memory pubkey = msg.data[0:48]; uint64 amount; assembly { - calldatacopy(add(32, pubkey), 0, 48) // copy bytes48 pubkey into memory calldatacopy(24, 48, 8) // copy uint64 amount to memory[0] amount := mload(0) } diff --git a/src/test/integration/mocks/EIP_7251_Mock.t.sol b/src/test/integration/mocks/EIP_7251_Mock.t.sol index a7f77dd52a..4c8e85e11d 100644 --- a/src/test/integration/mocks/EIP_7251_Mock.t.sol +++ b/src/test/integration/mocks/EIP_7251_Mock.t.sol @@ -1,31 +1,151 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; +struct ConsolidationRequest { + address source; + bytes sourcePubkey; + bytes targetPubkey; +} + contract EIP_7251_Mock { - mapping(uint => bytes32) blockRoots; - uint constant HISTORY_BUFFER_LENGTH = 8191; + address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + + uint constant EXCESS_INHIBITOR = type(uint).max; + uint constant MIN_CONSOLIDATION_REQUEST_FEE = 1; + uint constant CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION = 17; + + uint MAX_CONSOLIDATION_REQUESTS_PER_BLOCK = 2; + uint TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK = 1; + + uint excessRequests; + uint consolidationRequestCount; + + uint queueHead; + uint queueTail; + mapping(uint => ConsolidationRequest) requestQueue; + + fallback() external payable { + if (msg.sender == SYSTEM_ADDRESS) { + _doReadRequests(); + } else if (msg.data.length == 0) { + _doGetFee(); + } else if (msg.data.length == 96) { + _doAddRequest(); + } else { + revert("EIP_7251_Mock: unknown function"); + } + } + + /** + * + * "PUBLIC" METHODS + * + */ - fallback() external { - require(msg.data.length == 32, "4788OracleMock.fallback: malformed msg.data"); + function _doAddRequest() internal { + uint fee = _getFee(); + require(msg.value >= fee, "EIP_7251_Mock: insuffient value for fee"); - uint timestamp = abi.decode(msg.data, (uint)); - require(timestamp != 0, "4788OracleMock.fallback: timestamp is 0"); + consolidationRequestCount++; - bytes32 blockRoot = blockRoots[timestamp]; - require(blockRoot != 0, "4788OracleMock.fallback: no block root found. DID YOU USE CHEATS.WARP?"); + bytes memory sourcePubkey = msg.data[0:48]; + bytes memory targetPubkey = msg.data[48:]; + + ConsolidationRequest storage request = requestQueue[queueTail]; + request.source = msg.sender; + request.sourcePubkey = sourcePubkey; + request.targetPubkey = targetPubkey; + + queueTail++; + } + + function _doGetFee() internal view { + uint fee = _getFee(); assembly { - mstore(0, blockRoot) + mstore(0, fee) return(0, 32) } } - function timestampToBlockRoot(uint timestamp) public view returns (bytes32) { - return blockRoots[uint64(timestamp)]; + /// (see https://eips.ethereum.org/EIPS/eip-7251#system-call) + function _doReadRequests() internal { + ConsolidationRequest[] memory reqs = _dequeueConsolidationRequests(); + _updateExcessConsolidationRequests(); + _resetConsolidationRequestCount(); + + bytes memory returnData = abi.encode(reqs); + assembly { return(add(32, returnData), mload(returnData)) } } - function setBlockRoot(uint64 timestamp, bytes32 blockRoot) public { - blockRoots[timestamp] = blockRoot; + /** + * + * PRIVATE METHODS + * + */ + + function _dequeueConsolidationRequests() internal returns (ConsolidationRequest[] memory reqs) { + uint numInQueue = queueTail - queueHead; + uint numToDequeue = + numInQueue > MAX_CONSOLIDATION_REQUESTS_PER_BLOCK ? MAX_CONSOLIDATION_REQUESTS_PER_BLOCK : numInQueue; + + reqs = new ConsolidationRequest[](numToDequeue); + for (uint i = 0; i < numToDequeue; i++) { + reqs[i] = requestQueue[queueHead + i]; + } + + uint newQueueHead = queueHead + numToDequeue; + if (newQueueHead == queueTail) { + queueHead = 0; + queueTail = 0; + } else { + queueHead = newQueueHead; + } + + return reqs; + } + + function _updateExcessConsolidationRequests() internal { + uint prevExcess = excessRequests; + if (prevExcess == EXCESS_INHIBITOR) { + prevExcess = 0; + } + + uint newExcess = 0; + if (prevExcess + consolidationRequestCount > TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK) { + newExcess = prevExcess + consolidationRequestCount - TARGET_CONSOLIDATION_REQUESTS_PER_BLOCK; + } + + excessRequests = newExcess; + } + + function _resetConsolidationRequestCount() internal { + consolidationRequestCount = 0; + } + + function _getFee() private view returns (uint) { + require(excessRequests != EXCESS_INHIBITOR, "EIP_7251_Mock: excess inhibitor reached"); + + return _fakeExponential({ + factor: MIN_CONSOLIDATION_REQUEST_FEE, + numerator: excessRequests, + denominator: CONSOLIDATION_REQUEST_FEE_UPDATE_FRACTION + }); + } + + /// (see https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + function _fakeExponential(uint factor, uint numerator, uint denominator) private pure returns (uint) { + uint i = 1; + uint output = 0; + uint numeratorAccum = factor * denominator; + + while (numeratorAccum > 0) { + output += numeratorAccum; + numeratorAccum = (numeratorAccum * numerator) / (denominator * i); + i++; + } + + return output / denominator; } } From c80537374e7149ebebe4c12039c50651d34c4ba2 Mon Sep 17 00:00:00 2001 From: wadealexc Date: Mon, 28 Apr 2025 18:02:56 +0000 Subject: [PATCH 4/8] wip: beacon chain mock --- src/contracts/pods/EigenPod.sol | 24 ++-- .../integration/mocks/BeaconChainMock.t.sol | 115 +++++++++++++++--- .../mocks/BeaconChainMock_Deneb.t.sol | 2 +- 3 files changed, 109 insertions(+), 32 deletions(-) diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 742817a44c..6f88e2872d 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -383,26 +383,21 @@ contract EigenPod is for (uint256 i = 0; i < requests.length; i++) { ConsolidationRequest calldata request = requests[i]; - /// Validate pubkeys are well-formed + // Validate pubkeys are well-formed require(request.srcPubkey.length == 48, InvalidPubKeyLength()); require(request.targetPubkey.length == 48, InvalidPubKeyLength()); - /// Ensure both src and target have verified withdrawal credentials pointed at this pod - /// TODO - I'm fairly certain we don't need this check. verifyWC rejects validators - /// who have a nonzero exit epoch, so it shouldn't be possible to double-count shares - /// using a checkpoint + verifyWC. - // ValidatorInfo memory src = validatorPubkeyToInfo(request.srcPubkey); - // require(src.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); + // Ensure target has verified withdrawal credentials pointed at this pod ValidatorInfo memory target = validatorPubkeyToInfo(request.targetPubkey); require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); - /// Call the predeploy + // Call the predeploy bytes memory callData = bytes.concat(request.srcPubkey, request.targetPubkey); (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); require(ok, PredeployFailed()); } - /// Refund remainder of msg.value + // Refund remainder of msg.value if (remainder > 0) { Address.sendValue(payable(msg.sender), remainder); } @@ -418,18 +413,19 @@ contract EigenPod is for (uint256 i = 0; i < requests.length; i++) { WithdrawalRequest calldata request = requests[i]; - /// Validate pubkey is well-formed. - /// It's not necessary to perform any additional validation; the worst-case - /// as the worst-case scenario is just that the request + // Validate pubkey is well-formed. + // + // It's not necessary to perform any additional validation; the worst-case + // scenario is just that the consensus layer skips an invalid request. require(request.pubkey.length == 48, InvalidPubKeyLength()); - /// Call the predeploy + // Call the predeploy bytes memory callData = abi.encodePacked(request.pubkey, request.amount); (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); require(ok, PredeployFailed()); } - /// Refund remainder of msg.value + // Refund remainder of msg.value if (remainder > 0) { Address.sendValue(payable(msg.sender), remainder); } diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index a6d8a7bd9d..8af6bb3b4f 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -61,6 +61,9 @@ contract BeaconChainMock is Logger { uint64 effectiveBalanceGwei; uint64 activationEpoch; uint64 exitEpoch; + + // cumulative unprocessed withdraw requests + uint64 pendingBalanceToWithdrawGwei; } /// @dev The type of slash to apply to a validator @@ -82,6 +85,7 @@ contract BeaconChainMock is Logger { // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values uint public MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; uint64 public MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; + uint64 constant MIN_ACTIVATION_BALANCE_GWEI = 32 gwei; /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): /// @dev Non-constant values will change with the Pectra hard fork @@ -102,6 +106,9 @@ contract BeaconChainMock is Logger { EigenPodManager eigenPodManager; + // Used to call predeploys as the system + address constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE; + /// Canonical beacon chain predeploy addresses IETHPOSDeposit constant DEPOSIT_CONTRACT = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); EIP_4788_Oracle_Mock constant EIP_4788_ORACLE = EIP_4788_Oracle_Mock(0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02); @@ -316,6 +323,7 @@ contract BeaconChainMock is Logger { /// @dev Move forward one epoch on the beacon chain, taking care of important epoch processing: /// - Award ALL validators CONSENSUS_REWARD_AMOUNT + /// - Process any pending partial withdrawals (new in Pectra) /// - Withdraw any balance over Max EB /// - Withdraw any balance for exited validators /// - Effective balances updated (NOTE: we do not use hysteresis!) @@ -328,7 +336,9 @@ contract BeaconChainMock is Logger { /// - DOES withdraw in excess of Max EB / if validator is exited function advanceEpoch() public { print.method("advanceEpoch"); + _updateCurrentEpoch(); _generateRewards(); + _processWithdrawals(); _withdrawExcess(); _advanceEpoch(); } @@ -342,7 +352,8 @@ contract BeaconChainMock is Logger { /// - DOES withdraw in excess of Max EB / if validator is exited function advanceEpoch_NoRewards() public { print.method("advanceEpoch_NoRewards"); - _withdrawExcess(); + _updateCurrentEpoch(); + _processWithdrawals(); _advanceEpoch(); } @@ -356,12 +367,14 @@ contract BeaconChainMock is Logger { /// - does NOT withdraw if validator is exited function advanceEpoch_NoWithdraw() public { print.method("advanceEpoch_NoWithdraw"); + _updateCurrentEpoch(); _generateRewards(); _advanceEpoch(); } function advanceEpoch_NoWithdrawNoRewards() public { print.method("advanceEpoch_NoWithdrawNoRewards"); + _updateCurrentEpoch(); _advanceEpoch(); } @@ -386,6 +399,74 @@ contract BeaconChainMock is Logger { console.log(" - Generated rewards for %s of %s validators.", totalRewarded, validators.length); } + function _processWithdrawals() internal { + _process_EIP_7002_Requests(); + + // TODO - update this to also withdraw/reset pending amounts + _withdrawExcess(); + } + + /// @dev Handle pending partial withdrawals AND withdraw excess validator balance + function _process_EIP_7002_Requests() internal { + // Call EIP-7002 predeploy and dequeue any withdrawal requests + cheats.prank(SYSTEM_ADDRESS); + (bool ok, bytes memory data) = WITHDRAWAL_PREDEPLOY.call(); + require(ok, "BeaconChainMock._processWithdrawals: WITHDRAWAL_PREDEPLOY failed"); + + WithdrawalRequest[] memory requests = abi.decode(data, (WithdrawalRequest[])); + console.log(" - Dequeued %d withdrawal requests.", requests.length); + for (uint i = 0; i < requests.length; i++) { + WithdrawalRequest memory request = requests[i]; + // _createValidator sets each validator's pubkey to its index, so we can use it to look up here + bytes memory pubkey = request.validatorPubkey; + uint validatorIndex; + assembly { validatorIndex := mload(add(48, pubkey)) } + + // The CL would just skip this request, but we revert since it shouldn't be possible + // to have a validator with an invalid pubkey. + require(validatorIndex < validators.length, "BeaconChainMock._processWithdrawals: invalid pubkey"); + Validator storage v = validators[validatorIndex]; + address destination = _toAddress(v.withdrawalCreds); + + bool isFullExitRequest = request.amount == 0; + bool isCorrectSourceAddress = request.source == destination; + + uint64 balanceGwei = _currentBalanceGwei(validatorIndex); + bool hasExcessBalance = balanceGwei > MIN_ACTIVATION_BALANCE_GWEI + v.pendingBalanceToWithdrawGwei; + + string memory skipReason; + if (v.isDummy) { + skipReason = "dummy validator"; + } else if (!isCorrectSourceAddress) { + skipReason = "incorrect source address"; + } else if (!_isActiveAt(v, currentEpoch())) { + skipReason = "inactive validator"; + } else if (v.exitEpoch != FAR_FUTURE_EPOCH) { + skipReason = "exit in progress"; + } else if (isFullExitRequest && v.pendingBalanceToWithdrawGwei != 0) { + skipReason = "attempted full exit while pending withdrawal in queue"; + } else if (isFullExitRequest) { + exitValidator(validatorIndex); + continue; + } else if (!_hasCompoundingWithdrawalCredentials(v)) { + skipReason = "attempted partial exit without 0x02 credentials"; + } else if (!hasExcessBalance) { + skipReason = "validator does not have excess balance"; + } + + if (skipReason.length != 0) { + console.log(" -- Skipping request with reason: %s.", skipReason); + continue; + } + + // Partial withdrawal - only withdraw down to 32 ETH + uint64 toWithdrawGwei = balanceGwei - MIN_ACTIVATION_BALANCE_GWEI - v.pendingBalanceToWithdrawGwei; + toWithdrawGwei = toWithdrawGwei > request.amountGwei ? request.amountGwei : toWithdrawGwei; + + v.pendingBalanceToWithdrawGwei += toWithdrawGwei; + } + } + /// @dev Iterate over all validators. If the validator has > Max EB current balance /// OR is exited, withdraw the excess to the validator's withdrawal address. function _withdrawExcess() internal { @@ -423,8 +504,14 @@ contract BeaconChainMock is Logger { if (totalExcessWei != 0) console.log("- Withdrew excess balance:", totalExcessWei.asGwei()); } - function _advanceEpoch() public virtual { + function _updateCurrentEpoch() internal { + uint64 curEpoch = currentEpoch(); + cheats.warp(_nextEpochStartTimestamp(curEpoch)); + } + + function _advanceEpoch() internal virtual { cheats.pauseTracing(); + curTimestamp = uint64(block.timestamp); // Update effective balances for each validator for (uint i = 0; i < validators.length; i++) { @@ -438,18 +525,6 @@ contract BeaconChainMock is Logger { v.effectiveBalanceGwei = balanceGwei; } - // console.log(" Updated effective balances...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - - uint64 curEpoch = currentEpoch(); - cheats.warp(_nextEpochStartTimestamp(curEpoch)); - curTimestamp = uint64(block.timestamp); - - // console.log(" Jumping to next epoch...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - // console.log(" Building beacon state trees...".dim()); // Log total number of validators and number being processed for the first time @@ -538,7 +613,8 @@ contract BeaconChainMock is Logger { withdrawalCreds: "", effectiveBalanceGwei: dummyBalanceGwei, activationEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, - exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH + exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, + pendingBalanceToWithdrawGwei: 0 }) ); _setCurrentBalance(validatorIndex, dummyBalanceGwei); @@ -559,7 +635,8 @@ contract BeaconChainMock is Logger { withdrawalCreds: withdrawalCreds, effectiveBalanceGwei: balanceGwei, activationEpoch: currentEpoch(), - exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH + exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, + pendingBalanceToWithdrawGwei: 0 }) ); _setCurrentBalance(validatorIndex, balanceGwei); @@ -1042,6 +1119,10 @@ contract BeaconChainMock is Logger { } function isActive(uint40 validatorIndex) public view returns (bool) { - return validators[validatorIndex].exitEpoch == BeaconChainProofs.FAR_FUTURE_EPOCH; + return _isActiveAt(validators[validatorIndex], currentEpoch()); + } + + function _isActiveAt(Validator storage self, uint64 epoch) internal view returns (bool) { + return self.activationEpoch <= epoch && epoch < self.exitEpoch; } } diff --git a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol index aeb47f6e03..20baa0e3a4 100644 --- a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol +++ b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol @@ -37,7 +37,7 @@ contract BeaconChainMock_DenebForkable is BeaconChainMock { * INTERNAL FUNCTIONS * */ - function _advanceEpoch() public override { + function _advanceEpoch() internal override { cheats.pauseTracing(); // Update effective balances for each validator From 7c7e2b8b5eab7bf4af5ae76eae6b4ba78e640e39 Mon Sep 17 00:00:00 2001 From: wadealexc Date: Wed, 30 Apr 2025 15:32:20 +0000 Subject: [PATCH 5/8] wip: events and tests --- src/contracts/interfaces/IEigenPod.sol | 40 +- src/contracts/pods/EigenPod.sol | 17 +- .../integration/mocks/BeaconChainMock.t.sol | 343 ++++++++++-------- .../mocks/BeaconChainMock_Deneb.t.sol | 3 +- .../integration/mocks/EIP_7002_Mock.t.sol | 11 +- src/test/integration/mocks/LibValidator.t.sol | 98 +++++ .../tests/eigenpod/Pectra_Features.t.sol | 0 .../VerifyWC_StartCP_CompleteCP.t.sol | 3 +- src/test/integration/users/User.t.sol | 16 +- src/test/utils/EigenPodUser.t.sol | 4 - 10 files changed, 353 insertions(+), 182 deletions(-) create mode 100644 src/test/integration/mocks/LibValidator.t.sol create mode 100644 src/test/integration/tests/eigenpod/Pectra_Features.t.sol diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 1bf9471fd5..0742b45b88 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -61,8 +61,6 @@ interface IEigenPodErrors { /// Consolidation and Withdrawal Requests - /// @dev Thrown when a consolidation request is initiated where src == target - error SourceEqualsTarget(); /// @dev Thrown when a predeploy request is initiated with insufficient msg.value error InsufficientFunds(); /// @dev Thrown when refunding excess fees from a predeploy fails @@ -92,14 +90,16 @@ interface IEigenPodTypes { } + /** + * @param validatorIndex index of the validator on the beacon chain + * @param restakedBalanceGwei amount of beacon chain ETH restaked on EigenLayer in gwei + * @param lastCheckpointedAt timestamp of the validator's most recent balance update + * @param status last recorded status of the validator + */ struct ValidatorInfo { - // index of the validator in the beacon chain uint64 validatorIndex; - // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; - //timestamp of the validator's most recent balance update uint64 lastCheckpointedAt; - // status of the validator VALIDATOR_STATUS status; } @@ -111,14 +111,28 @@ interface IEigenPodTypes { uint64 prevBeaconBalanceGwei; } + /** + * @param srcPubkey the pubkey of the source validator for the consolidation + * @param targetPubkey the pubkey of the target validator for the consolidation + * @dev Note that if srcPubkey == targetPubkey, this is a "switch request," and will + * change the validator's withdrawal credential type from 0x01 to 0x02. + * For more notes on usage, see `requestConsolidation` + */ struct ConsolidationRequest { bytes srcPubkey; bytes targetPubkey; } + /** + * @param pubkey the pubkey of the validator to withdraw from + * @param amountGwei the amount (in gwei) to withdraw from the beacon chain to the pod + * @dev Note that if amountGwei == 0, this is a "full exit request," and will fully exit + * the validator to the pod. + * For more notes on usage, see `requestWithdrawal` + */ struct WithdrawalRequest { bytes pubkey; - uint64 amount; + uint64 amountGwei; } } @@ -155,6 +169,18 @@ interface IEigenPodEvents is IEigenPodTypes { /// @notice Emitted when a validaor is proven to have 0 balance at a given checkpoint event ValidatorWithdrawn(uint64 indexed checkpointTimestamp, uint40 indexed validatorIndex); + + /// @notice Emitted when a consolidation request is initiated where source == target + event SwitchToCompoundingRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a standard consolidation request is initiated + event ConsolidationRequested(bytes32 indexed sourcePubkeyHash, bytes32 indexed targetPubkeyHash); + + /// @notice Emitted when a withdrawal request is initiated where request.amountGwei == 0 + event ExitRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a partial withdrawal request is initiated + event WithdrawalRequested(bytes32 indexed validatorPubkeyHash, uint64 withdrawalAmountGwei); } /** diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 6f88e2872d..96ee905355 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -388,13 +388,19 @@ contract EigenPod is require(request.targetPubkey.length == 48, InvalidPubKeyLength()); // Ensure target has verified withdrawal credentials pointed at this pod - ValidatorInfo memory target = validatorPubkeyToInfo(request.targetPubkey); + bytes32 sourcePubkeyHash = _calcPubkeyHash(request.srcPubkey); + bytes32 targetPubkeyHash = _calcPubkeyHash(request.targetPubkey); + ValidatorInfo memory target = validatorPubkeyHashToInfo(targetPubkeyHash); require(target.status == VALIDATOR_STATUS.ACTIVE, ValidatorNotActiveInPod()); // Call the predeploy bytes memory callData = bytes.concat(request.srcPubkey, request.targetPubkey); (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); require(ok, PredeployFailed()); + + // Emit event depending on whether this is a switch to 0x02, or a regular consolidation + if (sourcePubkeyHash == targetPubkeyHash) emit SwitchToCompoundingRequested(sourcePubkeyHash); + else emit ConsolidationRequested(sourcePubkeyHash, targetPubkeyHash); } // Refund remainder of msg.value @@ -420,9 +426,14 @@ contract EigenPod is require(request.pubkey.length == 48, InvalidPubKeyLength()); // Call the predeploy - bytes memory callData = abi.encodePacked(request.pubkey, request.amount); + bytes memory callData = abi.encodePacked(request.pubkey, request.amountGwei); (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); require(ok, PredeployFailed()); + + // Emit event depending on whether the request is a full exit or a partial withdrawal + bytes32 pubkeyHash = _calcPubkeyHash(request.pubkey); + if (request.amountGwei == 0) emit ExitRequested(pubkeyHash); + else emit WithdrawalRequested(pubkeyHash, request.amountGwei); } // Refund remainder of msg.value @@ -801,7 +812,7 @@ contract EigenPod is /// @notice Returns the validatorInfo for a given validatorPubkeyHash function validatorPubkeyHashToInfo( bytes32 validatorPubkeyHash - ) external view returns (ValidatorInfo memory) { + ) public view returns (ValidatorInfo memory) { return _validatorPubkeyHashToInfo[validatorPubkeyHash]; } diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index 8af6bb3b4f..6180cb1f5e 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -11,6 +11,7 @@ import "src/test/mocks/ETHDepositMock.sol"; import "src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol"; import "src/test/integration/mocks/EIP_7002_Mock.t.sol"; import "src/test/integration/mocks/EIP_7251_Mock.t.sol"; +import "src/test/integration/mocks/LibValidator.t.sol"; import "src/test/utils/Logger.t.sol"; struct ValidatorFieldsProof { @@ -52,19 +53,7 @@ struct StaleBalanceProofs { contract BeaconChainMock is Logger { using StdStyle for *; using print for *; - - struct Validator { - bool isDummy; - bool isSlashed; - bytes32 pubkeyHash; - bytes withdrawalCreds; - uint64 effectiveBalanceGwei; - uint64 activationEpoch; - uint64 exitEpoch; - - // cumulative unprocessed withdraw requests - uint64 pendingBalanceToWithdrawGwei; - } + using LibValidator for *; /// @dev The type of slash to apply to a validator enum SlashType { @@ -81,10 +70,11 @@ contract BeaconChainMock is Logger { uint64 public constant CONSENSUS_REWARD_AMOUNT_GWEI = 1; uint64 public constant MINOR_SLASH_AMOUNT_GWEI = 10; - // Max effective balance for a validator + // Min/max balances for valdiators // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values uint public MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; uint64 public MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; + uint constant MIN_ACTIVATION_BALANCE_WEI = 32 ether; uint64 constant MIN_ACTIVATION_BALANCE_GWEI = 32 gwei; /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): @@ -240,7 +230,7 @@ contract BeaconChainMock is Logger { _setCurrentBalance(validatorIndex, 0); // Send current balance to pod - address destination = _toAddress(validators[validatorIndex].withdrawalCreds); + address destination = v.withdrawalAddress(); cheats.deal(destination, address(destination).balance + uint(uint(exitedBalanceGwei) * GWEI_TO_WEI)); return exitedBalanceGwei; @@ -321,6 +311,12 @@ contract BeaconChainMock is Logger { } } + modifier onEpoch() { + _updateCurrentEpoch(); + _; + _advanceEpoch(); + } + /// @dev Move forward one epoch on the beacon chain, taking care of important epoch processing: /// - Award ALL validators CONSENSUS_REWARD_AMOUNT /// - Process any pending partial withdrawals (new in Pectra) @@ -334,13 +330,11 @@ contract BeaconChainMock is Logger { /// Note: /// - DOES generate consensus rewards for ALL non-exited validators /// - DOES withdraw in excess of Max EB / if validator is exited - function advanceEpoch() public { + function advanceEpoch() public onEpoch { print.method("advanceEpoch"); - _updateCurrentEpoch(); + _generateRewards(); _processWithdrawals(); - _withdrawExcess(); - _advanceEpoch(); } /// @dev Like `advanceEpoch`, but does NOT generate consensus rewards for validators. @@ -350,11 +344,10 @@ contract BeaconChainMock is Logger { /// Note: /// - does NOT generate consensus rewards /// - DOES withdraw in excess of Max EB / if validator is exited - function advanceEpoch_NoRewards() public { + function advanceEpoch_NoRewards() public onEpoch { print.method("advanceEpoch_NoRewards"); - _updateCurrentEpoch(); + _processWithdrawals(); - _advanceEpoch(); } /// @dev Like `advanceEpoch`, but explicitly does NOT withdraw if balances @@ -365,17 +358,14 @@ contract BeaconChainMock is Logger { /// - DOES generate consensus rewards for ALL non-exited validators /// - does NOT withdraw in excess of Max EB /// - does NOT withdraw if validator is exited - function advanceEpoch_NoWithdraw() public { + function advanceEpoch_NoWithdraw() public onEpoch { print.method("advanceEpoch_NoWithdraw"); - _updateCurrentEpoch(); + _generateRewards(); - _advanceEpoch(); } - function advanceEpoch_NoWithdrawNoRewards() public { + function advanceEpoch_NoWithdrawNoRewards() public onEpoch { print.method("advanceEpoch_NoWithdrawNoRewards"); - _updateCurrentEpoch(); - _advanceEpoch(); } /// @dev Iterate over all validators. If the validator is still active, @@ -399,109 +389,211 @@ contract BeaconChainMock is Logger { console.log(" - Generated rewards for %s of %s validators.", totalRewarded, validators.length); } + /// @dev Process EIP-7002 partial withdrawal requests and any withdrawals from the beacon chain function _processWithdrawals() internal { _process_EIP_7002_Requests(); + _process_EIP_7521_Requests(); + _withdrawFromBeaconChain(); + } - // TODO - update this to also withdraw/reset pending amounts - _withdrawExcess(); + /// @dev Handle consolidation requests + function _process_EIP_7521_Requests() internal { + // Call EIP-7521 predeploy and dequeue any consolidation requests + cheats.prank(SYSTEM_ADDRESS); + (bool ok, bytes memory data) = address(CONSOLIDATION_PREDEPLOY).call(""); + require(ok, "BeaconChainMock._process_EIP_7521_Requests: CONSOLIDATION_PREDEPLOY failed"); + + ConsolidationRequest[] memory requests = abi.decode(data, (ConsolidationRequest[])); + console.log(" - Dequeued %d consolidation requests.", requests.length); + + for (uint i = 0; i < requests.length; i++) { + ConsolidationRequest memory request = requests[i]; + uint40 sourceIndex = request.sourcePubkey.toIndex(); + uint40 targetIndex = request.targetPubkey.toIndex(); + + if (sourceIndex >= validators.length || targetIndex >= validators.length) { + _logSkip("source or target does not exist"); + continue; + } + + Validator storage source = validators[sourceIndex]; + Validator storage target = validators[targetIndex]; + + if (source.isDummy || target.isDummy) { + _logSkip("dummy validator"); + } else if (request.source != source.withdrawalAddress()) { + _logSkip("incorrect source address"); + } else if (source.hasBLSWC()) { + _logSkip("source has BLS withdrawal credentials"); + } else if (!source.isActiveAt(currentEpoch())) { + _logSkip("source is not active at current epoch"); + } else if (source.isExiting()) { + _logSkip("source validator is exiting"); + } else if (sourceIndex == targetIndex) { + // Handle switch to 0x02 request + if (source.hasCompoundingWC()) { + _logSkip("switch request source already has 0x02 credentials"); + } else { + console.log(" -- Switching validator to 0x02 creds (idx: %d).", sourceIndex); + + // The beacon chain would queue excess balance here as a "pending deposit", but + // we don't follow the spec that closely. + source.withdrawalCreds[0] = 0x02; + } + } else if (!target.hasCompoundingWC()) { + _logSkip("target does not have compounding withdrawal credentials"); + } else if (!target.isActiveAt(currentEpoch())) { + _logSkip("target is not active at current epoch"); + } else if (target.isExiting()) { + _logSkip("target validator is exiting"); + } else if (source.pendingBalanceToWithdrawGwei != 0) { + _logSkip("source has pending withdrawals in queue"); + } else if (source.isSlashed) { + _logSkip("source validator is slashed"); + } else { + console.log(" -- Consolidating source (%d) to target (%d).", sourceIndex, targetIndex); + + // Mark source validator exited + source.exitEpoch = currentEpoch(); + + // Transfer balance from source to target + // Note the beacon chain would cap this transfer to the effective balance, + // and withdraw the rest. Our effective balances aren't using hysteresis, so + // we approximate this behavior by withdrawing anything over 32 ETH. + uint64 transferAmtGwei = _currentBalanceGwei(sourceIndex); + uint64 withdrawAmtGwei; + if (transferAmtGwei > MIN_ACTIVATION_BALANCE_GWEI) { + withdrawAmtGwei = transferAmtGwei - MIN_ACTIVATION_BALANCE_GWEI; + transferAmtGwei = MIN_ACTIVATION_BALANCE_GWEI; + } + + uint64 targetBalanceGwei = _currentBalanceGwei(targetIndex); + _setCurrentBalance(sourceIndex, 0); + _setCurrentBalance(targetIndex, targetBalanceGwei + transferAmtGwei); + + // Withdraw excess balance + address destination = source.withdrawalAddress(); + cheats.deal(destination, address(destination).balance + uint(withdrawAmtGwei * GWEI_TO_WEI)); + } + } } /// @dev Handle pending partial withdrawals AND withdraw excess validator balance function _process_EIP_7002_Requests() internal { // Call EIP-7002 predeploy and dequeue any withdrawal requests cheats.prank(SYSTEM_ADDRESS); - (bool ok, bytes memory data) = WITHDRAWAL_PREDEPLOY.call(); - require(ok, "BeaconChainMock._processWithdrawals: WITHDRAWAL_PREDEPLOY failed"); + (bool ok, bytes memory data) = address(WITHDRAWAL_PREDEPLOY).call(""); + require(ok, "BeaconChainMock._process_EIP_7002_Requests: WITHDRAWAL_PREDEPLOY failed"); WithdrawalRequest[] memory requests = abi.decode(data, (WithdrawalRequest[])); console.log(" - Dequeued %d withdrawal requests.", requests.length); + for (uint i = 0; i < requests.length; i++) { WithdrawalRequest memory request = requests[i]; - // _createValidator sets each validator's pubkey to its index, so we can use it to look up here - bytes memory pubkey = request.validatorPubkey; - uint validatorIndex; - assembly { validatorIndex := mload(add(48, pubkey)) } - - // The CL would just skip this request, but we revert since it shouldn't be possible - // to have a validator with an invalid pubkey. - require(validatorIndex < validators.length, "BeaconChainMock._processWithdrawals: invalid pubkey"); + uint40 validatorIndex = request.validatorPubkey.toIndex(); + + if (validatorIndex >= validators.length) { + _logSkip("validator does not exist"); + continue; + } + Validator storage v = validators[validatorIndex]; - address destination = _toAddress(v.withdrawalCreds); - bool isFullExitRequest = request.amount == 0; - bool isCorrectSourceAddress = request.source == destination; + bool isFullExitRequest = request.amountGwei == 0; uint64 balanceGwei = _currentBalanceGwei(validatorIndex); bool hasExcessBalance = balanceGwei > MIN_ACTIVATION_BALANCE_GWEI + v.pendingBalanceToWithdrawGwei; - string memory skipReason; if (v.isDummy) { - skipReason = "dummy validator"; - } else if (!isCorrectSourceAddress) { - skipReason = "incorrect source address"; - } else if (!_isActiveAt(v, currentEpoch())) { - skipReason = "inactive validator"; - } else if (v.exitEpoch != FAR_FUTURE_EPOCH) { - skipReason = "exit in progress"; + _logSkip("dummy validator"); + } else if (request.source != v.withdrawalAddress()) { + _logSkip("incorrect source address"); + } else if (!v.isActiveAt(currentEpoch())) { + _logSkip("inactive validator"); + } else if (v.isExiting()) { + _logSkip("exit in progress"); } else if (isFullExitRequest && v.pendingBalanceToWithdrawGwei != 0) { - skipReason = "attempted full exit while pending withdrawal in queue"; + _logSkip("attempted full exit while pending withdrawal in queue"); } else if (isFullExitRequest) { + // TODO - swap to internal method exitValidator(validatorIndex); - continue; - } else if (!_hasCompoundingWithdrawalCredentials(v)) { - skipReason = "attempted partial exit without 0x02 credentials"; + } else if (!v.hasCompoundingWC()) { + _logSkip("attempted partial exit without 0x02 credentials"); } else if (!hasExcessBalance) { - skipReason = "validator does not have excess balance"; - } + _logSkip("validator does not have excess balance"); + } else { + // Partial withdrawal - only withdraw down to 32 ETH + uint64 toWithdrawGwei = balanceGwei - MIN_ACTIVATION_BALANCE_GWEI - v.pendingBalanceToWithdrawGwei; + toWithdrawGwei = toWithdrawGwei > request.amountGwei ? request.amountGwei : toWithdrawGwei; - if (skipReason.length != 0) { - console.log(" -- Skipping request with reason: %s.", skipReason); - continue; + v.pendingBalanceToWithdrawGwei += toWithdrawGwei; } - - // Partial withdrawal - only withdraw down to 32 ETH - uint64 toWithdrawGwei = balanceGwei - MIN_ACTIVATION_BALANCE_GWEI - v.pendingBalanceToWithdrawGwei; - toWithdrawGwei = toWithdrawGwei > request.amountGwei ? request.amountGwei : toWithdrawGwei; - - v.pendingBalanceToWithdrawGwei += toWithdrawGwei; } } - /// @dev Iterate over all validators. If the validator has > Max EB current balance - /// OR is exited, withdraw the excess to the validator's withdrawal address. - function _withdrawExcess() internal { - uint totalExcessWei; + function _logSkip(string memory reason) internal { + console.log(" -- Skipping request with reason: %s.", reason); + } + + /// @dev Iterate over all validators. If the validator: + /// - has > than Max EB current balance + /// - is exited + /// - has a pending partial withdrawal + /// + /// ... withdraw the appropriate amount to the validator's withdrawal credentials + function _withdrawFromBeaconChain() internal { + uint64 totalWithdrawnGwei; for (uint i = 0; i < validators.length; i++) { Validator storage v = validators[i]; if (v.isDummy) continue; // don't process dummy validators - uint balanceWei = uint(_currentBalanceGwei(uint40(i))) * GWEI_TO_WEI; - address destination = _toAddress(v.withdrawalCreds); - uint excessBalanceWei; - uint64 newBalanceGwei = uint64(balanceWei / GWEI_TO_WEI); + uint64 withdrawAmtGwei; + uint64 curBalanceGwei = _currentBalanceGwei(uint40(i)); + // If the validator has nothing to withdraw, continue + if (curBalanceGwei == 0) continue; - // If the validator has exited, withdraw any existing balance - // - // If the validator has > Max EB, withdraw anything over that if (v.exitEpoch != BeaconChainProofs.FAR_FUTURE_EPOCH) { - if (balanceWei == 0) continue; + // Process full withdrawal for exited validator + withdrawAmtGwei = curBalanceGwei; + } else { + // If the validator has a pending withdrawal request, withdraw (but do not go below 32 eth) + // Note that these reqs are only fulfilled for 0x02 validators (this is enforced elsewhere) + if (v.pendingBalanceToWithdrawGwei != 0 && curBalanceGwei > MIN_ACTIVATION_BALANCE_GWEI) { + uint64 excessBalanceGwei = curBalanceGwei - MIN_ACTIVATION_BALANCE_GWEI; - excessBalanceWei = balanceWei; - newBalanceGwei = 0; - } else if (balanceWei > MAX_EFFECTIVE_BALANCE_WEI) { - excessBalanceWei = balanceWei - MAX_EFFECTIVE_BALANCE_WEI; - newBalanceGwei = MAX_EFFECTIVE_BALANCE_GWEI; + withdrawAmtGwei = v.pendingBalanceToWithdrawGwei; + if (withdrawAmtGwei > excessBalanceGwei) withdrawAmtGwei = excessBalanceGwei; + + // Reduce balance and set pending to 0 + curBalanceGwei -= withdrawAmtGwei; + v.pendingBalanceToWithdrawGwei = 0; + } + + // Withdraw any amount above the max effective balance: + // - For 0x01 validators, this is 32 ETH + // - For 0x02 validators, this is 2048 ETH + uint64 maxEBGwei = _getMaxEffectiveBalanceGwei(v); + if (curBalanceGwei > maxEBGwei) { + uint64 excessBalanceGwei = curBalanceGwei - maxEBGwei; + + withdrawAmtGwei += excessBalanceGwei; + } } + uint64 newBalanceGwei = curBalanceGwei - withdrawAmtGwei; + // Send ETH to withdrawal address - cheats.deal(destination, address(destination).balance + excessBalanceWei); - totalExcessWei += excessBalanceWei; + address destination = v.withdrawalAddress(); + uint withdrawAmtWei = withdrawAmtGwei * GWEI_TO_WEI; + + cheats.deal(destination, address(destination).balance + withdrawAmtWei); + totalWithdrawnGwei += withdrawAmtGwei; // Update validator's current balance _setCurrentBalance(uint40(i), newBalanceGwei); } - if (totalExcessWei != 0) console.log("- Withdrew excess balance:", totalExcessWei.asGwei()); + if (totalWithdrawnGwei != 0) console.log("- Total withdrawals from CL (gwei):", totalWithdrawnGwei); } function _updateCurrentEpoch() internal { @@ -601,16 +693,12 @@ contract BeaconChainMock is Logger { if (validatorIndex % 4 == 0) { uint64 dummyBalanceGwei = type(uint64).max - uint64(validators.length); - bytes memory _dummyPubkey = new bytes(48); - assembly { - mstore(add(48, _dummyPubkey), validatorIndex) - } validators.push( Validator({ isDummy: true, isSlashed: false, - pubkeyHash: sha256(abi.encodePacked(_dummyPubkey, bytes16(0))), - withdrawalCreds: "", + pubkeyHash: validatorIndex.toPubkey().pubkeyHash(), + withdrawalCreds: new bytes(1), effectiveBalanceGwei: dummyBalanceGwei, activationEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, exitEpoch: BeaconChainProofs.FAR_FUTURE_EPOCH, @@ -622,16 +710,11 @@ contract BeaconChainMock is Logger { validatorIndex++; } - // Use pubkey format from `EigenPod._calculateValidatorPubkeyHash` - bytes memory _pubkey = new bytes(48); - assembly { - mstore(add(48, _pubkey), validatorIndex) - } validators.push( Validator({ isDummy: false, isSlashed: false, - pubkeyHash: sha256(abi.encodePacked(_pubkey, bytes16(0))), + pubkeyHash: validatorIndex.toPubkey().pubkeyHash(), withdrawalCreds: withdrawalCreds, effectiveBalanceGwei: balanceGwei, activationEpoch: currentEpoch(), @@ -774,7 +857,7 @@ contract BeaconChainMock is Logger { // Calculate credential proofs for each validator for (uint i = 0; i < validators.length; i++) { bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); - bytes32[] memory validatorFields = _getValidatorFields(uint40(i)); + bytes32[] memory validatorFields = validators[i].getValidatorFields(); bytes32 curNode = Merkle.merkleizeSha256(validatorFields); // Validator fields leaf -> validator container root @@ -843,7 +926,7 @@ contract BeaconChainMock is Logger { // Place each validator's validatorFields into tree for (uint i = 0; i < validators.length; i++) { - leaves[i] = Merkle.merkleizeSha256(_getValidatorFields(uint40(i))); + leaves[i] = Merkle.merkleizeSha256(validators[i].getValidatorFields()); } return leaves; @@ -933,20 +1016,6 @@ contract BeaconChainMock is Logger { return validatorIndex / 4; } - function _getValidatorFields(uint40 validatorIndex) internal view returns (bytes32[] memory) { - bytes32[] memory vFields = new bytes32[](8); - Validator memory v = validators[validatorIndex]; - - vFields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = v.pubkeyHash; - vFields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = bytes32(v.withdrawalCreds); - vFields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = _toLittleEndianUint64(v.effectiveBalanceGwei); - vFields[BeaconChainProofs.VALIDATOR_SLASHED_INDEX] = bytes32(abi.encode(v.isSlashed)); - vFields[BeaconChainProofs.VALIDATOR_ACTIVATION_EPOCH_INDEX] = _toLittleEndianUint64(v.activationEpoch); - vFields[BeaconChainProofs.VALIDATOR_EXIT_EPOCH_INDEX] = _toLittleEndianUint64(v.exitEpoch); - - return vFields; - } - /// @dev Update the validator's current balance function _setCurrentBalance(uint40 validatorIndex, uint64 newBalanceGwei) internal { bytes32 balanceRoot = balances[validatorIndex / 4]; @@ -974,24 +1043,6 @@ contract BeaconChainMock is Logger { return zeroNodes[depth]; } - /// @dev Opposite of Endian.fromLittleEndianUint64 - function _toLittleEndianUint64(uint64 num) internal pure returns (bytes32) { - uint lenum; - - // Rearrange the bytes from big-endian to little-endian format - lenum |= uint((num & 0xFF) << 56); - lenum |= uint((num & 0xFF00) << 40); - lenum |= uint((num & 0xFF0000) << 24); - lenum |= uint((num & 0xFF000000) << 8); - lenum |= uint((num & 0xFF00000000) >> 8); - lenum |= uint((num & 0xFF0000000000) >> 24); - lenum |= uint((num & 0xFF000000000000) >> 40); - lenum |= uint((num & 0xFF00000000000000) >> 56); - - // Shift the little-endian bytes to the end of the bytes32 value - return bytes32(lenum << 192); - } - /// @dev Opposite of BeaconChainProofs.getBalanceAtIndex, calculates a new balance /// root by updating the balance at validatorIndex /// @return The new, updated balance root @@ -1002,22 +1053,12 @@ contract BeaconChainMock is Logger { uint clearedRoot = uint(balanceRoot) & mask; // Convert validator balance to little endian and shift to correct position - uint leBalance = uint(_toLittleEndianUint64(newBalanceGwei)); + uint leBalance = uint(newBalanceGwei.toLittleEndianUint64()); uint shiftedBalance = leBalance >> (192 - bitShiftAmount); return bytes32(clearedRoot | shiftedBalance); } - /// @dev Helper to convert 32-byte withdrawal credentials to an address - function _toAddress(bytes memory withdrawalCreds) internal pure returns (address a) { - bytes32 creds = bytes32(withdrawalCreds); - uint160 mask = type(uint160).max; - - assembly { - a := and(creds, mask) - } - } - /** * * VIEW METHODS @@ -1096,16 +1137,16 @@ contract BeaconChainMock is Logger { return validators[validatorIndex].effectiveBalanceGwei; } + function _getMaxEffectiveBalanceGwei(Validator storage v) internal view returns (uint64) { + return v.hasCompoundingWC() ? MAX_EFFECTIVE_BALANCE_GWEI : MIN_ACTIVATION_BALANCE_GWEI; + } + function pubkeyHash(uint40 validatorIndex) public view returns (bytes32) { return validators[validatorIndex].pubkeyHash; } function pubkey(uint40 validatorIndex) public pure returns (bytes memory) { - bytes memory _pubkey = new bytes(48); - assembly { - mstore(add(48, _pubkey), validatorIndex) - } - return _pubkey; + return validatorIndex.toPubkey(); } function getPubkeyHashes(uint40[] memory _validators) public view returns (bytes32[] memory) { @@ -1119,10 +1160,6 @@ contract BeaconChainMock is Logger { } function isActive(uint40 validatorIndex) public view returns (bool) { - return _isActiveAt(validators[validatorIndex], currentEpoch()); - } - - function _isActiveAt(Validator storage self, uint64 epoch) internal view returns (bool) { - return self.activationEpoch <= epoch && epoch < self.exitEpoch; + return validators[validatorIndex].isActiveAt(currentEpoch()); } } diff --git a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol index 20baa0e3a4..b99be89142 100644 --- a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol +++ b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol @@ -7,6 +7,7 @@ import "src/test/integration/mocks/BeaconChainMock.t.sol"; contract BeaconChainMock_DenebForkable is BeaconChainMock { using StdStyle for *; using print for *; + using LibValidator for *; // Denotes whether the beacon chain has been forked to Pectra bool isPectra; @@ -128,7 +129,7 @@ contract BeaconChainMock_DenebForkable is BeaconChainMock { // Calculate credential proofs for each validator for (uint i = 0; i < validators.length; i++) { bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); - bytes32[] memory validatorFields = _getValidatorFields(uint40(i)); + bytes32[] memory validatorFields = validators[i].getValidatorFields(); bytes32 curNode = Merkle.merkleizeSha256(validatorFields); // Validator fields leaf -> validator container root diff --git a/src/test/integration/mocks/EIP_7002_Mock.t.sol b/src/test/integration/mocks/EIP_7002_Mock.t.sol index 9c1f6b33a7..65b8e29bb3 100644 --- a/src/test/integration/mocks/EIP_7002_Mock.t.sol +++ b/src/test/integration/mocks/EIP_7002_Mock.t.sol @@ -1,10 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.27; - struct WithdrawalRequest { address source; bytes validatorPubkey; - uint64 amount; + uint64 amountGwei; } contract EIP_7002_Mock { @@ -50,16 +49,16 @@ contract EIP_7002_Mock { withdrawalRequestCount++; bytes memory pubkey = msg.data[0:48]; - uint64 amount; + uint64 amountGwei; assembly { - calldatacopy(24, 48, 8) // copy uint64 amount to memory[0] - amount := mload(0) + calldatacopy(24, 48, 8) // copy uint64 amountGwei to memory[0] + amountGwei := mload(0) } WithdrawalRequest storage request = requestQueue[queueTail]; request.source = msg.sender; request.validatorPubkey = pubkey; - request.amount = amount; + request.amountGwei = amountGwei; queueTail++; } diff --git a/src/test/integration/mocks/LibValidator.t.sol b/src/test/integration/mocks/LibValidator.t.sol new file mode 100644 index 0000000000..64d3d2a4c0 --- /dev/null +++ b/src/test/integration/mocks/LibValidator.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; + +import "src/contracts/libraries/BeaconChainProofs.sol"; + +struct Validator { + bool isDummy; + bool isSlashed; + bytes32 pubkeyHash; + bytes withdrawalCreds; + uint64 effectiveBalanceGwei; + uint64 activationEpoch; + uint64 exitEpoch; + + // cumulative unprocessed withdraw requests + uint64 pendingBalanceToWithdrawGwei; +} + +library LibValidator { + + /// @dev Generates a faux-pubkey from a uint40 validator index + function toPubkey(uint40 index) internal pure returns (bytes memory pubkey) { + pubkey = new bytes(48); + assembly { mstore(add(48, pubkey), index) } + } + + /// @dev Converts a validator pubkey to the index it uses in BeaconChainMock + /// NOTE this assumes a valid size pubkey + function toIndex(bytes memory pubkey) internal pure returns (uint40 validatorIndex) { + assembly { validatorIndex := mload(add(48, pubkey)) } + } + + /// @dev Computes a pubkey hash from a validator's pubkey + /// NOTE this assumes a valid size pubkey + function pubkeyHash(bytes memory pubkey) internal pure returns (bytes32) { + return sha256(abi.encodePacked(pubkey, bytes16(0))); + } + + function getValidatorFields(Validator memory self) internal pure returns (bytes32[] memory fields) { + fields = new bytes32[](8); + + fields[BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = self.pubkeyHash; + fields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = bytes32(self.withdrawalCreds); + fields[BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = toLittleEndianUint64(self.effectiveBalanceGwei); + fields[BeaconChainProofs.VALIDATOR_SLASHED_INDEX] = bytes32(abi.encode(self.isSlashed)); + fields[BeaconChainProofs.VALIDATOR_ACTIVATION_EPOCH_INDEX] = toLittleEndianUint64(self.activationEpoch); + fields[BeaconChainProofs.VALIDATOR_EXIT_EPOCH_INDEX] = toLittleEndianUint64(self.exitEpoch); + } + + /// @dev Returns whether the validator is considered "active" at the given epoch + function isActiveAt(Validator memory self, uint64 epoch) internal pure returns (bool) { + return self.activationEpoch <= epoch && epoch < self.exitEpoch; + } + + /// @dev Returns true if the validator has initiated exit + function isExiting(Validator memory self) internal pure returns (bool) { + return self.exitEpoch != BeaconChainProofs.FAR_FUTURE_EPOCH; + } + + /// @dev Returns the withdrawal address of the validator + /// NOTE this assumes the validator has 0x01 or 0x02 withdrawal credentials + function withdrawalAddress(Validator memory self) internal pure returns (address addr) { + bytes32 creds = bytes32(self.withdrawalCreds); + uint160 mask = type(uint160).max; + + assembly { addr := and(creds, mask) } + } + + /// @dev Returns true if the validator does not have 0x01/0x02 withdrawal credentials + function hasBLSWC(Validator memory self) internal pure returns (bool) { + return self.withdrawalCreds[0] != 0x01 && self.withdrawalCreds[0] != 0x02; + } + + /// @dev Returns true IFF the validator has 0x02 withdrawal credentials + function hasCompoundingWC(Validator memory self) internal pure returns (bool) { + return self.withdrawalCreds[0] == 0x02; + } + + /// @dev Opposite of Endian.fromLittleEndianUint64 + function toLittleEndianUint64(uint64 num) internal pure returns (bytes32) { + uint lenum; + + // Rearrange the bytes from big-endian to little-endian format + lenum |= uint((num & 0xFF) << 56); + lenum |= uint((num & 0xFF00) << 40); + lenum |= uint((num & 0xFF0000) << 24); + lenum |= uint((num & 0xFF000000) << 8); + lenum |= uint((num & 0xFF00000000) >> 8); + lenum |= uint((num & 0xFF0000000000) >> 24); + lenum |= uint((num & 0xFF000000000000) >> 40); + lenum |= uint((num & 0xFF00000000000000) >> 56); + + // Shift the little-endian bytes to the end of the bytes32 value + return bytes32(lenum << 192); + } +} \ No newline at end of file diff --git a/src/test/integration/tests/eigenpod/Pectra_Features.t.sol b/src/test/integration/tests/eigenpod/Pectra_Features.t.sol new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol index 0dad998bee..084a78e217 100644 --- a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol +++ b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol @@ -24,7 +24,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { EigenPod pod = staker.pod(); CredentialProofs memory proofs = beaconChain.getCredentialProofs(validators); - cheats.startPrank(address(staker)); + cheats.prank(address(staker)); cheats.resumeGasMetering(); uint startGas = gasleft(); @@ -51,6 +51,7 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { CheckpointProofs memory cpProofs = beaconChain.getCheckpointProofs(validators, pod.currentCheckpointTimestamp()); + cheats.prank(address(staker)); cheats.resumeGasMetering(); startGas = gasleft(); pod.verifyCheckpointProofs({balanceContainerProof: cpProofs.balanceContainerProof, proofs: cpProofs.balanceProofs}); diff --git a/src/test/integration/users/User.t.sol b/src/test/integration/users/User.t.sol index f97904acfc..741cfcf978 100644 --- a/src/test/integration/users/User.t.sol +++ b/src/test/integration/users/User.t.sol @@ -15,10 +15,6 @@ import "src/test/integration/mocks/BeaconChainMock.t.sol"; import "src/test/utils/Logger.t.sol"; import "src/test/utils/ArrayLib.sol"; -struct Validator { - uint40 index; -} - interface IUserDeployer { function allocationManager() external view returns (AllocationManager); function delegationManager() external view returns (DelegationManager); @@ -517,13 +513,19 @@ contract User is Logger, IDelegationManagerTypes, IAllocationManagerTypes { else break; } - // Track validators with maximum effective balance - if (validatorEth == 2048 ether) maxEBValidators++; - // Create the validator bytes memory withdrawalCredentials = validatorEth == 32 ether ? _podWithdrawalCredentials() : _podCompoundingWithdrawalCredentials(); + // Track validators with max effective balance + // - For 0x01 validators, this is 32 ETH + // - For 0x02 validators, this is 2048 ETH + if (withdrawalCredentials[0] == 0x01 && validatorEth == 32 ether) { + maxEBValidators++; + } else if (withdrawalCredentials[0] == 0x02 && validatorEth == 2048 ether) { + maxEBValidators++; + } + uint40 validatorIndex = beaconChain.newValidator{value: validatorEth}(withdrawalCredentials); newValidators[numValidators] = validatorIndex; diff --git a/src/test/utils/EigenPodUser.t.sol b/src/test/utils/EigenPodUser.t.sol index 089ff3ac67..420b74333f 100644 --- a/src/test/utils/EigenPodUser.t.sol +++ b/src/test/utils/EigenPodUser.t.sol @@ -12,10 +12,6 @@ import "src/test/integration/TimeMachine.t.sol"; import "src/test/integration/mocks/BeaconChainMock.t.sol"; import "src/test/utils/Logger.t.sol"; -struct Validator { - uint40 index; -} - interface IUserDeployer { function timeMachine() external view returns (TimeMachine); function beaconChain() external view returns (BeaconChainMock); From d5091508bc04c6d60622982cbe698bf9587eac3c Mon Sep 17 00:00:00 2001 From: wadealexc Date: Wed, 30 Apr 2025 16:26:13 +0000 Subject: [PATCH 6/8] test: fix broken test --- .../integration/mocks/BeaconChainMock.t.sol | 3 ++- .../VerifyWC_StartCP_CompleteCP.t.sol | 20 ++++++++++--------- src/test/integration/users/User.t.sol | 7 +++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index 6180cb1f5e..e30c824914 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -612,7 +612,8 @@ contract BeaconChainMock is Logger { // Get current balance and trim anything over MAX EB uint64 balanceGwei = _currentBalanceGwei(uint40(i)); - if (balanceGwei > MAX_EFFECTIVE_BALANCE_GWEI) balanceGwei = MAX_EFFECTIVE_BALANCE_GWEI; + uint64 maxEBGwei = _getMaxEffectiveBalanceGwei(v); + if (balanceGwei > maxEBGwei) balanceGwei = maxEBGwei; v.effectiveBalanceGwei = balanceGwei; } diff --git a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol index 084a78e217..c4037ad8f5 100644 --- a/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol +++ b/src/test/integration/tests/eigenpod/VerifyWC_StartCP_CompleteCP.t.sol @@ -518,22 +518,24 @@ contract Integration_VerifyWC_StartCP_CompleteCP is IntegrationCheckUtils { (uint40[] memory validators, uint64 beaconBalanceGwei, uint maxEBValidators) = staker.startValidators(); // Advance epoch and generate consensus rewards, but don't withdraw to pod beaconChain.advanceEpoch_NoWithdraw(); - - // Get the expected increase in beacon balance, accounting for validators with MaxEB - // The validators < MaxEB will have their rewards proven in the checkpoint - // The validators == MaxEB will have their rewards proven in verifyWC - uint64 beaconBalanceIncreaseGwei = uint64(validators.length) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); - uint64 expectedWithdrawnGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); - uint64 verifyWCRewardsIncreaseGwei = beaconBalanceIncreaseGwei - expectedWithdrawnGwei; + + /// "Expected effective balance increase gwei": + // For verifyWithdrawalCredentials, we expect beacon chain rewards to be reflected in effective balance + // for any validator NOT at max effective balance. + uint64 expectedEBIncreaseGwei = uint64((validators.length - maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI()); + /// "Expected current balance increase gwei": + // For checkpointing, we expect additional beacon chain rewards to be reflected in current balance + // for any validator at max effective balance + uint64 expectedCBIncreaseGwei = uint64(maxEBValidators) * beaconChain.CONSENSUS_REWARD_AMOUNT_GWEI(); staker.verifyWithdrawalCredentials(validators); - check_VerifyWC_State(staker, validators, beaconBalanceGwei + verifyWCRewardsIncreaseGwei); + check_VerifyWC_State(staker, validators, beaconBalanceGwei + expectedEBIncreaseGwei); staker.startCheckpoint(); check_StartCheckpoint_State(staker); staker.completeCheckpoint(); - check_CompleteCheckpoint_EarnOnBeacon_State(staker, expectedWithdrawnGwei); + check_CompleteCheckpoint_EarnOnBeacon_State(staker, expectedCBIncreaseGwei); } /// 1. Verify validators' withdrawal credentials diff --git a/src/test/integration/users/User.t.sol b/src/test/integration/users/User.t.sol index 741cfcf978..c4f29c6a09 100644 --- a/src/test/integration/users/User.t.sol +++ b/src/test/integration/users/User.t.sol @@ -333,11 +333,10 @@ contract User is Logger, IDelegationManagerTypes, IAllocationManagerTypes { /// @return The amount of wei sent to the beacon chain function startValidators(uint8 numValidators) public virtual createSnapshot returns (uint40[] memory, uint64, uint) { require(numValidators > 0 && numValidators <= 10, "startValidators: numValidators must be between 1 and 10"); + + // Deal ETH for the new validators uint balanceWei = address(this).balance; - - // given a number of validators, the current balance, calculate the amount of ETH needed to start that many validators - uint ethNeeded = numValidators * 32 ether - balanceWei; - cheats.deal(address(this), ethNeeded); + cheats.deal(address(this), balanceWei + (numValidators * 32 ether)); print.method("startValidators"); return _startValidators(); From c211ca80ac6df18e55f28d9a3994854c9fb631e0 Mon Sep 17 00:00:00 2001 From: wadealexc Date: Fri, 2 May 2025 21:12:07 +0000 Subject: [PATCH 7/8] refactor(wip): move proofgen out of mock beacon chain --- src/contracts/pods/EigenPod.sol | 2 +- .../integration/mocks/BeaconChainMock.t.sol | 388 ++-------------- src/test/integration/mocks/LibProofGen.t.sol | 435 ++++++++++++++++++ 3 files changed, 469 insertions(+), 356 deletions(-) create mode 100644 src/test/integration/mocks/LibProofGen.t.sol diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 96ee905355..2c27c08e6a 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -427,7 +427,7 @@ contract EigenPod is // Call the predeploy bytes memory callData = abi.encodePacked(request.pubkey, request.amountGwei); - (bool ok,) = CONSOLIDATION_REQUEST_ADDRESS.call{value: fee}(callData); + (bool ok,) = WITHDRAWAL_REQUEST_ADDRESS.call{value: fee}(callData); require(ok, PredeployFailed()); // Emit event depending on whether the request is a full exit or a partial withdrawal diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index e30c824914..e6611f88d3 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -12,18 +12,9 @@ import "src/test/integration/mocks/EIP_4788_Oracle_Mock.t.sol"; import "src/test/integration/mocks/EIP_7002_Mock.t.sol"; import "src/test/integration/mocks/EIP_7251_Mock.t.sol"; import "src/test/integration/mocks/LibValidator.t.sol"; +import "src/test/integration/mocks/LibProofGen.t.sol"; import "src/test/utils/Logger.t.sol"; -struct ValidatorFieldsProof { - bytes32[] validatorFields; - bytes validatorFieldsProof; -} - -struct BalanceRootProof { - bytes32 balanceRoot; - bytes proof; -} - struct CheckpointProofs { BeaconChainProofs.BalanceContainerProof balanceContainerProof; BeaconChainProofs.BalanceProof[] balanceProofs; @@ -63,9 +54,6 @@ contract BeaconChainMock is Logger { } - /// @dev All withdrawals are processed with index == 0 - uint constant ZERO_NODES_LENGTH = 100; - // Rewards given to each validator during epoch processing uint64 public constant CONSENSUS_REWARD_AMOUNT_GWEI = 1; uint64 public constant MINOR_SLASH_AMOUNT_GWEI = 10; @@ -77,20 +65,6 @@ contract BeaconChainMock is Logger { uint constant MIN_ACTIVATION_BALANCE_WEI = 32 ether; uint64 constant MIN_ACTIVATION_BALANCE_GWEI = 32 gwei; - /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): - /// @dev Non-constant values will change with the Pectra hard fork - - // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate - uint BEACON_STATE_FIELDS = 37; - // see https://eth2book.info/capella/part3/containers/blocks/#beaconblock - uint constant BEACON_BLOCK_FIELDS = 5; - - uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; - uint VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - uint BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - uint immutable BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); - uint64 genesisTime; uint64 public nextTimestamp; @@ -131,20 +105,6 @@ contract BeaconChainMock is Logger { uint lastIndexProcessed; uint64 curTimestamp; - // Maps block.timestamp -> beacon state root and proof - mapping(uint64 => BeaconChainProofs.StateRootProof) stateRootProofs; - - // Maps block.timestamp -> balance container root and proof - mapping(uint64 => BeaconChainProofs.BalanceContainerProof) balanceContainerProofs; - - // Maps block.timestamp -> validatorIndex -> credential proof for that timestamp - mapping(uint64 => mapping(uint40 => ValidatorFieldsProof)) validatorFieldsProofs; - - // Maps block.timestamp -> balanceRootIndex -> balance proof for that timestamp - mapping(uint64 => mapping(uint40 => BalanceRootProof)) balanceRootProofs; - - bytes32[] zeroNodes; - constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) { genesisTime = _genesisTime; eigenPodManager = _eigenPodManager; @@ -154,16 +114,6 @@ contract BeaconChainMock is Logger { cheats.etch(address(EIP_4788_ORACLE), type(EIP_4788_Oracle_Mock).runtimeCode); cheats.etch(address(CONSOLIDATION_PREDEPLOY), type(EIP_7251_Mock).runtimeCode); cheats.etch(address(WITHDRAWAL_PREDEPLOY), type(EIP_7002_Mock).runtimeCode); - - // Calculate nodes of empty merkle tree - bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); - zeroNodes = new bytes32[](ZERO_NODES_LENGTH); - zeroNodes[0] = curNode; - - for (uint i = 1; i < zeroNodes.length; i++) { - zeroNodes[i] = sha256(abi.encodePacked(curNode, curNode)); - curNode = zeroNodes[i]; - } } function NAME() public pure virtual override returns (string memory) { @@ -601,6 +551,8 @@ contract BeaconChainMock is Logger { cheats.warp(_nextEpochStartTimestamp(curEpoch)); } + mapping(uint64 => StateProofs) proofs; + function _advanceEpoch() internal virtual { cheats.pauseTracing(); curTimestamp = uint64(block.timestamp); @@ -631,48 +583,14 @@ contract BeaconChainMock is Logger { return; } - // Build merkle tree for validators - bytes32 validatorsRoot = _buildMerkleTree({ - leaves: _getValidatorLeaves(), - treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1, - tree: trees[curTimestamp].validatorTree - }); - // console.log("-- validator container root", validatorsRoot); - - // Build merkle tree for current balances - bytes32 balanceContainerRoot = _buildMerkleTree({ - leaves: _getBalanceLeaves(), - treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1, - tree: trees[curTimestamp].balancesTree - }); - // console.log("-- balances container root", balanceContainerRoot); - - // Build merkle tree for BeaconState - bytes32 beaconStateRoot = _buildMerkleTree({ - leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), - treeHeight: BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT, - tree: trees[curTimestamp].stateTree - }); - // console.log("-- beacon state root", beaconStateRoot); - - // Build merkle tree for BeaconBlock - bytes32 beaconBlockRoot = _buildMerkleTree({ - leaves: _getBeaconBlockLeaves(beaconStateRoot), - treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT, - tree: trees[curTimestamp].blockTree + bytes32 beaconBlockRoot = proofs[curTimestamp].generate({ + validators: validators, + balances: balances }); - // console.log("-- beacon block root", cheats.toString(beaconBlockRoot)); - // Push new block root to oracle EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot); - // Pre-generate proofs to pass to EigenPod methods - _genStateRootProof(beaconStateRoot); - _genBalanceContainerProof(balanceContainerRoot); - _genCredentialProofs(); - _genBalanceProofs(); - cheats.resumeTracing(); } @@ -728,253 +646,7 @@ contract BeaconChainMock is Logger { cheats.resumeTracing(); return validatorIndex; - } - - struct Tree { - mapping(bytes32 => bytes32) siblings; - mapping(bytes32 => bytes32) parents; - } - - struct MerkleTrees { - Tree validatorTree; - Tree balancesTree; - Tree stateTree; - Tree blockTree; - } - - /// Timestamp -> merkle trees constructed at that timestamp - /// Used to generate proofs - mapping(uint64 => MerkleTrees) trees; - - /// @dev Builds a merkle tree using the given leaves and height - /// -- if the leaves given are not complete (i.e. the depth should have more leaves), - /// a pre-calculated zero-node is used to complete the tree. - /// -- each pair of nodes is stored in `siblings`, and their parent in `parents`. - /// These mappings are used to build proofs for any individual leaf - /// @return The root of the merkle tree - /// - /// HACK: this sibling/parent method of tree construction relies on all passed-in leaves - /// being unique, so that we don't overwrite siblings/parents. This is simple for trees - /// like the validator tree, as each leaf is a validator's unique validatorFields. - /// However, for the balances tree, the leaves may not be distinct. To get around this, - /// _createValidator adds "dummy" validators every 4 validators created, with a unique - /// balance value. This ensures each balance root is unique. - function _buildMerkleTree(bytes32[] memory leaves, uint treeHeight, Tree storage tree) internal returns (bytes32) { - for (uint depth = 0; depth < treeHeight; depth++) { - uint newLength = (leaves.length + 1) / 2; - bytes32[] memory newLeaves = new bytes32[](newLength); - - // Hash each pair of nodes in this level of the tree - for (uint i = 0; i < newLength; i++) { - uint leftIdx = 2 * i; - uint rightIdx = leftIdx + 1; - - // Get left leaf - bytes32 leftLeaf = leaves[leftIdx]; - - // Calculate right leaf - bytes32 rightLeaf; - if (rightIdx < leaves.length) rightLeaf = leaves[rightIdx]; - else rightLeaf = _getZeroNode(depth); - - // Hash left and right - bytes32 result = sha256(abi.encodePacked(leftLeaf, rightLeaf)); - newLeaves[i] = result; - - // Record results, used to generate individual proofs later: - // Record left and right as siblings - tree.siblings[leftLeaf] = rightLeaf; - tree.siblings[rightLeaf] = leftLeaf; - // Record the result as the parent of left and right - tree.parents[leftLeaf] = result; - tree.parents[rightLeaf] = result; - } - - // Move up one level - leaves = newLeaves; - } - - require(leaves.length == 1, "BeaconChainMock._buildMerkleTree: invalid tree somehow"); - return leaves[0]; - } - - function _genStateRootProof(bytes32 beaconStateRoot) internal { - bytes memory proof = new bytes(BLOCKROOT_PROOF_LEN); - bytes32 curNode = beaconStateRoot; - - uint depth = 0; - for (uint i = 0; i < BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; i++) { - bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].blockTree.parents[curNode]; - depth++; - } - - stateRootProofs[curTimestamp] = BeaconChainProofs.StateRootProof({beaconStateRoot: beaconStateRoot, proof: proof}); - } - - function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal virtual { - bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); - bytes32 curNode = balanceContainerRoot; - - uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; - uint depth = 0; - for (uint i = 0; i < BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; i++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - for (uint i = depth; i < totalHeight; i++) { - bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].blockTree.parents[curNode]; - depth++; - } - - balanceContainerProofs[curTimestamp] = - BeaconChainProofs.BalanceContainerProof({balanceContainerRoot: balanceContainerRoot, proof: proof}); - } - - function _genCredentialProofs() internal virtual { - mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp]; - - // Calculate credential proofs for each validator - for (uint i = 0; i < validators.length; i++) { - bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); - bytes32[] memory validatorFields = validators[i].getValidatorFields(); - bytes32 curNode = Merkle.merkleizeSha256(validatorFields); - - // Validator fields leaf -> validator container root - uint depth = 0; - for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].validatorTree.parents[curNode]; - depth++; - } - - // Validator container root -> beacon state root - for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - vfProofs[uint40(i)].validatorFields = validatorFields; - vfProofs[uint40(i)].validatorFieldsProof = proof; - } - } - - function _genBalanceProofs() internal { - mapping(uint40 => BalanceRootProof) storage brProofs = balanceRootProofs[curTimestamp]; - - // Calculate current balance proofs for each balance root - uint numBalanceRoots = _numBalanceRoots(); - for (uint i = 0; i < numBalanceRoots; i++) { - bytes memory proof = new bytes(BALANCE_PROOF_LEN); - bytes32 balanceRoot = balances[uint40(i)]; - bytes32 curNode = balanceRoot; - - // Balance root leaf -> balances container root - uint depth = 0; - for (uint j = 0; j < 1 + BeaconChainProofs.BALANCE_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].balancesTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].balancesTree.parents[curNode]; - depth++; - } - - brProofs[uint40(i)].balanceRoot = balanceRoot; - brProofs[uint40(i)].proof = proof; - } - } - - function _getValidatorLeaves() internal view returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](validators.length); - - // Place each validator's validatorFields into tree - for (uint i = 0; i < validators.length; i++) { - leaves[i] = Merkle.merkleizeSha256(validators[i].getValidatorFields()); - } - - return leaves; - } - - function _getBalanceLeaves() internal view returns (bytes32[] memory) { - // Place each validator's current balance into tree - bytes32[] memory leaves = new bytes32[](_numBalanceRoots()); - for (uint i = 0; i < leaves.length; i++) { - leaves[i] = balances[uint40(i)]; - } - - return leaves; - } - - function _numBalanceRoots() internal view returns (uint) { - // Each balance leaf is shared by 4 validators. This uses div_ceil - // to calculate the number of balance leaves - return (validators.length == 0) ? 0 : ((validators.length - 1) / 4) + 1; - } - - function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal view returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS); - - // Pre-populate leaves with dummy values so sibling/parent tracking is correct - for (uint i = 0; i < leaves.length; i++) { - leaves[i] = bytes32(i + 1); - } - - // Place validatorsRoot and balancesRoot into tree - leaves[BeaconChainProofs.VALIDATOR_CONTAINER_INDEX] = validatorsRoot; - leaves[BeaconChainProofs.BALANCE_CONTAINER_INDEX] = balancesRoot; - return leaves; - } - - function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal pure returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](BEACON_BLOCK_FIELDS); - - // Pre-populate leaves with dummy values so sibling/parent tracking is correct - for (uint i = 0; i < leaves.length; i++) { - leaves[i] = bytes32(i + 1); - } - - // Place beaconStateRoot into tree - leaves[BeaconChainProofs.STATE_ROOT_INDEX] = beaconStateRoot; - return leaves; - } + } function _currentBalanceGwei(uint40 validatorIndex) internal view returns (uint64) { return currentBalance(validatorIndex); @@ -1038,12 +710,6 @@ contract BeaconChainMock is Logger { return (BeaconChainProofs.BALANCE_CONTAINER_INDEX << (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1)) | uint(balanceRootIndex); } - function _getZeroNode(uint depth) internal view returns (bytes32) { - require(depth < ZERO_NODES_LENGTH, "_getZeroNode: invalid depth"); - - return zeroNodes[depth]; - } - /// @dev Opposite of BeaconChainProofs.getBalanceAtIndex, calculates a new balance /// root by updating the balance at validatorIndex /// @return The new, updated balance root @@ -1076,21 +742,24 @@ contract BeaconChainMock is Logger { ); } - CredentialProofs memory proofs = CredentialProofs({ + StateProofs storage p = proofs[curTimestamp]; + + CredentialProofs memory credentialProofs = CredentialProofs({ beaconTimestamp: curTimestamp, - stateRootProof: stateRootProofs[curTimestamp], + stateRootProof: p.stateRootProof, validatorFieldsProofs: new bytes[](_validators.length), validatorFields: new bytes32[][](_validators.length) }); // Get proofs for each validator for (uint i = 0; i < _validators.length; i++) { - ValidatorFieldsProof memory proof = validatorFieldsProofs[curTimestamp][_validators[i]]; - proofs.validatorFieldsProofs[i] = proof.validatorFieldsProof; - proofs.validatorFields[i] = proof.validatorFields; + ValidatorFieldsProof memory proof = p.validatorFieldsProofs[_validators[i]]; + + credentialProofs.validatorFieldsProofs[i] = proof.validatorFieldsProof; + credentialProofs.validatorFields[i] = proof.validatorFields; } - return proofs; + return credentialProofs; } function getCheckpointProofs(uint40[] memory _validators, uint64 timestamp) public view returns (CheckpointProofs memory) { @@ -1100,12 +769,14 @@ contract BeaconChainMock is Logger { for (uint i = 0; i < _validators.length; i++) { require( _validators[i] <= lastIndexProcessed, - "BeaconChain.getCredentialProofs: no checkpoint proof found (did you call advanceEpoch yet?)" + "BeaconChain.getCheckpointProofs: no checkpoint proof found (did you call advanceEpoch yet?)" ); } - CheckpointProofs memory proofs = CheckpointProofs({ - balanceContainerProof: balanceContainerProofs[timestamp], + StateProofs storage p = proofs[curTimestamp]; + + CheckpointProofs memory checkpointProofs = CheckpointProofs({ + balanceContainerProof: p.balanceContainerProof, balanceProofs: new BeaconChainProofs.BalanceProof[](_validators.length) }); @@ -1113,23 +784,30 @@ contract BeaconChainMock is Logger { for (uint i = 0; i < _validators.length; i++) { uint40 validatorIndex = _validators[i]; uint40 balanceRootIndex = _getBalanceRootIndex(validatorIndex); - BalanceRootProof memory proof = balanceRootProofs[timestamp][balanceRootIndex]; + BalanceRootProof memory proof = p.balanceRootProofs[balanceRootIndex]; - proofs.balanceProofs[i] = BeaconChainProofs.BalanceProof({ + checkpointProofs.balanceProofs[i] = BeaconChainProofs.BalanceProof({ pubkeyHash: validators[validatorIndex].pubkeyHash, balanceRoot: proof.balanceRoot, proof: proof.proof }); } - return proofs; + return checkpointProofs; } function getStaleBalanceProofs(uint40 validatorIndex) public view returns (StaleBalanceProofs memory) { - ValidatorFieldsProof memory vfProof = validatorFieldsProofs[curTimestamp][validatorIndex]; + // If we have not advanced an epoch since a validator was created, no proofs have been + // generated for that validator. We check this here and revert early so we don't return + // empty proofs. + require(validatorIndex <= lastIndexProcessed, "BeaconChain.getStaleBalanceProofs: no proof found (did you call advanceEpoch yet?)"); + + StateProofs storage p = proofs[curTimestamp]; + + ValidatorFieldsProof memory vfProof = p.validatorFieldsProofs[validatorIndex]; return StaleBalanceProofs({ beaconTimestamp: curTimestamp, - stateRootProof: stateRootProofs[curTimestamp], + stateRootProof: p.stateRootProof, validatorProof: BeaconChainProofs.ValidatorProof({validatorFields: vfProof.validatorFields, proof: vfProof.validatorFieldsProof}) }); } diff --git a/src/test/integration/mocks/LibProofGen.t.sol b/src/test/integration/mocks/LibProofGen.t.sol new file mode 100644 index 0000000000..47fb2e8d0f --- /dev/null +++ b/src/test/integration/mocks/LibProofGen.t.sol @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; + +import "src/contracts/libraries/BeaconChainProofs.sol"; +import "src/contracts/libraries/Merkle.sol"; + +import "src/test/integration/mocks/LibValidator.t.sol"; + +struct ValidatorFieldsProof { + bytes32[] validatorFields; + bytes validatorFieldsProof; +} + +struct BalanceRootProof { + bytes32 balanceRoot; + bytes proof; +} + +struct Tree { + mapping(bytes32 => bytes32) siblings; + mapping(bytes32 => bytes32) parents; +} + +struct MerkleTrees { + Tree validatorTree; + Tree balancesTree; + Tree stateTree; + Tree blockTree; +} + +struct StateProofs { + MerkleTrees trees; + + BeaconChainProofs.StateRootProof stateRootProof; + BeaconChainProofs.BalanceContainerProof balanceContainerProof; + mapping(uint40 => ValidatorFieldsProof) validatorFieldsProofs; + mapping(uint40 => BalanceRootProof) balanceRootProofs; +} + +library LibProofGen { + + using LibProofGen for *; + using LibValidator for *; + + /** + * + * CONSTANTS AND CONFIG + * + */ + + // /// @dev Maximum height of a merkle tree used for proofs. We use the max height + // /// to pre-generate filler nodes so we don't need to store entire merkle trees in memory + // /// + // /// As of Pectra, the max tree height we need is BALANCE_TREE_HEIGHT + 1, which is 41. + // /// This constant is set to 50 on the off chance a future hard fork uses a different tree + // /// height and we forget to update this value. + // uint constant MAX_TREE_HEIGHT = 50; TODO + + /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): + /// @dev Non-constant values will change with the Pectra hard fork + + /// TODO - non-constant because this changed during the fork + /// Can probably fix this by adding a "config" struct somewhere? + // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate + uint constant BEACON_STATE_FIELDS = 37; + + // see https://eth2book.info/capella/part3/containers/blocks/#beaconblock + uint constant BEACON_BLOCK_FIELDS = 5; + + uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; + uint VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); + uint BALANCE_CONTAINER_PROOF_LEN = + 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); + uint immutable BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + + /** + * + * PROOFGEN: MAIN METHOD + * + */ + + /// @dev This method is how our mock beacon chain provides EigenPods with valid proofs of beacon state. + /// + /// INPUT: the beacon state for the current timestamp (validators and balances) + /// OUTPUT: the beacon block root calculated from the input. The mock beacon chain will inject this root + /// into the EIP-4788 beacon block root oracle, as EigenPods query this oracle during proof verification. + /// + /// Proofs against the beacon block root are also generated and stored in StateProofs; these proofs can + /// be fetched before calling an EigenPod method. + /// + /// This process is broken into 2 steps: + /// + /// 1. The merkle tree builder builds 4 merkle trees, one for each beacon state object we care about: + /// + /// beaconBlockRoot + /// | (beacon block tree) + /// beaconStateRoot + /// / \ (beacon state tree) + /// validatorContainerRoot, balanceContainerRoot + /// | | (balances tree) + /// | individual balances + /// | (validators tree) + /// individual validators + /// + /// Since the full beacon state is quite large, the merkle tree builder uses some hacks to generate + /// only as much of the tree as we need for our tests. See `build` for details. + /// + /// 2. Once the merkle trees are built, we pre-generate proofs for EigenPod methods. + function generate( + StateProofs storage p, + Validator[] memory validators, + mapping(uint40 => bytes32) storage balances + ) internal returns (bytes32 beaconBlockRoot) { + MerkleTrees storage trees = p.trees; + + // Build merkle tree for validators + bytes32 validatorsRoot = trees.validatorTree.build({ + leaves: _getValidatorLeaves(validators), + height: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1 + }); + + // Build merkle tree for current balances + bytes32[] memory balanceLeaves = _getBalanceLeaves(balances, validators.length); + bytes32 balanceContainerRoot = trees.balancesTree.build({ + leaves: balanceLeaves, + height: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1 + }); + + // Build merkle tree for BeaconState + bytes32 beaconStateRoot = trees.stateTree.build({ + leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), + height: BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT + }); + + // Build merkle tree for BeaconBlock + beaconBlockRoot = trees.blockTree.build({ + leaves: _getBeaconBlockLeaves(beaconStateRoot), + height: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + }); + + // Pre-generate proofs for EigenPod methods + p.genStateRootProof(beaconStateRoot); + p.genBalanceContainerProof(balanceContainerRoot); + p.genCredentialProofs(validators); + p.genBalanceProofs(balanceLeaves); + + return beaconBlockRoot; + } + + /** + * + * MERKLE TREE BUILDER + * + */ + + /// @dev Builds a merkle tree in storage using the given leaves and height + /// -- if the leaves given are not complete (i.e. the depth should have more leaves), + /// a pre-calculated zero-node is used to complete the tree. + /// -- each pair of nodes is stored in `siblings`, and their parent in `parents`. + /// These mappings are used to build proofs for any individual leaf + /// @return The root of the merkle tree + /// + /// HACK: this sibling/parent method of tree construction relies on all passed-in leaves + /// being unique, so that we don't overwrite siblings/parents. This is simple for trees + /// like the validator tree, as each leaf is a validator's unique validatorFields. + /// However, for the balances tree, the leaves may not be distinct. To get around this, + /// BeaconChainMock._createValidator adds "dummy" validators every 4 validators created, + /// with a unique balance value. This ensures each balance root is unique. + function build(Tree storage tree, bytes32[] memory leaves, uint height) internal returns (bytes32 root) { + for (uint depth = 0; depth < height; depth++) { + uint newLength = (leaves.length + 1) / 2; + bytes32[] memory newLeaves = new bytes32[](newLength); + + // Hash each pair of nodes in this level of the tree + for (uint i = 0; i < newLength; i++) { + uint leftIdx = 2 * i; + uint rightIdx = leftIdx + 1; + + // Get left leaf + bytes32 leftLeaf = leaves[leftIdx]; + + // Calculate right leaf + bytes32 rightLeaf; + if (rightIdx < leaves.length) rightLeaf = leaves[rightIdx]; + else rightLeaf = _getFillerNode(depth); + + // Hash left and right + bytes32 result = sha256(abi.encodePacked(leftLeaf, rightLeaf)); + newLeaves[i] = result; + + // Record results, used to generate individual proofs later: + // Record left and right as siblings + tree.siblings[leftLeaf] = rightLeaf; + tree.siblings[rightLeaf] = leftLeaf; + // Record the result as the parent of left and right + tree.parents[leftLeaf] = result; + tree.parents[rightLeaf] = result; + } + + // Move up one level + leaves = newLeaves; + } + + require(leaves.length == 1, "LibProofGen.build: invalid tree somehow"); + return leaves[0]; + } + + /// @dev Fetch unique filler node for given depth + function _getFillerNode(uint depth) internal view returns (bytes32) { + bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); + + for (uint i = 1; i < depth; i++) { + curNode = sha256(abi.encodePacked(curNode, curNode)); + } + + return curNode; + } + + // /// @dev Pre-generate filler nodes used to fill in incomplete merkle trees at various depths + // function _calcZeroNodes() internal pure returns (bytes32[] memory) { + // // Calculate nodes of empty merkle tree + // bytes32 curNode = Merkle.merkleizeSha256(new bytes32[](8)); + // bytes32[] memory zeroNodes = new bytes32[](MAX_TREE_HEIGHT); + // zeroNodes[0] = curNode; + + // for (uint i = 1; i < zeroNodes.length; i++) { + // zeroNodes[i] = sha256(abi.encodePacked(curNode, curNode)); + // curNode = zeroNodes[i]; + // } + + // return zeroNodes; + // } TODO + + /** + * + * PROOF GENERATION + * + */ + + /// @dev Generate global proof of beaconStateRoot -> beaconBlockRoot + /// Used in verifyWithdrawalCredentials and verifyStaleBalance + function genStateRootProof(StateProofs storage p, bytes32 beaconStateRoot) internal { + bytes memory proof = new bytes(BLOCKROOT_PROOF_LEN); + bytes32 curNode = beaconStateRoot; + + uint depth = 0; + for (uint i = 0; i < BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; i++) { + bytes32 sibling = p.trees.blockTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, i))), sibling) + } + + curNode = p.trees.blockTree.parents[curNode]; + depth++; + } + + p.stateRootProof.beaconStateRoot = beaconStateRoot; + p.stateRootProof.proof = proof; + } + + /// @dev Generate global proof of balanceContainerRoot -> beaconStateRoot -> beaconBlockRoot + /// Used in verifyCheckpointProofs + function genBalanceContainerProof(StateProofs storage p, bytes32 balanceContainerRoot) internal virtual { + bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); + bytes32 curNode = balanceContainerRoot; + + uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; + uint depth = 0; + for (uint i = 0; i < BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; i++) { + bytes32 sibling = p.trees.stateTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, i))), sibling) + } + + curNode = p.trees.stateTree.parents[curNode]; + depth++; + } + + for (uint i = depth; i < totalHeight; i++) { + bytes32 sibling = p.trees.blockTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, i))), sibling) + } + + curNode = p.trees.blockTree.parents[curNode]; + depth++; + } + + p.balanceContainerProof.balanceContainerRoot = balanceContainerRoot; + p.balanceContainerProof.proof = proof; + } + + /// @dev Generate per-validator proofs of their validator -> validatorsRoot + /// Used in verifyWithdrawalCredentials and verifyStaleBalance + function genCredentialProofs(StateProofs storage p, Validator[] memory validators) internal virtual { + mapping(uint40 => ValidatorFieldsProof) storage vfProofs = p.validatorFieldsProofs; + + // Calculate credential proofs for each validator + for (uint i = 0; i < validators.length; i++) { + bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); + bytes32[] memory validatorFields = validators[i].getValidatorFields(); + bytes32 curNode = Merkle.merkleizeSha256(validatorFields); + + // Validator fields leaf -> validator container root + uint depth = 0; + for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { + bytes32 sibling = p.validatorTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, j))), sibling) + } + + curNode = p.validatorTree.parents[curNode]; + depth++; + } + + // Validator container root -> beacon state root + for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; j++) { + bytes32 sibling = p.stateTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, j))), sibling) + } + + curNode = p.stateTree.parents[curNode]; + depth++; + } + + vfProofs[uint40(i)].validatorFields = validatorFields; + vfProofs[uint40(i)].validatorFieldsProof = proof; + } + } + + /// @dev Generate per-balance root proofs of each balance root -> balanceContainerRoot + /// Used in verifyCheckpointProofs + function genBalanceProofs(StateProofs storage p, bytes32[] memory balanceLeaves) internal { + mapping(uint40 => BalanceRootProof) storage brProofs = p.balanceRootProofs; + + // Calculate current balance proofs for each balance root + for (uint i = 0; i < balanceLeaves.length; i++) { + bytes memory proof = new bytes(BALANCE_PROOF_LEN); + bytes32 balanceRoot = balanceLeaves[i]; + bytes32 curNode = balanceRoot; + + // Balance root leaf -> balances container root + uint depth = 0; + for (uint j = 0; j < 1 + BeaconChainProofs.BALANCE_TREE_HEIGHT; j++) { + bytes32 sibling = p.balancesTree.siblings[curNode]; + + // proof[j] = sibling; + assembly { + mstore(add(proof, add(32, mul(32, j))), sibling) + } + + curNode = p.balancesTree.parents[curNode]; + depth++; + } + + brProofs[uint40(i)].balanceRoot = balanceRoot; + brProofs[uint40(i)].proof = proof; + } + } + + /** + * + * MERKLE LEAVES GETTERS + * + */ + + /// @dev Get the leaves of the beacon block tree + function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal pure returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](BEACON_BLOCK_FIELDS); + + // Pre-populate leaves with dummy values so sibling/parent tracking is correct + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = bytes32(i + 1); + } + + // Place beaconStateRoot into tree + leaves[BeaconChainProofs.STATE_ROOT_INDEX] = beaconStateRoot; + return leaves; + } + + /// @dev Get the leaves of the beacon state tree + function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal pure returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS); + + // Pre-populate leaves with dummy values so sibling/parent tracking is correct + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = bytes32(i + 1); + } + + // Place validatorsRoot and balancesRoot into tree + leaves[BeaconChainProofs.VALIDATOR_CONTAINER_INDEX] = validatorsRoot; + leaves[BeaconChainProofs.BALANCE_CONTAINER_INDEX] = balancesRoot; + return leaves; + } + + /// @dev Get the leaves of the validators merkle tree + function _getValidatorLeaves(Validator[] memory validators) internal pure returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](validators.length); + + // Place each validator's validatorFields into tree + for (uint i = 0; i < validators.length; i++) { + leaves[i] = Merkle.merkleizeSha256(validators[i].getValidatorFields()); + } + + return leaves; + } + + /// @dev Get the leaves of the balances merkle tree + function _getBalanceLeaves(mapping(uint40 => bytes32) storage balances, uint numValidators) internal view returns (bytes32[] memory) { + // Each balance leaf is shared by 4 validators. This uses div_ceil + // to calculate the number of balance leaves + uint numBalanceRoots = numValidators == 0 ? 0 : ((numValidators - 1) / 4) + 1; + + // Place each validator's current balance into tree + bytes32[] memory leaves = new bytes32[](numBalanceRoots); + for (uint i = 0; i < leaves.length; i++) { + leaves[i] = balances[uint40(i)]; + } + + return leaves; + } +} \ No newline at end of file From c7fa475d5bc13490bb298d5aae82ba9bbbc5c370 Mon Sep 17 00:00:00 2001 From: wadealexc Date: Wed, 7 May 2025 17:20:21 +0000 Subject: [PATCH 8/8] test: finish beacon chain refactor and fix tests --- src/contracts/interfaces/IEigenPod.sol | 10 +- src/contracts/libraries/BeaconChainProofs.sol | 14 +- src/contracts/pods/EigenPod.sol | 2 + src/test/integration/IntegrationBase.t.sol | 9 + .../integration/mocks/BeaconChainMock.t.sol | 12 +- .../mocks/BeaconChainMock_Deneb.t.sol | 199 +----------------- src/test/integration/mocks/LibProofGen.t.sol | 101 ++++++--- .../tests/eigenpod/Pectra_Features.t.sol | 105 +++++++++ 8 files changed, 212 insertions(+), 240 deletions(-) diff --git a/src/contracts/interfaces/IEigenPod.sol b/src/contracts/interfaces/IEigenPod.sol index 0742b45b88..f5ba7ed94d 100644 --- a/src/contracts/interfaces/IEigenPod.sol +++ b/src/contracts/interfaces/IEigenPod.sol @@ -497,13 +497,13 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { uint64 timestamp ) external view returns (bytes32); - /// @notice Returns the current fee required to add a consolidation request to the EIP-7251 predeploy. - /// @dev Note that this getter only returns the fee required to perform a single request. For multiple - /// requests, see https://eips.ethereum.org/EIPS/eip-7251#fee-calculation + /// @notice Returns the fee required to add a consolidation request to the EIP-7251 predeploy this block. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7251#fee-calculation + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. function getConsolidationRequestFee() external view returns (uint256); /// @notice Returns the current fee required to add a withdrawal request to the EIP-7002 predeploy. - /// @dev Note that this getter only returns the fee required to perform a single request. For multiple - /// requests, see https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. function getWithdrawalRequestFee() external view returns (uint256); } diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index 5c8297f98d..5d7af5d8ad 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -18,14 +18,14 @@ library BeaconChainProofs { error InvalidValidatorFieldsLength(); /// @notice Heights of various merkle trees in the beacon chain - /// - beaconBlockRoot - /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT - /// -- beaconStateRoot - /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// beaconStateRoot + /// / \ HEIGHT: BEACON_STATE_TREE_HEIGHT /// validatorContainerRoot, balanceContainerRoot - /// | | HEIGHT: BALANCE_TREE_HEIGHT - /// | individual balances - /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT /// individual validators uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; uint256 internal constant DENEB_BEACON_STATE_TREE_HEIGHT = 5; diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index 2c27c08e6a..82556a0a47 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -857,10 +857,12 @@ contract EigenPod is return abi.decode(result, (bytes32)); } + /// @inheritdoc IEigenPod function getConsolidationRequestFee() public view returns (uint256) { return _getFee(CONSOLIDATION_REQUEST_ADDRESS); } + /// @inheritdoc IEigenPod function getWithdrawalRequestFee() public view returns (uint256) { return _getFee(WITHDRAWAL_REQUEST_ADDRESS); } diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 5b768f7ae4..f191bfe9ca 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -63,6 +63,15 @@ abstract contract IntegrationBase is IntegrationDeployer, TypeImporter { return (staker, tokenBalances); } + /// @dev Creates a blank slate user with no assets + function _newEmptyStaker() internal returns (User) { + User staker = _randUser_NoAssets(_getStakerName()); + + if (!isUpgraded) stakersToMigrate.push(staker); + + return staker; + } + /** * @dev Create a new operator according to configured random variants. * This user will immediately deposit their randomized assets into eigenlayer. diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index e6611f88d3..9a997e006e 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -45,6 +45,7 @@ contract BeaconChainMock is Logger { using StdStyle for *; using print for *; using LibValidator for *; + using LibProofGen for *; /// @dev The type of slash to apply to a validator enum SlashType { @@ -60,8 +61,8 @@ contract BeaconChainMock is Logger { // Min/max balances for valdiators // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values - uint public MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; - uint64 public MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; + uint constant MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; + uint64 constant MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; uint constant MIN_ACTIVATION_BALANCE_WEI = 32 ether; uint64 constant MIN_ACTIVATION_BALANCE_GWEI = 32 gwei; @@ -114,6 +115,8 @@ contract BeaconChainMock is Logger { cheats.etch(address(EIP_4788_ORACLE), type(EIP_4788_Oracle_Mock).runtimeCode); cheats.etch(address(CONSOLIDATION_PREDEPLOY), type(EIP_7251_Mock).runtimeCode); cheats.etch(address(WITHDRAWAL_PREDEPLOY), type(EIP_7002_Mock).runtimeCode); + + LibProofGen.usePectra(); } function NAME() public pure virtual override returns (string memory) { @@ -766,6 +769,7 @@ contract BeaconChainMock is Logger { // If we have not advanced an epoch since a validator was created, no proofs have been // generated for that validator. We check this here and revert early so we don't return // empty proofs. + require(timestamp <= curTimestamp, "BeaconChain.getCheckpointProofs: future timestamp provided (did you call advanceEpoch yet?)"); for (uint i = 0; i < _validators.length; i++) { require( _validators[i] <= lastIndexProcessed, @@ -773,7 +777,7 @@ contract BeaconChainMock is Logger { ); } - StateProofs storage p = proofs[curTimestamp]; + StateProofs storage p = proofs[timestamp]; CheckpointProofs memory checkpointProofs = CheckpointProofs({ balanceContainerProof: p.balanceContainerProof, @@ -816,7 +820,7 @@ contract BeaconChainMock is Logger { return validators[validatorIndex].effectiveBalanceGwei; } - function _getMaxEffectiveBalanceGwei(Validator storage v) internal view returns (uint64) { + function _getMaxEffectiveBalanceGwei(Validator storage v) internal virtual view returns (uint64) { return v.hasCompoundingWC() ? MAX_EFFECTIVE_BALANCE_GWEI : MIN_ACTIVATION_BALANCE_GWEI; } diff --git a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol index b99be89142..1b90c8adab 100644 --- a/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol +++ b/src/test/integration/mocks/BeaconChainMock_Deneb.t.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.27; import "src/test/integration/mocks/BeaconChainMock.t.sol"; +import "src/test/integration/mocks/LibProofGen.t.sol"; /// @notice A backwards-compatible BeaconChain Mock that updates containers & proofs from Deneb to Pectra contract BeaconChainMock_DenebForkable is BeaconChainMock { using StdStyle for *; using print for *; using LibValidator for *; + using LibProofGen for *; // Denotes whether the beacon chain has been forked to Pectra bool isPectra; @@ -15,209 +17,28 @@ contract BeaconChainMock_DenebForkable is BeaconChainMock { // The timestamp of the Pectra hard fork uint64 public pectraForkTimestamp; - constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) BeaconChainMock(_eigenPodManager, _genesisTime) { - /// DENEB SPECIFIC CONSTANTS (PROOF LENGTHS, FIELD SIZES): - // see https://eth2book.info/capella/part3/containers/state/#beaconstate - BEACON_STATE_FIELDS = 32; - - VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); - - BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); - - MAX_EFFECTIVE_BALANCE_GWEI = 32 gwei; - MAX_EFFECTIVE_BALANCE_WEI = 32 ether; + constructor(EigenPodManager _eigenPodManager, uint64 _genesisTime) + BeaconChainMock(_eigenPodManager, _genesisTime) + { + LibProofGen.useDencun(); } function NAME() public pure override returns (string memory) { return "BeaconChain_PectraForkable"; } - /** - * - * INTERNAL FUNCTIONS - * - */ - function _advanceEpoch() internal override { - cheats.pauseTracing(); - - // Update effective balances for each validator - for (uint i = 0; i < validators.length; i++) { - Validator storage v = validators[i]; - if (v.isDummy) continue; // don't process dummy validators - - // Get current balance and trim anything over MaxEB - uint64 balanceGwei = _currentBalanceGwei(uint40(i)); - if (balanceGwei > MAX_EFFECTIVE_BALANCE_GWEI) balanceGwei = MAX_EFFECTIVE_BALANCE_GWEI; - - v.effectiveBalanceGwei = balanceGwei; - } - - // console.log(" Updated effective balances...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - - uint64 curEpoch = currentEpoch(); - cheats.warp(_nextEpochStartTimestamp(curEpoch)); - curTimestamp = uint64(block.timestamp); - - // console.log(" Jumping to next epoch...".dim()); - // console.log(" timestamp:", block.timestamp); - // console.log(" epoch:", currentEpoch()); - - // console.log(" Building beacon state trees...".dim()); - - // Log total number of validators and number being processed for the first time - if (validators.length > 0) { - lastIndexProcessed = validators.length - 1; - } else { - // generate an empty root if we don't have any validators - EIP_4788_ORACLE.setBlockRoot(curTimestamp, keccak256("")); - - // console.log("-- no validators; added empty block root"); - return; - } - - // Build merkle tree for validators - bytes32 validatorsRoot = _buildMerkleTree({ - leaves: _getValidatorLeaves(), - treeHeight: BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1, - tree: trees[curTimestamp].validatorTree - }); - // console.log("-- validator container root", validatorsRoot); - - // Build merkle tree for current balances - bytes32 balanceContainerRoot = _buildMerkleTree({ - leaves: _getBalanceLeaves(), - treeHeight: BeaconChainProofs.BALANCE_TREE_HEIGHT + 1, - tree: trees[curTimestamp].balancesTree - }); - // console.log("-- balances container root", balanceContainerRoot); - - // Build merkle tree for BeaconState - bytes32 beaconStateRoot = _buildMerkleTree({ - leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), - treeHeight: getBeaconStateTreeHeight(), - tree: trees[curTimestamp].stateTree - }); - // console.log("-- beacon state root", beaconStateRoot); - - // Build merkle tree for BeaconBlock - bytes32 beaconBlockRoot = _buildMerkleTree({ - leaves: _getBeaconBlockLeaves(beaconStateRoot), - treeHeight: BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT, - tree: trees[curTimestamp].blockTree - }); - - // console.log("-- beacon block root", cheats.toString(beaconBlockRoot)); - - // Push new block root to oracle - EIP_4788_ORACLE.setBlockRoot(curTimestamp, beaconBlockRoot); - - // Pre-generate proofs to pass to EigenPod methods - _genStateRootProof(beaconStateRoot); - _genBalanceContainerProof(balanceContainerRoot); - _genCredentialProofs(); - _genBalanceProofs(); - - cheats.resumeTracing(); - } - - function _genCredentialProofs() internal override { - mapping(uint40 => ValidatorFieldsProof) storage vfProofs = validatorFieldsProofs[curTimestamp]; - - // Calculate credential proofs for each validator - for (uint i = 0; i < validators.length; i++) { - bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); - bytes32[] memory validatorFields = validators[i].getValidatorFields(); - bytes32 curNode = Merkle.merkleizeSha256(validatorFields); - - // Validator fields leaf -> validator container root - uint depth = 0; - for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { - bytes32 sibling = trees[curTimestamp].validatorTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].validatorTree.parents[curNode]; - depth++; - } - - // Validator container root -> beacon state root - for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + getBeaconStateTreeHeight(); j++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, j))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - vfProofs[uint40(i)].validatorFields = validatorFields; - vfProofs[uint40(i)].validatorFieldsProof = proof; - } - } - - function _genBalanceContainerProof(bytes32 balanceContainerRoot) internal override { - bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); - bytes32 curNode = balanceContainerRoot; - - uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; - uint depth = 0; - for (uint i = 0; i < getBeaconStateTreeHeight(); i++) { - bytes32 sibling = trees[curTimestamp].stateTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].stateTree.parents[curNode]; - depth++; - } - - for (uint i = depth; i < totalHeight; i++) { - bytes32 sibling = trees[curTimestamp].blockTree.siblings[curNode]; - - // proof[j] = sibling; - assembly { - mstore(add(proof, add(32, mul(32, i))), sibling) - } - - curNode = trees[curTimestamp].blockTree.parents[curNode]; - depth++; - } - - balanceContainerProofs[curTimestamp] = - BeaconChainProofs.BalanceContainerProof({balanceContainerRoot: balanceContainerRoot, proof: proof}); + /// @dev Always return 32 ETH in gwei + function _getMaxEffectiveBalanceGwei(Validator storage v) internal override view returns (uint64) { + return isPectra ? super._getMaxEffectiveBalanceGwei(v) : MIN_ACTIVATION_BALANCE_GWEI; } /// @notice Forks the beacon chain to Pectra /// @dev Test battery should warp to the fork timestamp after calling this method function forkToPectra(uint64 _pectraForkTimestamp) public { - // https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate - BEACON_STATE_FIELDS = 37; - - VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - - MAX_EFFECTIVE_BALANCE_GWEI = 2048 gwei; - MAX_EFFECTIVE_BALANCE_WEI = 2048 ether; - isPectra = true; + LibProofGen.usePectra(); cheats.warp(_pectraForkTimestamp); pectraForkTimestamp = _pectraForkTimestamp; } - - function getBeaconStateTreeHeight() public view returns (uint) { - return isPectra ? BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT : BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT; - } } diff --git a/src/test/integration/mocks/LibProofGen.t.sol b/src/test/integration/mocks/LibProofGen.t.sol index 47fb2e8d0f..942aed858f 100644 --- a/src/test/integration/mocks/LibProofGen.t.sol +++ b/src/test/integration/mocks/LibProofGen.t.sol @@ -39,6 +39,16 @@ struct StateProofs { mapping(uint40 => BalanceRootProof) balanceRootProofs; } +struct Config { + uint BEACON_STATE_TREE_HEIGHT; + uint BEACON_STATE_FIELDS; + uint BEACON_BLOCK_FIELDS; + uint BLOCKROOT_PROOF_LEN; + uint VAL_FIELDS_PROOF_LEN; + uint BALANCE_CONTAINER_PROOF_LEN; + uint BALANCE_PROOF_LEN; +} + library LibProofGen { using LibProofGen for *; @@ -58,22 +68,43 @@ library LibProofGen { // /// height and we forget to update this value. // uint constant MAX_TREE_HEIGHT = 50; TODO - /// PROOF CONSTANTS (PROOF LENGTHS, FIELD SIZES): - /// @dev Non-constant values will change with the Pectra hard fork - - /// TODO - non-constant because this changed during the fork - /// Can probably fix this by adding a "config" struct somewhere? - // see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate - uint constant BEACON_STATE_FIELDS = 37; + bytes32 constant CONFIG_SLOT = keccak256("LibProofGen.config"); - // see https://eth2book.info/capella/part3/containers/blocks/#beaconblock - uint constant BEACON_BLOCK_FIELDS = 5; + function config() internal view returns (Config storage) { + Config storage cfg; + bytes32 _CONFIG_SLOT = CONFIG_SLOT; + assembly { cfg.slot := _CONFIG_SLOT } + + return cfg; + } - uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; - uint VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - uint BALANCE_CONTAINER_PROOF_LEN = - 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); - uint immutable BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + /// @dev Dencun-specific config + /// See (https://eth2book.info/capella/part3/containers/state/#beaconstate) + function useDencun() internal { + Config storage cfg = config(); + + cfg.BEACON_STATE_TREE_HEIGHT = BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT; + cfg.BEACON_STATE_FIELDS = 32; + cfg.BEACON_BLOCK_FIELDS = 5; + cfg.BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; + cfg.VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_CONTAINER_PROOF_LEN = 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.DENEB_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + } + + /// @dev Pectra-specific config + /// See (see https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#beaconstate) + function usePectra() internal { + Config storage cfg = config(); + + cfg.BEACON_STATE_TREE_HEIGHT = BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; + cfg.BEACON_STATE_FIELDS = 37; + cfg.BEACON_BLOCK_FIELDS = 5; + cfg.BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT; + cfg.VAL_FIELDS_PROOF_LEN = 32 * ((BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_CONTAINER_PROOF_LEN = 32 * (BeaconChainProofs.BEACON_BLOCK_HEADER_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT); + cfg.BALANCE_PROOF_LEN = 32 * (BeaconChainProofs.BALANCE_TREE_HEIGHT + 1); + } /** * @@ -131,7 +162,7 @@ library LibProofGen { // Build merkle tree for BeaconState bytes32 beaconStateRoot = trees.stateTree.build({ leaves: _getBeaconStateLeaves(validatorsRoot, balanceContainerRoot), - height: BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT + height: config().BEACON_STATE_TREE_HEIGHT }); // Build merkle tree for BeaconBlock @@ -160,7 +191,7 @@ library LibProofGen { /// a pre-calculated zero-node is used to complete the tree. /// -- each pair of nodes is stored in `siblings`, and their parent in `parents`. /// These mappings are used to build proofs for any individual leaf - /// @return The root of the merkle tree + /// @return root the root of the merkle tree /// /// HACK: this sibling/parent method of tree construction relies on all passed-in leaves /// being unique, so that we don't overwrite siblings/parents. This is simple for trees @@ -242,7 +273,7 @@ library LibProofGen { /// @dev Generate global proof of beaconStateRoot -> beaconBlockRoot /// Used in verifyWithdrawalCredentials and verifyStaleBalance function genStateRootProof(StateProofs storage p, bytes32 beaconStateRoot) internal { - bytes memory proof = new bytes(BLOCKROOT_PROOF_LEN); + bytes memory proof = new bytes(config().BLOCKROOT_PROOF_LEN); bytes32 curNode = beaconStateRoot; uint depth = 0; @@ -264,13 +295,13 @@ library LibProofGen { /// @dev Generate global proof of balanceContainerRoot -> beaconStateRoot -> beaconBlockRoot /// Used in verifyCheckpointProofs - function genBalanceContainerProof(StateProofs storage p, bytes32 balanceContainerRoot) internal virtual { - bytes memory proof = new bytes(BALANCE_CONTAINER_PROOF_LEN); + function genBalanceContainerProof(StateProofs storage p, bytes32 balanceContainerRoot) internal { + bytes memory proof = new bytes(config().BALANCE_CONTAINER_PROOF_LEN); bytes32 curNode = balanceContainerRoot; - uint totalHeight = BALANCE_CONTAINER_PROOF_LEN / 32; + uint totalHeight = config().BALANCE_CONTAINER_PROOF_LEN / 32; uint depth = 0; - for (uint i = 0; i < BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; i++) { + for (uint i = 0; i < config().BEACON_STATE_TREE_HEIGHT; i++) { bytes32 sibling = p.trees.stateTree.siblings[curNode]; // proof[j] = sibling; @@ -300,39 +331,39 @@ library LibProofGen { /// @dev Generate per-validator proofs of their validator -> validatorsRoot /// Used in verifyWithdrawalCredentials and verifyStaleBalance - function genCredentialProofs(StateProofs storage p, Validator[] memory validators) internal virtual { + function genCredentialProofs(StateProofs storage p, Validator[] memory validators) internal { mapping(uint40 => ValidatorFieldsProof) storage vfProofs = p.validatorFieldsProofs; // Calculate credential proofs for each validator for (uint i = 0; i < validators.length; i++) { - bytes memory proof = new bytes(VAL_FIELDS_PROOF_LEN); + bytes memory proof = new bytes(config().VAL_FIELDS_PROOF_LEN); bytes32[] memory validatorFields = validators[i].getValidatorFields(); bytes32 curNode = Merkle.merkleizeSha256(validatorFields); // Validator fields leaf -> validator container root uint depth = 0; for (uint j = 0; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT; j++) { - bytes32 sibling = p.validatorTree.siblings[curNode]; + bytes32 sibling = p.trees.validatorTree.siblings[curNode]; // proof[j] = sibling; assembly { mstore(add(proof, add(32, mul(32, j))), sibling) } - curNode = p.validatorTree.parents[curNode]; + curNode = p.trees.validatorTree.parents[curNode]; depth++; } // Validator container root -> beacon state root - for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + BeaconChainProofs.PECTRA_BEACON_STATE_TREE_HEIGHT; j++) { - bytes32 sibling = p.stateTree.siblings[curNode]; + for (uint j = depth; j < 1 + BeaconChainProofs.VALIDATOR_TREE_HEIGHT + config().BEACON_STATE_TREE_HEIGHT; j++) { + bytes32 sibling = p.trees.stateTree.siblings[curNode]; // proof[j] = sibling; assembly { mstore(add(proof, add(32, mul(32, j))), sibling) } - curNode = p.stateTree.parents[curNode]; + curNode = p.trees.stateTree.parents[curNode]; depth++; } @@ -348,21 +379,21 @@ library LibProofGen { // Calculate current balance proofs for each balance root for (uint i = 0; i < balanceLeaves.length; i++) { - bytes memory proof = new bytes(BALANCE_PROOF_LEN); + bytes memory proof = new bytes(config().BALANCE_PROOF_LEN); bytes32 balanceRoot = balanceLeaves[i]; bytes32 curNode = balanceRoot; // Balance root leaf -> balances container root uint depth = 0; for (uint j = 0; j < 1 + BeaconChainProofs.BALANCE_TREE_HEIGHT; j++) { - bytes32 sibling = p.balancesTree.siblings[curNode]; + bytes32 sibling = p.trees.balancesTree.siblings[curNode]; // proof[j] = sibling; assembly { mstore(add(proof, add(32, mul(32, j))), sibling) } - curNode = p.balancesTree.parents[curNode]; + curNode = p.trees.balancesTree.parents[curNode]; depth++; } @@ -378,8 +409,8 @@ library LibProofGen { */ /// @dev Get the leaves of the beacon block tree - function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal pure returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](BEACON_BLOCK_FIELDS); + function _getBeaconBlockLeaves(bytes32 beaconStateRoot) internal view returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](config().BEACON_BLOCK_FIELDS); // Pre-populate leaves with dummy values so sibling/parent tracking is correct for (uint i = 0; i < leaves.length; i++) { @@ -392,8 +423,8 @@ library LibProofGen { } /// @dev Get the leaves of the beacon state tree - function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal pure returns (bytes32[] memory) { - bytes32[] memory leaves = new bytes32[](BEACON_STATE_FIELDS); + function _getBeaconStateLeaves(bytes32 validatorsRoot, bytes32 balancesRoot) internal view returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](config().BEACON_STATE_FIELDS); // Pre-populate leaves with dummy values so sibling/parent tracking is correct for (uint i = 0; i < leaves.length; i++) { diff --git a/src/test/integration/tests/eigenpod/Pectra_Features.t.sol b/src/test/integration/tests/eigenpod/Pectra_Features.t.sol index e69de29bb2..0de11737de 100644 --- a/src/test/integration/tests/eigenpod/Pectra_Features.t.sol +++ b/src/test/integration/tests/eigenpod/Pectra_Features.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "src/test/integration/IntegrationChecks.t.sol"; + +contract Integration_Pectra_Features_Base is IntegrationCheckUtils { + using ArrayLib for *; + + function _init() internal virtual override { + _configAssetTypes(HOLDS_ETH); + // (staker, strategies, initTokenBalances) = _newRandomStaker(); + // cheats.assume(initTokenBalances[0] >= 64 ether); + + // // Deposit staker + // uint[] memory shares = _calculateExpectedShares(strategies, initTokenBalances); + // staker.depositIntoEigenlayer(strategies, initTokenBalances); + // check_Deposit_State(staker, strategies, shares); + // initDepositShares = shares; + // validators = staker.getActiveValidators(); + + // // Slash all validators fully + // slashedGwei = beaconChain.slashValidators(validators, BeaconChainMock.SlashType.Full); + // beaconChain.advanceEpoch_NoRewards(); // Withdraw slashed validators to pod + } + + function testFuzz_consolidate(uint24 _r) public rand(_r) { + User staker = _newEmptyStaker(); + + // Deal ETH and start 2 validators; both ETH1 + (uint40[] memory validators, uint balanceWei,) = staker.startValidators(2); + + staker.verifyWithdrawalCredentials(validators); + } + + function _zip(uint40[] memory a1, uint40[] memory a2) internal pure returns (uint40[] memory) { + uint40[] memory result = new uint40[](a1.length + a2.length); + + uint resultIdx; + for (uint i = 0; i < a1.length; i++) { + result[resultIdx] = a1[i]; + resultIdx++; + } + + for (uint i = 0; i < a2.length; i++) { + result[resultIdx] = a2[i]; + resultIdx++; + } + + return result; + } +} + +// contract Integration_FullySlashedEigenpod_Checkpointed is Integration_FullySlashedEigenpod_Base { +// function _init() internal override { +// super._init(); + +// // // Start & complete a checkpoint +// // staker.startCheckpoint(); +// // check_StartCheckpoint_WithPodBalance_State(staker, 0); +// // staker.completeCheckpoint(); +// // check_CompleteCheckpoint_FullySlashed_State(staker, validators, slashedGwei); +// } + +// function testFuzz_fullSlash_registerStakerAsOperator_Revert_Redeposit(uint24 _rand) public rand(_rand) { +// // // Register staker as operator +// // staker.registerAsOperator(); + +// // // Start a new validator & verify withdrawal credentials +// // cheats.deal(address(staker), 32 ether); +// // (uint40[] memory newValidators,,) = staker.startValidators(); +// // beaconChain.advanceEpoch_NoRewards(); + +// // // We should revert on verifyWithdrawalCredentials since the staker's slashing factor is 0 +// // cheats.expectRevert(IDelegationManagerErrors.FullySlashed.selector); +// // staker.verifyWithdrawalCredentials(newValidators); +// } + +// function testFuzz_fullSlash_registerStakerAsOperator_delegate_undelegate_completeAsShares(uint24 _rand) public rand(_rand) { +// // // Register staker as operator +// // staker.registerAsOperator(); +// // User operator = User(payable(address(staker))); + +// // // Initialize new staker +// // (User staker2, IStrategy[] memory strategies2, uint[] memory initTokenBalances2) = _newRandomStaker(); +// // uint[] memory shares = _calculateExpectedShares(strategies2, initTokenBalances2); +// // staker2.depositIntoEigenlayer(strategies2, initTokenBalances2); +// // check_Deposit_State(staker2, strategies2, shares); + +// // // Delegate to an operator who has now become a staker, this should succeed as slashed operator's BCSF should not affect the staker +// // staker2.delegateTo(operator); +// // check_Delegation_State(staker2, operator, strategies2, shares); + +// // // Register as operator and undelegate - the equivalent of redelegating to yourself +// // Withdrawal[] memory withdrawals = staker2.undelegate(); +// // bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); +// // check_Undelegate_State(staker2, operator, withdrawals, withdrawalRoots, strategies2, shares); + +// // // Complete withdrawals as shares +// // _rollBlocksForCompleteWithdrawals(withdrawals); +// // for (uint i = 0; i < withdrawals.length; i++) { +// // staker2.completeWithdrawalAsShares(withdrawals[i]); +// // check_Withdrawal_AsShares_Undelegated_State(staker2, operator, withdrawals[i], strategies2, shares); +// // } +// } +// } \ No newline at end of file