Defragment: BeaconState and Validator Balances for Ethereum Consensus Layer (e.g. EIP-7521) ====== TL;DR: ------ 1. The Beacon Chain has multiple variables related to validators balance, e.g. `validator.active_balance`, `validator.effective_balance`, `validator.excess_balance`, etc. 2. [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) implementation needs to touch on multiple place on balance and state. [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/). 3. This article aims to give a systematic review on how balance and state works BeaconChain in [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/), and prepare for [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) implementation. 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) Architecture diagram to summary all kinds of balance and state in Beacon Chain and how they work. ================================================================================================= ![whiteboard_exported_image (47)](https://hackmd.io/_uploads/r1blpNHYR.png) Validator's Active Balance (`validator.active_balance`) ======================================================= 1. Active balance or `validator.active_balance` is a container variable defined in Beacon State since [Phase0](https://ethereum.github.io/consensus-specs/specs/phase0/beacon-chain/). 1. `validator.active_balance` is the sum of any deposits made for a validator via [the deposit contract](https://eth2book.info/capella/part2/deposits-withdrawals/contract/), plus accrued beacon chain rewards, minus accrued penalties and withdrawals. It is in Gwei. 2. `validator.active_balance` can be directly obtained by using `stake.balances[validator_index]` ````python= # new in Phase0 class BeaconState(Container): ... ... # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] # validator balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] # active balance ... ... ```` 1. `validator.active_balance` is a validator's true and actual balance. It connects to another variable which is specific in the Validator container called `validator.effective_balance` which will be discussed later. 1. `validator.active_balance` and `validator.effective_balance` are not always equal. 2. `validator.active_balance` or `stake.balances[validator_index]` is updated in real time for all validators whenever the actual balance is updated, e.g. in each slot for sync committee participants, and at least once per epoch for all validators. 1. However, `validator.effective_balance` is updated only when condition matches, minimually in epoch level. 3. The update of `stake.balances[validator_index]` may trigger the update of `validator.effective_balance`, but not the opposite. 2. Active balance can be safely obtained with helper function `get_active_balance()`, which is newly added in [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/). By calling `get_active_balance()` will return 1. `stake.balances[validator_index]` 2. Or if `has_eth1_withdrawal_credential(validator)` == True: 1. Return `MAX_EFFECTIVE_BALANCE_ELECTRA` 2. Else return `MIN_ACTIVATION_BALANCE` ````python= # new in Electra, ''' # TODO: if to add custom ceiling, MIN_ACTIVATION_BALANCE should be replaced ''' 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) ) # new in Electra 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) # new in Electra def is_compounding_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: return withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX # new in Electra 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) # modifed in Electra 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 # new in Electra 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) ```` 1. Active Balance is Beacon State and can not be directly updated. The only update can be done is to call Beacon state mutators:`increase_balance()` and `decrease_balance()` ````python= # new in Phase0 def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: """ Increase the validator balance at index ``index`` by ``delta``. """ state.balances[index] += delta # new in Phase0 def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: """ Decrease the validator balance at index ``index`` by ``delta``, with underflow protection. """ state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta ```` Validator's Effective Balance (**`validator.effective_balance`**) ================================================================= 1. Effective Balance is a variable defined in Validator Construction State since [Phase0](https://ethereum.github.io/consensus-specs/specs/phase0/beacon-chain/). 1. The effective balance was first introduced to represent the "[maximum balance at risk](https://github.com/ethereum/consensus-specs/pull/162#issuecomment-441759461)" for a validator ````python= # new in Phase0 class Validator(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals effective_balance: Gwei # Balance at stake ... ... # new in Phase0 class BeaconState(Container): ... ... # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] ... ... ```` 1. Effective balance can be directly obtained by using `validator.effective_balance`. 1. Currently, there is no help function like `get_effective_balance()` function available, as it can be retrieved directly by referring `validator.effective_balance`. 2. `validator.effective_balance` has a cap which is defined by `MAX_EFFECTIVE_BALANCE`. 1. A validator's actual balance could be much higher. For example, if a double deposit had been accidentally made, a validator would have an actual balance of `64 ETH` but an effective balance of only `32 ETH`. 3. The update of `validator.effective_balance` depends on `validator.active_balance` updates. 1. `validator.effective_balance` tracks the `validator.active_balance`, but is designed to change much more rarely. This is optimisation. 2. `validator.effective_balance` are [updated](https://eth2book.info/capella/part3/transition/epoch/#def_process_effective_balance_updates) only once per epoch, which means that we need only calculate things like the [base reward per increment](https://eth2book.info/capella/part2/incentives/issuance/#the-base-reward-per-increment) once, then cache the result for the whole epoch, irrespective of any changes in actual balances. 3. The reason `validator.effective_balance` is updated slowly because of `[EFFECTIVE_BALANCE_INCREMENT]`, see [Ben's explain](https://eth2book.info/capella/part3/config/preset/#effective_balance_increment). `[EFFECTIVE_BALANCE_INCREMENT]` controls that Effective Balance can only update in unit which currently is `1 ETH`. - Therefore, Effective Balance can only be changed from `0->1, 1->2, or 31->32`. - But Active Balance can be changed any time from `31.1-.31.2 or 31.11 to 31.12`. 4. This is called [hysteresis](https://eth2book.info/capella/part2/incentives/balances/#hysteresis). 4. `validator.effective_balance` (not `validator.active_balance`) can directly impact a validator's calculation on qualification, reward and penalty. The validator's influence on the protocol is proportional to its effective balance, as are its rewards and penalties. e.g. 1. the probability of being [selected](https://eth2book.info/capella/part3/helper/misc/#def_compute_proposer_index) as the beacon block proposer; 2. the validator's weight in the LMD-GHOST [fork choice rule](https://eth2book.info/capella/part3/forkchoice/phase0/#get_weight); 3. the validator's weight in the justification and finalisation [calculations](https://eth2book.info/capella/part3/transition/epoch/#def_weigh_justification_and_finalization); and 4. the probability of being [included](https://eth2book.info/capella/part3/helper/accessors/#def_get_next_sync_committee_indices) in a sync committee. 5. the [base reward](https://eth2book.info/capella/part3/transition/epoch/#def_get_base_reward) for a validator, in terms of which the attestation rewards and penalties are calculated; 6. the [inactivity penalties](https://eth2book.info/capella/part2/incentives/inactivity/) applied to a validator as a consequence of an inactivity leak; and 7. both the [initial](https://eth2book.info/capella/part2/incentives/slashing/#the-initial-penalty) slashing penalty and the [correlated](https://eth2book.info/capella/part2/incentives/slashing/#the-correlation-penalty) slashing penalty. 5. `validator.effective_balance` can be updated with the help function `process_effective_balance_updates()` ````python= # modified in Elctra 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 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) ```` Ethereum's config: `EFFECTIVE_BALANCE_INCREMENT` ================================================ 1. `EFFECTIVE_BALANCE_INCREMENT` is a constant defined in Beacon Chain since [Phase0](https://ethereum.github.io/consensus-specs/specs/phase0/beacon-chain/). 2. It controls how fast `validator.effective_balance` can be updated when validator.active_balance is updated. 3. `EFFECTIVE_BALANCE_INCREMENT` is currently set to 1 ETH. Therefore 1. `validator.effective_balance` has about `0.25` to `0.75 ETH` difference from `validator.active_balance`. 2. It makes `validator.effective_balance` to updated much less frequently when `validator.active_balance`. 3. `validator.effective_balance` can only be an integer value from `[0, 1, 2, ..., 31, 32] ETH`. Ethereum's config: `MAX_EFFECTIVE_BALANCE` ========================================== 1. `MAX_EFFECTIVE_BALANCE` is a constant defined in Beacon Chain since [Phase0](https://ethereum.github.io/consensus-specs/specs/phase0/beacon-chain/). 2. It sets the cap of a `validator's effective balance` 3. From [Phase0](https://ethereum.github.io/consensus-specs/specs/phase0/beacon-chain/) to Dencon, `MAX_EFFECTIVE_BALANCE` is always `32 ETH`. The fixed value encourages validators set size to grow significantly. 1. [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) is proposed and scope into [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/) upgrade by increasing it into `2048 ETH`. 2. We still need to maintain the validator activation requirement, so we added a new variable called `MIN_ACTIVATION_BALANCE` which requires `32 ETH` in order to activate in [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/). | Name | Value | Comments | | ---- | ---- | ---- | | MAX_EFFECTIVE_BALANCE_ELECTRA | Gwei(2**11 * 10**9) (= 2048,000,000,000) | increased from 32 ETH to 2048 ETH | Ethereum's config: `MIN_ACTIVATION_BALANCE` =========================================== 1. `MIN_ACTIVATION_BALANCE` a constant defined in Beacon Chain since [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/). 1. The current value is `32 ETH` 2. It sets the minimal requirement to activate a validator. | Name | Value | Comments | | ---- | ---- | ---- | | MIN_ACTIVATION_BALANCE | Gwei(2**5 * 10**9) (= 32,000,000,000) | equivalent to MAX_EFFECTIVE_BALANCE = 32 ETH since Phase0 | Validator's Excess Balance (`validator.excess_balance`) ======================================================= 1. `validator.excess_balance` is not a formal variable in Beacon Chain spec, but an intermediate variable for partial withdrawal to know validator's balance exceeds a threadhold. 1. So `validator.excess_balance` can be automatically swept from `validator.active_balance` into `validator.withdraw_address`. 2. Partial withdrawals sweep makes up the majority of withdrawals processed. 2. `validator.excess_balance` first introduced in [Capella](https://ethereum.github.io/consensus-specs/specs/capella/beacon-chain/), and then upgraded to [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/). The main difference is on the way to determine the value of threadhold. 1. In Capella, the exceed threadhold is `MAX_EFFECTIVE_BALANCE` ````python= # in is_partially_withdrawable_validator() balance = state.balances[validator_index] has_excess_balance = balance > MAX_EFFECTIVE_BALANCE ```` 3. In [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/), the exceed threadhold is `MIN_ACTIVATION_BALANCE` ````python= # in is_partially_withdrawable_validator() function balance = state.balances[validator_index] max_effective_balance = get_validator_max_effective_balance(validator) has_excess_balance = state.balances[withdrawal.index] > MIN_ACTIVATION_BALANCE # in process_withdrawal_request() function def get_pending_balance_to_withdraw(state: BeaconState, validator_index: ValidatorIndex) -> Gwei: return sum( withdrawal.amount for withdrawal in state.pending_partial_withdrawals if withdrawal.index == validator_index ) pending_balance_to_withdraw = get_pending_balance_to_withdraw(state, index) has_excess_balance = state.balances[index] > MIN_ACTIVATION_BALANCE + pending_balance_to_withdraw ```` 3. However, [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) is proposing to allow each validator to set a custom ceiling. The ceiling is a value between `MIN_ACTIVATION_BALANCE` and `MAX_EFFECTIVE_BALANCE`. 1. TODO, add custom ceiling as suggested in EIP-7251 Ethereum's config: `EJECTION_BALANCE` ===================================== 1. `EJECTION_BALANCE` was first introduced into Beacon Chain spec [Phase0](https://ethereum.github.io/consensus-specs/specs/phase0/beacon-chain/). 2. `EJECTION_BALANCE` is used to automatically trigger a validator exit when the balance is reduced. 1. This is most likely to happen as a result of the ["inactivity leak"](https://eth2book.info/capella/part3/config/preset/#inactivity_penalty_quotient), which gradually reduces the balance of inactive validators in order to maintain the liveness of the beacon chain. 2. This mechanism is intended to protect stakers who no longer have access to their keys. If a validator has been offline for long enough to lose half of its balance, it is unlikely to be coming back. To save the staker from losing everything, we choose to eject the validator before its balance reaches zero. 3. Note that the dependence on `validator.active_balance` means that the validator is queued for ejection as soon as its actual balance falls to `16.75 Ether`. | Name | Value | Comments | | ---- | ---- | ---- | | EJECTION_BALANCE | Gwei(`2**4 * 10**9`) (= 16,000,000,000) | threadhold to auto exit a validator | 1. In [Electra](https://ethereum.github.io/consensus-specs/specs/electra/beacon-chain/), the exceed threadhold is `MIN_ACTIVATION_BALANCE` ````python= # modified in Electra def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: """ Initiate the exit of the validator with index ``index``. """ # Return if validator already initiated exit validator = state.validators[index] if validator.exit_epoch != FAR_FUTURE_EPOCH: return # Compute exit queue epoch [Modified in Electra:EIP7251] exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) # Set validator exit epoch and withdrawable epoch validator.exit_epoch = exit_queue_epoch validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) # modified in Electra def process_registry_updates(state: BeaconState) -> None: # Process activation eligibility and ejections for index, validator in enumerate(state.validators): if is_eligible_for_activation_queue(validator): validator.activation_eligibility_epoch = get_current_epoch(state) + 1 if ( is_active_validator(validator, get_current_epoch(state)) and validator.effective_balance <= EJECTION_BALANCE ): initiate_validator_exit(state, ValidatorIndex(index)) # Activate all eligible validators activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) for validator in state.validators: if is_eligible_for_activation(state, validator): validator.activation_epoch = activation_epoch ```` TODO: ===== I plan to write another article to explain bellow topics; 1. How does deposit influence balance ? 2. How does withdrawal influence balance ? 3. How does reward influence balance ? 4. How does penalty influence balance ?