Settings

Simplified Active Validator Cap and Rotation Proposal

The goal of this proposal is to cap the active validator set to some fixed value (eg. 219 validators) while at the same time ensuring (i) an economic finality guarantee where close to 1/3 of the cap must be slashed to finalize two conflicting blocks, (ii) low long-term variance in validator income and (iii) maximum simplicity.

Constants

Constant Value Notes
MAX_VALIDATOR_COUNT 2**19 (= 524,288) ~16.7M ETH
ROTATION_RATE 2**6 (= 64) Up to 1.56% per epoch
FINALIZED_EPOCH_VECTOR_LENGTH 2**16 (= 65,536) 32 eeks ~= 291 days
FINALITY_LOOKBACK 2**3 (= 8) Measured in epochs

Definition of get_awake_validator_indices and helpers

get_awake_validator_indices returns a subset of get_active_validator_indices. The function and helpers required for it are defined as follows.

state.is_epoch_finalized is a new BeaconState member value of type BitList[FINALIZED_EPOCH_VECTOR_LENGTH]. We add to the weigh_justification_and_finalization function the lines:

When state.finalized_checkpoint is updated to new_checkpoint, we update:

current_epoch_position = get_current_epoch(state) % FINALIZED_EPOCH_VECTOR_LENGTH
state.is_epoch_finalized[current_epoch_position] = False
# In all cases where we do `state.finalized_checkpoint = new_checkpoint`
new_finalized_epoch_position = new_checkpoint.epoch % FINALIZED_EPOCH_VECTOR_LENGTH
state.is_epoch_finalized[new_finalized_epoch_position] = True

We now define helpers.

First, the “was this epoch finalized?” helper:

def did_epoch_finalize(state: BeaconState, epoch: Epoch) -> bool:
    assert epoch < get_current_epoch(state)
    assert epoch + FINALIZED_EPOCH_VECTOR_LENGTH > get_current_epoch(state)
    return state.is_epoch_finalized[epoch % FINALIZED_EPOCH_VECTOR_LENGTH]

This next function outputs a set of validators that get slept in a given epoch (the output is nonempty only if the epoch has been finalized). Note that we use the finality bit of epoch N and the active validator set of epoch N+8; this ensures that by the time the active validator set that will be taken offline is known there is no way to affect finality.

def get_slept_validators(state: BeaconState,
                         epoch: Epoch) -> Set[ValidatorIndex]:
    assert get_current_epoch(state) >= epoch + MAX_SEED_LOOKAHEAD * 2
    active_validators = get_active_validators(state, epoch + FINALITY_LOOKBACK)
    if len(active_validators) >= MAX_VALIDATOR_COUNT:
        excess_validators = len(active_validators) - MAX_VALIDATOR_COUNT
    else:
        excess_validators = 0
    if did_epoch_finalize(state, epoch):
        seed = get_seed(state, epoch, DOMAIN_BEACON_ATTESTER)
        validator_count = len(active_validators)
        return set(
            active_validators[compute_shuffled_index(i, validator_count, seed)]
            for i in range(len(excess_validators // ROTATION_RATE))
        )
    else:
        return set()

This next function outputs the currently awake validators. The idea is that a validator is awake if they have not been slept in one of the last ROTATION_RATE finalized epochs.

def get_awake_validator_indices(state: BeaconState,
                                epoch: Epoch) -> Set[ValidatorIndex]:
    o = set()
    finalized_epochs_counted = 0
    search_start = FINALITY_LOOKBACK
    search_end = min(epoch + 1, FINALIZED_EPOCH_VECTOR_LENGTH)
    for step in range(search_start, search_end):
        check_epoch = epoch - step
        if did_epoch_finalize(check_epoch):
            o = o.union(get_slept_validators(state, finalized_epoch))
            finalized_epochs_counted += 1
            if finalized_epochs_counted == ROTATION_RATE:
                break            
    return [v for v in get_active_validator_indices(state, epoch) if v not in o]

The intention is that get_awake_validator_indices contains at most roughly MAX_VALIDATOR_COUNT validators (possibly slightly more at certain times, but it equilibrates toward that limit), and it changes by at most 1/ROTATION_RATE per finalized epoch. The restriction to finalized epochs ensures that two conflicting finalized blocks can only differ by at most an extra 1/ROTATION_RATE as a result of this mechanism (it can also be viewed as an implementation of the dynasty mechanism from the original Casper FFG paper).

Protocol changes

All existing references to get_active_validator_indices are replaced with get_awake_validator_indices. Specifically, only awake indices are shuffled and put into any committee or proposer selection algorithm. Rewards and non-slashing penalties for non-awake active validators should equal 0. Non-aware active validators should still be vulnerable to slashing.

Economic effects