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.
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 |
get_awake_validator_indices
and helpersget_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).
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.
MAX_VALIDATOR_COUNT
, validator returns should start decreasing proportionately to 1/total_deposits
and not 1/sqrt(total_deposits)
. But the functions to compute total_deposits -> validator_return_rate
and total_deposits -> max_total_issuance
remain continuous.