Defragment: Implement EIP-7251 (MaxEB) ====== TL;DR: ------ 1. [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) improves MaxEB, and is [scoping](https://eips.ethereum.org/EIPS/eip-7600) into [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/) Upgrade ([~2025/Q1](https://x.com/TimBeiko/status/1793684244612407687)), and with the mission to [sustain validator set size growth](https://ethresear.ch/t/sticking-to-8192-signatures-per-slot-post-ssf-how-and-why/17989), and preparing for [SSF](https://ethereum.org/en/roadmap/single-slot-finality/) and [ePBS](https://ethereum.org/en/roadmap/pbs/). 2. I have [studied](https://hackmd.io/@georgesheth/HJKkx3NSR) that [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) has 6 features and explains what they are. 3. This article aims to explain how to implement all these features for [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/) Upgrade at coding level ([Spec](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/) and [Test](https://github.com/ethereum/execution-spec-tests)). In summary, 1. Feature `1, 2, 3, 4` are fully implemented. 2. Feature `5` needs to be completed by implementing one missing part and Feature `6` needs to be fully implemented. 4. Welcome suggestion and collaboration. 5. My other relevent articles on EIP-7251: - [BeaconState and Validator Balance for EIP-7251](https://hackmd.io/@georgesheth/BJGl24HYA) - [History of EIP-7251](https://hackmd.io/@georgesheth/rJxnQBrtC) - [Feature Lists of EIP-7251](https://hackmd.io/@georgesheth/HJKkx3NSR) - [Implement EIP-7251](https://hackmd.io/@georgesheth/Hk2r2BHFC) - [Develop Push Based Custom Ceiling Partial Feature Withdraw for EIP-7251](https://hackmd.io/@georgesheth/BJ5tkLBtA) Feature 1: Update `validator.effective_balance` =============================================== Feature Description: -------------------- 1. Elastic`validator.effective_balance` to large value range. 2. by increasing the`MAX_EFFECTIVE_BALANCE`. 3. while creating a `MIN_ACTIVATION_BALANCE`. After EIP-7251 Looklike: ------------------------ 1. `MIN_ACTIVATION_BALANCE`sets the threadhold to activate a validator, which is `32 ETH` fixed. 2. `MAX_EFFECTIVE_BALANCE` also sets the threadhold to cap for `validator.active_balance` which is `2048 ETH` fixed. 3. `validator.active_balance` is an integer value from `[0, 1, ..., 31, 32, ..., 2047, 2048] ETH` which is controlled by `EFFECTIVE_BALANCE_INCREMENT` (`1 ETH` fixed) How to implement: ----------------- 1. Update the value of `MAX_EFFECTIVE_BALANCE_ELECTRA` 2. Create `MIN_ACTIVATION_BALANCE` as a new constant. 3. Exam the impact of `validator.effective_balance` ### Spec change on Constants | Name | Value | Comments | | ---- | ---- | ---- | | MIN_ACTIVATION_BALANCE | Gwei(`2**5 * 10**9`) (= 32,000,000,000) | equavilent to MAX_EFFECTIVE_BALANCE = 32 ETH since Phase0 | | MAX_EFFECTIVE_BALANCE_ELECTRA | Gwei(`2**11 * 10**9`) (= 2048,000,000,000) | updated from 32 ETH to 2048 ETH | ### Impact of updating the value of `MAX_EFFECTIVE_BALANCE_ELECTRA` 1. No logical changes in `compute_proposer_index()`. Only modified to use `MAX_EFFECTIVE_BALANCE_ELECTRA`**.** 2. No logical changes in `get_next_sync_committee_indices()`. Only modified to use `MAX_EFFECTIVE_BALANCE_ELECTRA`. 3. New `get_validator_max_effective_balancefor()` to help backward compatibility. ```python= def get_validator_max_effective_balance(validator: Validator) -> Gwei: """ Get max effective balance for ``validator``. """ if has_compounding_withdrawal_credential(validator): return MAX_EFFECTIVE_BALANCE_ELECTRA else: return MIN_ACTIVATION_BALANCE ``` ### Impact of creating `MIN_ACTIVATION_BALANCE` 1. Modified `is_eligible_for_activation_queue` to use `MIN_ACTIVATION_BALANCE` instead of `MAX_EFFECTIVE_BALANCE`. ```python= def is_eligible_for_activation_queue(validator: Validator) -> bool: """ Check if ``validator`` is eligible to be placed into the activation queue. """ return ( validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH and validator.effective_balance >= MIN_ACTIVATION_BALANCE # [Modified in Electra:EIP7251] ) ``` ### Impact on how to process `validator.effective_balance` 1. Modified `process_effective_balance_updates()` to use the new limit for the maximum effective balance. ```python= def process_effective_balance_updates(state: BeaconState) -> None: # Update effective balances with hysteresis for index, validator in enumerate(state.validators): balance = state.balances[index] HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER # [Modified in Electra:EIP7251] EFFECTIVE_BALANCE_LIMIT = ( MAX_EFFECTIVE_BALANCE_ELECTRA if has_compounding_withdrawal_credential(validator) else MIN_ACTIVATION_BALANCE ) if ( balance + DOWNWARD_THRESHOLD < validator.effective_balance or validator.effective_balance + UPWARD_THRESHOLD < balance ): validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, EFFECTIVE_BALANCE_LIMIT) ``` Feature 2: Enable validator consolidation (optional): ===================================================== After EIP-7251 Looklike : ------------------------- 1. Stakers may like to consolidate their validators. 2. It is optional, and gasless. 3. Rely on [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) to generate a validator consolidation request. How to implement: ----------------- 1. Create new container for `ConsolidationRequest` and `PendingConsolidation` 2. Queue Control of `ConsolidationRequest` 3. Help function of `get_active_balance` 4. Process `ConsolidationRequest` ### Impact of new `ConsolidationRequest` and `PendingConsolidation` 1. `ConsolidationRequest`[¶](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/#consolidationrequest)*Note*: The container is new in EIP7251. ```python= class ConsolidationRequest(Container): source_address: ExecutionAddress source_pubkey: BLSPubkey target_pubkey: BLSPubkey ``` 1. `PendingConsolidation`[¶](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/#pendingconsolidation)*Note*: The container is new in EIP7251. ```python= class PendingConsolidation(Container): source_index: ValidatorIndex target_index: ValidatorIndex ``` ### Impact of process of ConsolidationRequest queue | Name | Value | Description | | ---- | ---- | ---- | | MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD | uint64(1) (= 1) | [New in Electra:EIP7002] Maximum number of execution layer consolidation requests in each payload | 1. New `get_consolidation_churn_limit()` ```python= def get_consolidation_churn_limit(state: BeaconState) -> Gwei: return get_balance_churn_limit(state) - get_activation_exit_churn_limit(state) ``` 1. New `compute_consolidation_epoch_and_update_churn` ```python= def compute_consolidation_epoch_and_update_churn(state: BeaconState, consolidation_balance: Gwei) -> Epoch: earliest_consolidation_epoch = max( state.earliest_consolidation_epoch, compute_activation_exit_epoch(get_current_epoch(state))) per_epoch_consolidation_churn = get_consolidation_churn_limit(state) # New epoch for consolidations. if state.earliest_consolidation_epoch < earliest_consolidation_epoch: consolidation_balance_to_consume = per_epoch_consolidation_churn else: consolidation_balance_to_consume = state.consolidation_balance_to_consume # Consolidation doesn't fit in the current earliest epoch. if consolidation_balance > consolidation_balance_to_consume: balance_to_process = consolidation_balance - consolidation_balance_to_consume additional_epochs = (balance_to_process - 1) // per_epoch_consolidation_churn + 1 earliest_consolidation_epoch += additional_epochs consolidation_balance_to_consume += additional_epochs * per_epoch_consolidation_churn # Consume the balance and update state variables. state.consolidation_balance_to_consume = consolidation_balance_to_consume - consolidation_balance state.earliest_consolidation_epoch = earliest_consolidation_epoch return state.earliest_consolidation_epoch ``` ### Impact on Validator.effective_balance 1. New `get_active_balance` ```python= def get_active_balance(state: BeaconState, validator_index: ValidatorIndex) -> Gwei: max_effective_balance = get_validator_max_effective_balance(state.validators[validator_index]) return min(state.balances[validator_index], max_effective_balance) ``` ### Impact of processing `ConsolidationRequest` 1. New `process_pending_consolidations` ```python= def process_pending_consolidations(state: BeaconState) -> None: next_pending_consolidation = 0 for pending_consolidation in state.pending_consolidations: source_validator = state.validators[pending_consolidation.source_index] if source_validator.slashed: next_pending_consolidation += 1 continue if source_validator.withdrawable_epoch > get_current_epoch(state): break # Churn any target excess active balance of target and raise its max switch_to_compounding_validator(state, pending_consolidation.target_index) # Move active balance to target. Excess balance is withdrawable. active_balance = get_active_balance(state, pending_consolidation.source_index) decrease_balance(state, pending_consolidation.source_index, active_balance) increase_balance(state, pending_consolidation.target_index, active_balance) next_pending_consolidation += 1 state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:] ``` 1. New `process_consolidation_request` for Execution layer consolidation requests ```python= def process_consolidation_request( state: BeaconState, consolidation_request: ConsolidationRequest ) -> None: # If the pending consolidations queue is full, consolidation requests are ignored if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT: return # If there is too little available consolidation churn limit, consolidation requests are ignored if get_consolidation_churn_limit(state) <= MIN_ACTIVATION_BALANCE: return validator_pubkeys = [v.pubkey for v in state.validators] # Verify pubkeys exists request_source_pubkey = consolidation_request.source_pubkey request_target_pubkey = consolidation_request.target_pubkey if request_source_pubkey not in validator_pubkeys: return if request_target_pubkey not in validator_pubkeys: return source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey)) target_index = ValidatorIndex(validator_pubkeys.index(request_target_pubkey)) source_validator = state.validators[source_index] target_validator = state.validators[target_index] # Verify that source != target, so a consolidation cannot be used as an exit. if source_index == target_index: return # Verify source withdrawal credentials has_correct_credential = has_execution_withdrawal_credential(source_validator) is_correct_source_address = ( source_validator.withdrawal_credentials[12:] == consolidation_request.source_address ) if not (has_correct_credential and is_correct_source_address): return # Verify that target has execution withdrawal credentials if not has_execution_withdrawal_credential(target_validator): return # Verify the source and the target are active current_epoch = get_current_epoch(state) if not is_active_validator(source_validator, current_epoch): return if not is_active_validator(target_validator, current_epoch): return # Verify exits for source and target have not been initiated if source_validator.exit_epoch != FAR_FUTURE_EPOCH: return if target_validator.exit_epoch != FAR_FUTURE_EPOCH: return # Initiate source validator exit and append pending consolidation source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn( state, source_validator.effective_balance ) source_validator.withdrawable_epoch = Epoch( source_validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY ) state.pending_consolidations.append(PendingConsolidation( source_index=source_index, target_index=target_index )) ``` Feature 3: Enable compounding rewards management. ================================================= After EIP-7251 Looklike: ------------------------ 1. Earn compounding rewards when `validator.effective_balance` > `32 ETH` 2. Earn compounding rewards only stop when `validator.effective_balance` > `2048 ETH`. How to implement: ----------------- 1. Create new container for `PendingBalanceDeposit` 2. Enable compounding validator switch for backward compatibility 3. All new help functions to identify compounding validators. ### Impact of new `PendingBalanceDeposit` 1. New `PendingBalanceDeposit` in EIP7251 ```python= class PendingBalanceDeposit(Container): index: ValidatorIndex amount: Gwei ``` ### Impact of compounding validator switch function 1. New `queue_excess_active_balance` ```python= def queue_excess_active_balance(state: BeaconState, index: ValidatorIndex) -> None: balance = state.balances[index] if balance > MIN_ACTIVATION_BALANCE: excess_balance = balance - MIN_ACTIVATION_BALANCE state.balances[index] = MIN_ACTIVATION_BALANCE state.pending_balance_deposits.append( PendingBalanceDeposit(index=index, amount=excess_balance) # [Modified in Electra:EIP7251] ) ``` 1. New `switch_to_compounding_validator` ```python= def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None: validator = state.validators[index] if has_eth1_withdrawal_credential(validator): validator.withdrawal_credentials = COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] queue_excess_active_balance(state, index) ``` ### Impact of adding new help function to identify compounding validators. 1. New `is_compounding_withdrawal_credential` ```python= def is_compounding_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: return withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX ``` 1. New `has_compounding_withdrawal_credential` ```python= def has_compounding_withdrawal_credential(validator: Validator) -> bool: """ Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. """ return is_compounding_withdrawal_credential(validator.withdrawal_credentials) ``` 1. New `has_execution_withdrawal_credential` ```python= def has_execution_withdrawal_credential(validator: Validator) -> bool: """ Check if ``validator`` has a 0x01 or 0x02 prefixed withdrawal credential. """ return has_compounding_withdrawal_credential(validator) or has_eth1_withdrawal_credential(validator) ``` Feature 4: Extend deposit and top-up. ===================================== Looklike After EIP-7251: ------------------------ 1. **Deposit for activation only**: Staker should deposit 32 ETH in one go for validator activation, and avoid [edge case](https://eth2book.info/capella/part2/incentives/balances/#an-edge-case). 2. **Deposit for activation and top-up for weight**: Staker would like to deposit more than 32 ETH in one go for validator activation and also increase weight and earn compounding rewards. 3. **Top-up for weight**: Staker would like to continue to top-up ETH to increase weight and earn compounding rewards 4. **Top-up to avoid ejection**: Staker may only deposit again if validator.effective_balance is under 32 ETH to top-up into 32 ETH again. How to implement: ----------------- 1. Create a new `BalanceDeposit` container, in [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110) 2. Register validator and Queue `PendingBalanceDeposit` 3. Queue Control of `BalanceDeposit` 4. Process `PendingBalanceDeposit` queue ### Impact of adding new balance deposit 1. New `DepositRequest`. The container is new in EIP6110. ```python= class DepositRequest(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 amount: Gwei signature: BLSSignature index: uint64 ``` 1. New `PendingBalanceDeposit` in EIP7251 ```python= class PendingBalanceDeposit(Container): index: ValidatorIndex amount: Gwei ``` ### Impact of add and queue `BalanceDeposit` 1. Updated `get_validator_from_deposit` ```python= def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32) -> Validator: return Validator( pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, activation_eligibility_epoch=FAR_FUTURE_EPOCH, activation_epoch=FAR_FUTURE_EPOCH, exit_epoch=FAR_FUTURE_EPOCH, withdrawable_epoch=FAR_FUTURE_EPOCH, effective_balance=0, # [Modified in Electra:EIP7251] ) ``` 1. Modified `add_validator_to_registry` ```python= def add_validator_to_registry(state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64) -> None: index = get_index_for_new_validator(state) validator = get_validator_from_deposit(pubkey, withdrawal_credentials) set_or_append_list(state.validators, index, validator) set_or_append_list(state.balances, index, 0) # [Modified in Electra:EIP7251] set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) set_or_append_list(state.inactivity_scores, index, uint64(0)) state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) # [New in Electra:EIP7251] ``` ### Impact of Queue Control of `BalanceDeposit` | Name | Value | Description | | ---- | ---- | ---- | | MAX_DEPOSIT_REQUESTS_PER_PAYLOAD | uint64(2**13) (= 8,192) | [New in Electra:EIP6110] Maximum number of deposit receipts allowed in each payload | TODO: cross check how execution layer handles it. ### Impact of Process `PendingBalanceDeposit` queue 1. Updated `apply_deposit` for execution layer ```python= def apply_deposit(state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64, signature: BLSSignature) -> None: validator_pubkeys = [v.pubkey for v in state.validators] if pubkey not in validator_pubkeys: # Verify the deposit signature (proof of possession) which is not checked by the deposit contract if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): add_validator_to_registry(state, pubkey, withdrawal_credentials, amount) else: # Increase balance by deposit amount index = ValidatorIndex(validator_pubkeys.index(pubkey)) state.pending_balance_deposits.append( PendingBalanceDeposit(index=index, amount=amount) ) # [Modified in Electra:EIP-7251] # Check if valid deposit switch to compounding credentials if ( is_compounding_withdrawal_credential(withdrawal_credentials) and has_eth1_withdrawal_credential(state.validators[index]) and is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature) ): switch_to_compounding_validator(state, index) ``` 1. **New** **`process_deposit_request`****, This function is new in Electra:EIP6110** ```python= def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: # Set deposit request start index if state.deposit_requests_start_index == UNSET_DEPOSIT_REQUESTS_START_INDEX: state.deposit_requests_start_index = deposit_request.index apply_deposit( state=state, pubkey=deposit_request.pubkey, withdrawal_credentials=deposit_request.withdrawal_credentials, amount=deposit_request.amount, signature=deposit_request.signature, ) ``` Feature 5: Extend withdraw and exit: ==================================== Looklike After EIP-7251: ------------------------ 1. **Voluntary Full Exit**: unstake everything and become excited. 2. **Rewards Sweep above MaxEB**: automatic partial withdrawal (gaslessly) excess balance when validator.active_balance is larger than 2048 ETH to maintain 2048 ETH consistently. 4. **Rewards Sweep above custom ceiling but less than MaxEB**: (TODO) 1. A validator can setup a custom ceiling for auto sweep. The custom ceiling should be a value between 32 to 2048 ETH. 2. Automatic partial withdrawal (gaslessly) excess balance when validator.active_balance is larger than custom ceiling. How to implement: ----------------- 1. No change to Voluntary Full Exit. 2. No change to Rewards Sweep above MaxEB 3. For manual partial withdrawal, we need to add a PartialWithdrawal container and modify `is_partially_withdrawable_validator`, and then modify in `get_expected_withdrawals()`, and then start to process the manual partial withdrawal from the Execitional Layer. 4. Rewards Sweep above custom ceiling but less MaxEB: 1. The logic is still under development. 2. (TODO) ### Impacts of adding a new PartialWithdrawal container 1. `WithdrawalRequest`[¶](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/#withdrawalrequest), *Note*: The container is new in EIP7251:EIP7002. ```python= class WithdrawalRequest(Container): source_address: ExecutionAddress validator_pubkey: BLSPubkey amount: Gwei ``` 1. `PendingPartialWithdrawal`[¶](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/#pendingpartialwithdrawal), *Note*: The container is new in EIP7251. ```python= class PendingPartialWithdrawal(Container): index: ValidatorIndex amount: Gwei withdrawable_epoch: Epoch ``` ### Impact of Queue Control of `BalanceDeposit` | Name | Value | Description | | ---- | ---- | ---- | | MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD | uint64(2**4) (= 16) | [New in Electra:EIP7002] Maximum number of execution layer withdrawal requests in each payload | ### Impacts of process manual partial withdraw request 1. Modified `is_partially_withdrawable_validator`to use `get_validator_max_effective_balance` instead of `MAX_EFFECTIVE_BALANCE` and `has_execution_withdrawal_credential` instead of `has_eth1_withdrawal_credential`. ```python= def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: """ Check if ``validator`` is partially withdrawable. """ max_effective_balance = get_validator_max_effective_balance(validator) has_max_effective_balance = validator.effective_balance == max_effective_balance # [Modified in Electra:EIP7251] has_excess_balance = balance > max_effective_balance # [Modified in Electra:EIP7251] return ( has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251] and has_max_effective_balance and has_excess_balance ) ``` 1. Modified `get_expected_withdrawals()` ```python= def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal], uint64]: ... ... withdrawals: List[Withdrawal] = [] # [New in Electra:EIP7251] Consume pending partial withdrawals for withdrawal in state.pending_partial_withdrawals: ... ... if validator.exit_epoch == FAR_FUTURE_EPOCH and has_sufficient_effective_balance and has_excess_balance: withdrawable_balance = min(state.balances[withdrawal.index] - MIN_ACTIVATION_BALANCE, withdrawal.amount) withdrawals.append(Withdrawal( ... ... amount=withdrawable_balance, )) withdrawal_index += WithdrawalIndex(1) partial_withdrawals_count = len(withdrawals) # Sweep for remaining. bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) for _ in range(bound): validator = state.validators[validator_index] balance = state.balances[validator_index] if is_fully_withdrawable_validator(validator, balance, epoch): withdrawals.append(Withdrawal( ... ... amount=balance, )) withdrawal_index += WithdrawalIndex(1) elif is_partially_withdrawable_validator(validator, balance): withdrawals.append(Withdrawal( ... ... amount=balance - get_validator_max_effective_balance(validator), # [Modified in Electra:EIP7251] )) withdrawal_index += WithdrawalIndex(1) if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: break validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) return withdrawals, partial_withdrawals_count ``` 1. Modified `process_withdrawal_request()` ```python= def process_withdrawal_request( state: BeaconState, withdrawal_request: WithdrawalRequest ) -> None: amount = withdrawal_request.amount is_full_exit_request = amount == FULL_EXIT_REQUEST_AMOUNT # FULL_EXIT_REQUEST_AMOUNT = 0 if is_full_exit_request: # Only exit validator if it has no pending withdrawals in the queue if pending_balance_to_withdraw == 0: initiate_validator_exit(state, index) return ... ... # Only allow partial withdrawals with compounding withdrawal credentials if has_compounding_withdrawal_credential(validator) and has_sufficient_effective_balance and has_excess_balance: to_withdraw = min( state.balances[index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw, amount ) ... ... state.pending_partial_withdrawals.append(PendingPartialWithdrawal( index=index, amount=to_withdraw, withdrawable_epoch=withdrawable_epoch, )) ``` 1. Modified `process_withdrawals()` ```python= def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: expected_withdrawals, partial_withdrawals_count = get_expected_withdrawals(state) # [Modified in Electra:EIP7251] assert len(payload.withdrawals) == len(expected_withdrawals) for expected_withdrawal, withdrawal in zip(expected_withdrawals, payload.withdrawals): assert withdrawal == expected_withdrawal decrease_balance(state, withdrawal.validator_index, withdrawal.amount) # Update pending partial withdrawals [New in Electra:EIP7251] state.pending_partial_withdrawals = state.pending_partial_withdrawals[partial_withdrawals_count:] # Update the next withdrawal index if this block contained withdrawals if len(expected_withdrawals) != 0: latest_withdrawal = expected_withdrawals[-1] state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) # Update the next validator index to start the next withdrawal sweep if len(expected_withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: # Next sweep starts after the latest withdrawal's validator index next_validator_index = ValidatorIndex((expected_withdrawals[-1].validator_index + 1) % len(state.validators)) state.next_withdrawal_validator_index = next_validator_index else: # Advance sweep by the max length of the sweep if there was not a full set of withdrawals next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP next_validator_index = ValidatorIndex(next_index % len(state.validators)) state.next_withdrawal_validator_index = next_validator_index ``` Feature 6: Update slashing and penalty: ======================================= After EIP-7251 Looklike: ------------------------ 1. **Initial slashing penalty**: fixed to a constant amount (1 ETH fixed, not 2048*1/32) or modified to scale sublinearly 2. For about 36 days, the validator is removed from the active validation set and is placed in the exit queue. 3. **Correlation penalty**: is modified to scale quadratically rather than linearly. How to implement: ----------------- 1. still under discussion, but with below [proposal](https://ethresear.ch/t/slashing-penalty-analysis-eip-7251/16509) from Mike 2. Initial slashing penalty is fixed to a constant amount (1 ETH fixed, not 2048*1/32) or modified to scale sublinearly 3. Correlation penalty is modified to scale quadratically rather than linearly. ### Impacts of modifying Initial Slashing Penalty 1. Modified `slash_validator()` ```python= def slash_validator(state: BeaconState, slashed_index: ValidatorIndex, whistleblower_index: ValidatorIndex=None) -> None: ... ... state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance # existing logic # decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT) # TODO: to be add into Electra slashing_penalty = MIN_SLASHING_PENALTY decrease_balance(state, slashed_index, slashing_penalty) ... ... ``` ### Impact of modifying correlation penalty 1. Modified `process_slashings()` ```python= def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei: """ Return the combined effective balance of the ``indices``. ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. Math safe up to ~10B ETH, after which this overflows uint64. """ return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([state.validators[index].effective_balance for index in indices]))) def get_total_active_balance(state: BeaconState) -> Gwei: """ Return the combined effective balance of the active validators. Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. """ return get_total_balance(state, set(get_active_validator_indices(state, get_current_epoch(state)))) def process_slashings(state: BeaconState) -> None: epoch = get_current_epoch(state) total_balance = get_total_active_balance(state) adjusted_total_slashing_balance = min(sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER, total_balance) for index, validator in enumerate(state.validators): if validator.slashed and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch: increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from penalty numerator to avoid uint64 overflow # existing logic #penalty_numerator = validator.effective_balance // increment * adjusted_total_slashing_balance #penalty = penalty_numerator // total_balance * increment # TODO penalty_numerator = validator.effective_balance**2 // increment * adjusted_total_slashing_balance penalty_numerator *= PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX penalty = penalty_numerator // total_balance**2 * increment decrease_balance(state, ValidatorIndex(index), penalty) ``` Other Changes to the Spec: ========================== Impact of modify Execution Payload ---------------------------------- 1. Modified `ExecutionPayload()` ```python= class ExecutionPayload(Container): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress state_root: Bytes32 receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] prev_randao: Bytes32 block_number: uint64 gas_limit: uint64 gas_used: uint64 timestamp: uint64 extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 # Extra payload fields block_hash: Hash32 transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] blob_gas_used: uint64 excess_blob_gas: uint64 deposit_requests: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] # [New in Electra:EIP6110] # [New in Electra:EIP7002:EIP7251] withdrawal_requests: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] # [New in Electra:EIP7251] consolidation_requests: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] ``` 1. Modified `ExecutionPayloadHeader()` ```python= class ExecutionPayloadHeader(Container): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress state_root: Bytes32 receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] prev_randao: Bytes32 block_number: uint64 gas_limit: uint64 gas_used: uint64 timestamp: uint64 extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 # Extra payload fields block_hash: Hash32 transactions_root: Root withdrawals_root: Root blob_gas_used: uint64 excess_blob_gas: uint64 deposit_requests_root: Root # [New in Electra:EIP6110] withdrawal_requests_root: Root # [New in Electra:EIP7002:EIP7251] consolidation_requests_root: Root # [New in Electra:EIP7251] ``` 1. MODIFIED `PROCESS_EXECUTION_PAYLOAD`[¶](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/#modified-process_execution_payload) *Note*: The function `process_execution_payload` is modified to use the new `ExecutionPayloadHeader` type. ```python= def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None: payload = body.execution_payload # Verify consistency of the parent hash with respect to the previous execution payload header assert payload.parent_hash == state.latest_execution_payload_header.block_hash # Verify prev_randao assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) # Verify timestamp assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) # Verify commitments are under limit assert len(body.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK # Verify the execution payload is valid versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] assert execution_engine.verify_and_notify_new_payload( NewPayloadRequest( execution_payload=payload, versioned_hashes=versioned_hashes, parent_beacon_block_root=state.latest_block_header.parent_root, ) ) # Cache execution payload header state.latest_execution_payload_header = ExecutionPayloadHeader( parent_hash=payload.parent_hash, fee_recipient=payload.fee_recipient, state_root=payload.state_root, receipts_root=payload.receipts_root, logs_bloom=payload.logs_bloom, prev_randao=payload.prev_randao, block_number=payload.block_number, gas_limit=payload.gas_limit, gas_used=payload.gas_used, timestamp=payload.timestamp, extra_data=payload.extra_data, base_fee_per_gas=payload.base_fee_per_gas, block_hash=payload.block_hash, transactions_root=hash_tree_root(payload.transactions), withdrawals_root=hash_tree_root(payload.withdrawals), blob_gas_used=payload.blob_gas_used, excess_blob_gas=payload.excess_blob_gas, deposit_requests_root=hash_tree_root(payload.deposit_requests), # [New in Electra:EIP6110] withdrawal_requests_root=hash_tree_root(payload.withdrawal_requests), # [New in Electra:EIP7002:EIP7251] consolidation_requests_root=hash_tree_root(payload.consolidation_requests), # [New in Electra:EIP7251] ) ``` Impact of modifying process_epoch --------------------------------- 1. Updated `process_epoch()` ```python= def process_epoch(state: BeaconState) -> None: process_justification_and_finalization(state) process_inactivity_updates(state) process_rewards_and_penalties(state) process_registry_updates(state) # [Modified in Electra:EIP7251] process_slashings(state) process_eth1_data_reset(state) process_pending_balance_deposits(state) # [New in Electra:EIP7251] process_pending_consolidations(state) # [New in Electra:EIP7251] process_effective_balance_updates(state) # [Modified in Electra:EIP7251] process_slashings_reset(state) process_randao_mixes_reset(state) process_historical_summaries_update(state) process_participation_flag_updates(state) process_sync_committee_updates(state) ``` 1. MODIFIED `PROCESS_OPERATIONS`[¶](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/#modified-process_operations), *Note*: The function `process_operations` is modified to support all of the new functionality in Electra. ```python= def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: # [Modified in Electra:EIP6110] # Disable former deposit mechanism once all prior deposits are processed eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_requests_start_index) if state.eth1_deposit_index < eth1_deposit_index_limit: assert len(body.deposits) == min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index) else: assert len(body.deposits) == 0 def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: for operation in operations: fn(state, operation) for_ops(body.proposer_slashings, process_proposer_slashing) for_ops(body.attester_slashings, process_attester_slashing) for_ops(body.attestations, process_attestation) # [Modified in Electra:EIP7549] for_ops(body.deposits, process_deposit) # [Modified in Electra:EIP7251] for_ops(body.voluntary_exits, process_voluntary_exit) # [Modified in Electra:EIP7251] for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) for_ops(body.execution_payload.deposit_requests, process_deposit_request) # [New in Electra:EIP6110] # [New in Electra:EIP7002:EIP7251] for_ops(body.execution_payload.withdrawal_requests, process_withdrawal_request) # [New in Electra:EIP7251] for_ops(body.execution_payload.consolidation_requests, process_consolidation_request) ``` Impact of modifying BeaconState ------------------------------- ```python= class BeaconState(Container): # Versioning genesis_time: uint64 genesis_validators_root: Root slot: Slot fork: Fork # History latest_block_header: BeaconBlockHeader block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Eth1 eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] # Randomness randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] # Slashings slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances # Participation previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # Finality justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch previous_justified_checkpoint: Checkpoint current_justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint # Inactivity inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] # Sync current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee # Execution latest_execution_payload_header: ExecutionPayloadHeader # [Modified in Electra:EIP6110:EIP7002] # Withdrawals next_withdrawal_index: WithdrawalIndex next_withdrawal_validator_index: ValidatorIndex # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] deposit_requests_start_index: uint64 # [New in Electra:EIP6110] deposit_balance_to_consume: Gwei # [New in Electra:EIP7251] exit_balance_to_consume: Gwei # [New in Electra:EIP7251] earliest_exit_epoch: Epoch # [New in Electra:EIP7251] consolidation_balance_to_consume: Gwei # [New in Electra:EIP7251] earliest_consolidation_epoch: Epoch # [New in Electra:EIP7251] pending_balance_deposits: List[PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT] # [New in Electra:EIP7251] # [New in Electra:EIP7251] pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] # [New in Electra:EIP7251] ``` Impact of Testing ----------------- 1. *Note*: The function `initialize_beacon_state_from_eth1` is modified for pure Electra testing only. Modifications include: 2. Use `ELECTRA_FORK_VERSION` as the previous and current fork version. 3. Utilize the Electra `BeaconBlockBody` when constructing the initial `latest_block_header`. 4. *[New in Electra:EIP6110]* Add `deposit_requests_start_index` variable to the genesis state initialization. 5. *[New in Electra:EIP7251]* Initialize new fields to support increasing the maximum effective balance. ```python= def initialize_beacon_state_from_eth1(eth1_block_hash: Hash32, eth1_timestamp: uint64, deposits: Sequence[Deposit], execution_payload_header: ExecutionPayloadHeader=ExecutionPayloadHeader() ) -> BeaconState: fork = Fork( previous_version=ELECTRA_FORK_VERSION, # [Modified in Electra:EIP6110] for testing only current_version=ELECTRA_FORK_VERSION, # [Modified in Electra:EIP6110] epoch=GENESIS_EPOCH, ) state = BeaconState( genesis_time=eth1_timestamp + GENESIS_DELAY, fork=fork, eth1_data=Eth1Data(block_hash=eth1_block_hash, deposit_count=uint64(len(deposits))), latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), randao_mixes=[eth1_block_hash] * EPOCHS_PER_HISTORICAL_VECTOR, # Seed RANDAO with Eth1 entropy deposit_requests_start_index=UNSET_DEPOSIT_REQUESTS_START_INDEX, # [New in Electra:EIP6110] ) # Process deposits leaves = list(map(lambda deposit: deposit.data, deposits)) for index, deposit in enumerate(deposits): deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[:index + 1]) state.eth1_data.deposit_root = hash_tree_root(deposit_data_list) process_deposit(state, deposit) # Process deposit balance updates for deposit in state.pending_balance_deposits: increase_balance(state, deposit.index, deposit.amount) state.pending_balance_deposits = [] # Process activations for index, validator in enumerate(state.validators): balance = state.balances[index] # [Modified in Electra:EIP7251] validator.effective_balance = min( balance - balance % EFFECTIVE_BALANCE_INCREMENT, get_validator_max_effective_balance(validator)) if validator.effective_balance >= MIN_ACTIVATION_BALANCE: validator.activation_eligibility_epoch = GENESIS_EPOCH validator.activation_epoch = GENESIS_EPOCH # Set genesis validators root for domain separation and chain versioning state.genesis_validators_root = hash_tree_root(state.validators) # Fill in sync committees # Note: A duplicate committee is assigned for the current and next committee at genesis state.current_sync_committee = get_next_sync_committee(state) state.next_sync_committee = get_next_sync_committee(state) # Initialize the execution payload header state.latest_execution_payload_header = execution_payload_header return state ``` Summary of TODO: ================ 1. Feature 1, 2, 3, 4 are fully implemented in the Electra Spec. 2. Feature 5 is partially implemented, except set custom ceiling for partially withdrawal. 3. Feature 6 is proposed, and needs to be implemented into the spec. 4. Cross check test cases in Execution Layer and Consensus Layer.