-
-
Published
Linked with GitHub
# Simplified Active Validator Cap and Rotation Proposal
The goal of this proposal is to cap the active validator set to some fixed value (eg. $2^{19}$ 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:
```python
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:
```python
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.
```python
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.
```python
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](https://arxiv.org/pdf/1710.09437.pdf)).
### 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
* Once the active validator set size exceeds `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.
* Validators active in epoch N can affect the finalization status of epoch N. At that time, the active validator set in epoch N+8 is unknown. Hence, validators have no action that they can take to manipulate the randomness to keep themselves active.
* Validators _can_ delay finality to keep themselves active. But they cannot increase their profits by doing this, as this would put them into an inactivity leak.