Try   HackMD

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

1.1. Actual balance

A validator's actual balance

=deposits+rewardspenaltieswithdrawals

1.1.1 Beacon chain records

Actual balance is stored in the BeaconState records, as a registry of validators and their balances:

# Registry
    validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
    balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]

1.1.2 Units

Actual balance is fine-grained;
Units: Gwei (

109 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:

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 (

20109 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:

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):

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":

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

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

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