# Beacon chain accounting (under current Ethereum protocol) ## 1. Validator balances >The beacon chain maintains two separate records of each validator's balance: its actual balance and its effective balance - *Ben Edgington's "Upgrading Ethereum" on [Balances](https://eth2book.info/capella/part2/incentives/balances/#introduction)* ### 1.1. Actual balance A validator's actual balance $= deposits + rewards - penalties - withdrawals$ #### 1.1.1 Beacon chain records Actual balance is stored in the `BeaconState` records, as a registry of validators and their balances: ```python # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] ``` #### 1.1.2 Units Actual balance is fine-grained; Units: Gwei ($10^-9$ ETH) #### 1.1.3 Update period - active validators: min. once / epoch - sync committee participants: every slot ### 1.2. Effective balance A validator's effective balance is derived from its actual balance. #### 1.2.1 Beacon chain records Effective balance is stored in the `Validator`'s records: ```python class Validator(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals effective_balance: Gwei # Balance at stake slashed: boolean [...] ``` #### 1.2.2 Units Effective balance is much more granular than actual balance; Units: `EFFECTIVE_BALANCE_INCREMENT` ( $2^0 * 10^9$ Gwei = 1 ETH) #### 1.2.3 Update period Effective balance is changed according to a process with hysteresis to avoid situations where it might change frequently. ##### Hysteresis Effective balances are guaranteed to update much less frequently than actual balance, by using **hysteresis** in their calculation, so that there's a lag in the reaction time of the output (effective balance), based on the variation of the input (actual balance) The hysteresis levels are controlled by the hysteresis parameters in the [consensus-specs](https://github.com/ethereum/consensus-specs/blob/85e2452301382a9d099df16f38deac8178355358/specs/phase0/beacon-chain.md#misc-1): | Name | Value | | - | - | | `HYSTERESIS_QUOTIENT` | `uint64(4)` | | `HYSTERESIS_DOWNWARD_MULTIPLIER` | `uint64(1)` | | `HYSTERESIS_UPWARD_MULTIPLIER` | `uint64(5)` | The parameters are used at the end of every epoch at effective balance updates, when the effective balance of all validators present in the `BeaconState`'s registry (either active or not) is updated by the following rules: - If actual balance is less than effective balance minus `HYSTERESIS_DOWNWARD_MULTIPLIER` / `HYSTERESIS_QUOTIENT` (0.25), then reduce effective balance by one `EFFECTIVE_BALANCE_INCREMENT` - If actual balance is more than effective balance plus `HYSTERESIS_UPWARD_MULTIPLIER` / `HYSTERESIS_QUOTIENT` (1.25), then increase effective balance by one `EFFECTIVE_BALANCE_INCREMENT` The actual code in the specs (Electra fork): ```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] max_effective_balance = get_max_effective_balance(validator) if ( balance + DOWNWARD_THRESHOLD < validator.effective_balance or validator.effective_balance + UPWARD_THRESHOLD < balance ): validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, max_effective_balance) ``` ### 1.3. Technical and economic reasons behind using the effective balance #### 1.3.1 Economic considerations ##### A validator's weight A validator's effective balance represents its weight in the consensus protocol, the following consensus-related quantities being proportional to its effective balance: * the *probability* of being selected as the beacon block proposer; * the validator's *weight* in the LMD-GHOST fork choice rule; * the validator's *weight* in the justification and finalisation calculations; and * the *probability* of being included in a sync committee. #### Rewards, penalties, and punishments The following rewards, penalties, and punishments are also weighted by effective balance: * The *base reward* for a validator, in terms of which the attestation rewards and penalties are calculated * The *inactivity penalties* applied to a validator as a consequence of an inactivity leak, and * The *initial slashing penalty* and *the correlated slashing penalty* ##### Exceptions * block proposer reward * sync committee rewards Reason : a validator's probability of being selected to propose is proportional to its effective balance, so there's no need to scale the reward itself with the effective balance. #### 1.3.2 Technical considerations ##### Performance A Validator's effective balance updates much more rarely than actual balance, because it is granular and has hysteresis applied to any updates. This allows validator records inside the beacon state, a large data structure, to be cached and Merkelized less often, improving performance when generating the hash tree root of the entire state. If we would keep the more frequently changing actual balances in the validator's records, the whole data structure would need to be rehashed at least once per epoch. ##### Base reward per increment All rewards are calculated in terms of a "base reward per increment": ```python def get_base_reward_per_increment(state: BeaconState) -> Gwei: return Gwei(EFFECTIVE_BALANCE_INCREMENT * BASE_REWARD_FACTOR // integer_squareroot(get_total_active_balance(state))) ``` Because effective balances are updated only once per epoch, we only need to calculate the base reward per increment once, then cache the result for the whole epoch, irrespective of any changes in actual balances. ## 2. Withdrawals ### `get_expected_withdrawals` ```python def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal], uint64]: epoch = get_current_epoch(state) withdrawal_index = state.next_withdrawal_index validator_index = state.next_withdrawal_validator_index withdrawals: List[Withdrawal] = [] # [New in Electra:EIP7251] Consume pending partial withdrawals for withdrawal in state.pending_partial_withdrawals: if withdrawal.withdrawable_epoch > epoch or len(withdrawals) == MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: break validator = state.validators[withdrawal.index] has_sufficient_effective_balance = validator.effective_balance >= MIN_ACTIVATION_BALANCE has_excess_balance = state.balances[withdrawal.index] > MIN_ACTIVATION_BALANCE 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( index=withdrawal_index, validator_index=withdrawal.index, address=ExecutionAddress(validator.withdrawal_credentials[12:]), 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( index=withdrawal_index, validator_index=validator_index, address=ExecutionAddress(validator.withdrawal_credentials[12:]), amount=balance, )) withdrawal_index += WithdrawalIndex(1) elif is_partially_withdrawable_validator(validator, balance): withdrawals.append(Withdrawal( index=withdrawal_index, validator_index=validator_index, address=ExecutionAddress(validator.withdrawal_credentials[12:]), amount=balance - get_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 ``` ### `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 payload.withdrawals == expected_withdrawals for withdrawal in expected_withdrawals: 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 ```