Skip to content

Commit

Permalink
feat: poc for partial withdrawal batching, does not compile
Browse files Browse the repository at this point in the history
  • Loading branch information
wadealexc committed Apr 16, 2024
1 parent 1ec0c0e commit 4853f23
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 414 deletions.
62 changes: 7 additions & 55 deletions src/contracts/interfaces/IEigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
* to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts
*/
interface IEigenPod {

enum VALIDATOR_STATUS {
INACTIVE, // doesnt exist
ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod
Expand All @@ -38,22 +39,14 @@ interface IEigenPod {
VALIDATOR_STATUS status;
}

/**
* @notice struct used to store amounts related to proven withdrawals in memory. Used to help
* manage stack depth and optimize the number of external calls, when batching withdrawal operations.
*/
struct VerifiedWithdrawal {
// amount to send to a podOwner from a proven withdrawal
uint256 amountToSendGwei;
// difference in shares to be recorded in the eigenPodManager, as a result of the withdrawal
int256 sharesDeltaGwei;
}
struct Checkpoint {
bytes32 beaconBlockRoot;
bytes32 beaconStateRoot;

uint256 podBalanceGwei;
int256 balanceDeltas;

enum PARTIAL_WITHDRAWAL_CLAIM_STATUS {
REDEEMED,
PENDING,
FAILED
uint256 proofsRemaining;
}

/// @notice Emitted when an ETH validator stakes via this eigenPod
Expand All @@ -66,22 +59,6 @@ interface IEigenPod {
// is the validator's balance that is credited on EigenLayer.
event ValidatorBalanceUpdated(uint40 validatorIndex, uint64 balanceTimestamp, uint64 newValidatorBalanceGwei);

/// @notice Emitted when an ETH validator is prove to have withdrawn from the beacon chain
event FullWithdrawalRedeemed(
uint40 validatorIndex,
uint64 withdrawalTimestamp,
address indexed recipient,
uint64 withdrawalAmountGwei
);

/// @notice Emitted when a partial withdrawal claim is successfully redeemed
event PartialWithdrawalRedeemed(
uint40 validatorIndex,
uint64 withdrawalTimestamp,
address indexed recipient,
uint64 partialWithdrawalAmountGwei
);

/// @notice Emitted when restaked beacon chain ETH is withdrawn from the eigenPod.
event RestakedBeaconChainETHWithdrawn(address indexed recipient, uint256 amount);

Expand All @@ -94,10 +71,6 @@ interface IEigenPod {
/// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn
event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn);


/// @notice The max amount of eth, in gwei, that can be restaked per validator
function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64);

/// @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);

Expand Down Expand Up @@ -141,9 +114,6 @@ interface IEigenPod {
/// @notice Returns the validatorInfo struct for the provided pubkey
function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory);

///@notice mapping that tracks proven withdrawals
function provenWithdrawal(bytes32 validatorPubkeyHash, uint64 slot) external view returns (bool);

/// @notice This returns the status of a given validator
function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS);

Expand All @@ -170,24 +140,6 @@ interface IEigenPod {
)
external;

/**
* @notice This function records an update (either increase or decrease) in the pod's balance in the StrategyManager.
It also verifies a merkle proof of the validator's current beacon chain balance.
* @param oracleTimestamp The oracleTimestamp whose state root the `proof` will be proven against.
* Must be within `VERIFY_BALANCE_UPDATE_WINDOW_SECONDS` of the current block.
* @param validatorIndices is the list of indices of the validators being proven, refer to consensus specs
* @param validatorFieldsProofs proofs against the `beaconStateRoot` for each validator in `validatorFields`
* @param validatorFields are the fields of the "Validator Container", refer to consensus specs
* @dev For more details on the Beacon Chain spec, see: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator
*/
function verifyBalanceUpdates(
uint64 oracleTimestamp,
uint40[] calldata validatorIndices,
BeaconChainProofs.StateRootProof calldata stateRootProof,
bytes[] calldata validatorFieldsProofs,
bytes32[][] calldata validatorFields
) external;

/**
* @notice Called by the pod owner to activate restaking by withdrawing
* all existing ETH from the pod and preventing further withdrawals via
Expand Down
211 changes: 37 additions & 174 deletions src/contracts/libraries/BeaconChainProofs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,18 @@ library BeaconChainProofs {

bytes8 internal constant UINT64_MASK = 0xffffffffffffffff;

/// @notice This struct contains the merkle proofs and leaves needed to verify a partial/full withdrawal
struct WithdrawalProof {
bytes withdrawalProof;
bytes slotProof;
bytes executionPayloadProof;
bytes timestampProof;
bytes historicalSummaryBlockRootProof;
uint64 blockRootIndex;
uint64 historicalSummaryIndex;
uint64 withdrawalIndex;
bytes32 blockRoot;
bytes32 slotRoot;
bytes32 timestampRoot;
bytes32 executionPayloadRoot;
}

/// @notice This struct contains the root and proof for verifying the state root against the oracle block root
struct StateRootProof {
bytes32 beaconStateRoot;
bytes proof;
}

struct BalanceProof {
bytes32 pubkeyHash;
bytes32 balanceRoot;
bytes proof;
}

/**
* @notice This function verifies merkle proofs of the fields of a certain validator against a beacon chain state root
* @param validatorIndex the index of the proven validator
Expand Down Expand Up @@ -144,6 +134,37 @@ library BeaconChainProofs {
);
}

function verifyValidatorBalance(
bytes32 beaconStateRoot,
uint40 validatorIndex,
BalanceProof memory proof
) internal view returns (uint64 validatorBalanceGwei) {
require(
proof.proof.length == 32 * ((BALANCE_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length"
);

// Beacon chain balances are lists of uint64 values which are grouped together in 4s when merkleized
uint256 balanceIndex = uint256(validatorIndex / 4);
/**
* Note: Merkleization of the balance root tree uses MerkleizeWithMixin, i.e., the length of the array is hashed with the root of
* the array. Thus we shift the BALANCE_INDEX over by BALANCE_TREE_HEIGHT + 1 and not just BALANCE_TREE_HEIGHT.
*/
balanceIndex = (BALANCE_INDEX << (BALANCE_TREE_HEIGHT + 1)) | balanceIndex;

require(
Merkle.verifyInclusionSha256({
proof: proof.proof,
root: beaconStateRoot,
leaf: proof.balanceRoot,
index: balanceIndex
}),
"BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof"
);

return getBalanceAtIndex(proof.balanceRoot, validatorIndex);
}

/**
* @notice This function verifies the latestBlockHeader against the state root. the latestBlockHeader is
* a tracked in the beacon state.
Expand Down Expand Up @@ -172,148 +193,6 @@ library BeaconChainProofs {
);
}

/**
* @notice This function verifies the slot and the withdrawal fields for a given withdrawal
* @param withdrawalProof is the provided set of merkle proofs
* @param withdrawalFields is the serialized withdrawal container to be proven
*/
function verifyWithdrawal(
bytes32 beaconStateRoot,
bytes32[] calldata withdrawalFields,
WithdrawalProof calldata withdrawalProof,
uint64 denebForkTimestamp
) internal view {
require(
withdrawalFields.length == 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT,
"BeaconChainProofs.verifyWithdrawal: withdrawalFields has incorrect length"
);

require(
withdrawalProof.blockRootIndex < 2 ** BLOCK_ROOTS_TREE_HEIGHT,
"BeaconChainProofs.verifyWithdrawal: blockRootIndex is too large"
);
require(
withdrawalProof.withdrawalIndex < 2 ** WITHDRAWALS_TREE_HEIGHT,
"BeaconChainProofs.verifyWithdrawal: withdrawalIndex is too large"
);

require(
withdrawalProof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT,
"BeaconChainProofs.verifyWithdrawal: historicalSummaryIndex is too large"
);

//Note: post deneb hard fork, the number of exection payload header fields increased from 15->17, adding an extra level to the tree height
uint256 executionPayloadHeaderFieldTreeHeight = (getWithdrawalTimestamp(withdrawalProof) < denebForkTimestamp) ? EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA : EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB;
require(
withdrawalProof.withdrawalProof.length ==
32 * (executionPayloadHeaderFieldTreeHeight + WITHDRAWALS_TREE_HEIGHT + 1),
"BeaconChainProofs.verifyWithdrawal: withdrawalProof has incorrect length"
);
require(
withdrawalProof.executionPayloadProof.length ==
32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyWithdrawal: executionPayloadProof has incorrect length"
);
require(
withdrawalProof.slotProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT),
"BeaconChainProofs.verifyWithdrawal: slotProof has incorrect length"
);
require(
withdrawalProof.timestampProof.length == 32 * (executionPayloadHeaderFieldTreeHeight),
"BeaconChainProofs.verifyWithdrawal: timestampProof has incorrect length"
);

require(
withdrawalProof.historicalSummaryBlockRootProof.length ==
32 *
(BEACON_STATE_FIELD_TREE_HEIGHT +
(HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) +
1 +
(BLOCK_ROOTS_TREE_HEIGHT)),
"BeaconChainProofs.verifyWithdrawal: historicalSummaryBlockRootProof has incorrect length"
);
/**
* Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the "block_root_summary" within the individual
* "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is hashed with the root of the array,
* but not here.
*/
uint256 historicalBlockHeaderIndex = (HISTORICAL_SUMMARIES_INDEX <<
((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT))) |
(uint256(withdrawalProof.historicalSummaryIndex) << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) |
(BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) |
uint256(withdrawalProof.blockRootIndex);

require(
Merkle.verifyInclusionSha256({
proof: withdrawalProof.historicalSummaryBlockRootProof,
root: beaconStateRoot,
leaf: withdrawalProof.blockRoot,
index: historicalBlockHeaderIndex
}),
"BeaconChainProofs.verifyWithdrawal: Invalid historicalsummary merkle proof"
);

//Next we verify the slot against the blockRoot
require(
Merkle.verifyInclusionSha256({
proof: withdrawalProof.slotProof,
root: withdrawalProof.blockRoot,
leaf: withdrawalProof.slotRoot,
index: SLOT_INDEX
}),
"BeaconChainProofs.verifyWithdrawal: Invalid slot merkle proof"
);

{
// Next we verify the executionPayloadRoot against the blockRoot
uint256 executionPayloadIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) |
EXECUTION_PAYLOAD_INDEX;
require(
Merkle.verifyInclusionSha256({
proof: withdrawalProof.executionPayloadProof,
root: withdrawalProof.blockRoot,
leaf: withdrawalProof.executionPayloadRoot,
index: executionPayloadIndex
}),
"BeaconChainProofs.verifyWithdrawal: Invalid executionPayload merkle proof"
);
}

// Next we verify the timestampRoot against the executionPayload root
require(
Merkle.verifyInclusionSha256({
proof: withdrawalProof.timestampProof,
root: withdrawalProof.executionPayloadRoot,
leaf: withdrawalProof.timestampRoot,
index: TIMESTAMP_INDEX
}),
"BeaconChainProofs.verifyWithdrawal: Invalid timestamp merkle proof"
);

{
/**
* Next we verify the withdrawal fields against the executionPayloadRoot:
* First we compute the withdrawal_index, then we merkleize the
* withdrawalFields container to calculate the withdrawalRoot.
*
* Note: Merkleization of the withdrawals root tree uses MerkleizeWithMixin, i.e., the length of the array is hashed with the root of
* the array. Thus we shift the WITHDRAWALS_INDEX over by WITHDRAWALS_TREE_HEIGHT + 1 and not just WITHDRAWALS_TREE_HEIGHT.
*/
uint256 withdrawalIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) |
uint256(withdrawalProof.withdrawalIndex);
bytes32 withdrawalRoot = Merkle.merkleizeSha256(withdrawalFields);
require(
Merkle.verifyInclusionSha256({
proof: withdrawalProof.withdrawalProof,
root: withdrawalProof.executionPayloadRoot,
leaf: withdrawalRoot,
index: withdrawalIndex
}),
"BeaconChainProofs.verifyWithdrawal: Invalid withdrawal merkle proof"
);
}
}

/**
* @notice This function replicates the ssz hashing of a validator's pubkey, outlined below:
* hh := ssz.NewHasher()
Expand All @@ -326,22 +205,6 @@ library BeaconChainProofs {
return sha256(abi.encodePacked(validatorPubkey, bytes16(0)));
}

/**
* @dev Retrieve the withdrawal timestamp
*/
function getWithdrawalTimestamp(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) {
return
Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot);
}

/**
* @dev Converts the withdrawal's slot to an epoch
*/
function getWithdrawalEpoch(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) {
return
Endian.fromLittleEndianUint64(withdrawalProof.slotRoot) / SLOTS_PER_EPOCH;
}

/**
* Indices for validator fields (refer to consensus specs):
* 0: pubkey
Expand Down
Loading

0 comments on commit 4853f23

Please sign in to comment.