Part 3: Annotated Specification
Beacon State Mutators
def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: """ Increase the validator balance at index ``index`` by ``delta``. """ state.balances[index] += delta
After creating a validator with its deposit balance, this and
decrease_balance() are the only places in the spec where validator balances are ever modified.
We need two separate functions to change validator balances, one to increase them and one to decrease them, since we are using only unsigned integers.
Fun fact: A typo around this led to Teku's one and only consensus failure at the initial client interop event. Unsigned integers induce bugs!
def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: """ Decrease the validator balance at index ``index`` by ``delta``, with underflow protection. """ state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta
The counterpart to
increase_balance(). This has a little extra work to do to check for unsigned int underflow since balances may not go negative.
def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: """ Initiate the exit of the validator with index ``index``. """ # Return if validator already initiated exit validator = state.validators[index] if validator.exit_epoch != FAR_FUTURE_EPOCH: return # Compute exit queue epoch exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) if exit_queue_churn >= get_validator_churn_limit(state): exit_queue_epoch += Epoch(1) # Set validator exit epoch and withdrawable epoch validator.exit_epoch = exit_queue_epoch validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
Exits may be initiated voluntarily, as a result of being slashed, or by dropping to the
In all cases, a dynamic "churn limit" caps the number of validators that may exit per epoch. This is calculated by
get_validator_churn_limit(). The mechanism for enforcing this is the exit queue: the validator's
exit_epoch is set such that it is at the end of the queue.
The exit queue is not maintained as a separate data structure, but is continually re-calculated from the exit epochs of all validators and allowing for a fixed number to exit per epoch. I expect there are some optimisations to be had around this in actual implementations.
An exiting validator is expected to continue with its proposing and attesting duties until its
exit_epoch has passed, and will continue to receive rewards and penalties accordingly.
In addition, an exited validator remains eligible to be slashed until its
withdrawable_epoch, which is set to
MIN_VALIDATOR_WITHDRAWABILITY_DELAY epochs after its
exit_epoch. This is to allow some extra time for any slashable offences by the validator to be detected and reported.
|See also||Voluntary Exits,
def slash_validator(state: BeaconState, slashed_index: ValidatorIndex, whistleblower_index: ValidatorIndex=None) -> None: """ Slash the validator with index ``slashed_index``. """ epoch = get_current_epoch(state) initiate_validator_exit(state, slashed_index) validator = state.validators[slashed_index] validator.slashed = True validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance slashing_penalty = validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX decrease_balance(state, slashed_index, slashing_penalty) # Apply proposer and whistleblower rewards proposer_index = get_beacon_proposer_index(state) if whistleblower_index is None: whistleblower_index = proposer_index whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) proposer_reward = Gwei(whistleblower_reward * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR) increase_balance(state, proposer_index, proposer_reward) increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward))
Both proposer slashings and attester slashings end up here when a report of a slashable offence has been verified during block processing.
When a validator is slashed, several things happen immediately:
- The validator is processed for exit via
initiate_validator_exit(), so it joins the exit queue.
- The validator is marked as slashed. This information is used when calculating rewards and penalties: while being exited, whatever it does, a slashed validator receives penalties as if it had failed to propose or attest, including the inactivity leak if applicable.
- Normally, as part of the exit process, the
withdrawable_epochfor a validator (the point at which a validator's stake is in principle unlocked) is set to
MIN_VALIDATOR_WITHDRAWABILITY_DELAYepochs after it exits. When a validator is slashed, a much longer period of lock-up applies, namely
EPOCHS_PER_SLASHINGS_VECTOR. This is to allow a further, potentially much greater, slashing penalty to be applied later once the chain knows how many validators have been slashed together around the same time. The postponement of the withdrawable epoch is twice as long as required to apply the extra penalty, which is applied half-way through the period. This simply means that slashed validators continue to accrue attestation penalties for some 18 days longer than necessary. Treating slashed validators fairly is not a big priority for the protocol.
- The effective balance of the validator is added to the accumulated effective balances of validators slashed this epoch, and stored in the circular list,
state.slashings. This will later be used by the slashing penalty calculation mentioned in the previous point.
- An initial "slap on the wrist" slashing penalty of the validator's effective balance (in Gwei) divided by the
MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIXis applied. For a validator with a full Effective Balance of 32 ETH, this initial penalty is 1 ETH.
- The block proposer that included the slashing proof receives a reward.
In short, a slashed validator receives an initial minor penalty, can expect to receive a further penalty later, and is marked for exit.
Note that the
whistleblower_index defaults to
None in the parameter list. This is never used in Phase 0, with the result that the proposer that included the slashing gets the entire whistleblower reward; there is no separate whistleblower reward for the finder of proposer or attester slashings. One reason is simply that reports are too easy to steal: if I report a slashable event to a block proposer, there is nothing to prevent that proposer claiming the report as its own. We could introduce some fancy ZK protocol to make this trustless, but this is what we're going with for now. Later developments, such as the proof-of-custody game, may reward whistleblowers directly.