owned this note
owned this note
Published
Linked with GitHub
# 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
```