Part 3: Annotated Specification
Helper Functions
Predicates
is_active_validator
def is_active_validator(validator: Validator, epoch: Epoch) -> bool:
"""
Check if ``validator`` is active.
"""
return validator.activation_epoch <= epoch < validator.exit_epoch
Validators don't explicitly track their own state (eligible for activation, active, exited, withdrawable - the sole exception being whether they have been slashed or not). Instead, a validator's state is calculated by looking at the fields in the Validator
record that store the epoch numbers of state transitions.
In this case, if the validator was activated in the past and has not yet exited, then it is active.
This is used a few times in the spec, most notably in get_active_validator_indices()
which returns a list of all active validators at an epoch.
Used by | get_active_validator_indices() , get_eligible_validator_indices() , process_registry_updates() , process_voluntary_exit() |
See also | Validator |
is_eligible_for_activation_queue
def is_eligible_for_activation_queue(validator: Validator) -> bool:
"""
Check if ``validator`` is eligible to be placed into the activation queue.
"""
return (
validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
and validator.effective_balance == MAX_EFFECTIVE_BALANCE
)
When a deposit is processed with a previously unseen public key, a new Validator
record is created with all the state-transition fields set to the default value of FAR_FUTURE_EPOCH
.
It is possible to deposit any amount over MIN_DEPOSIT_AMOUNT
(currently 1 Ether) into the deposit contract. However, validators do not become eligible for activation until their effective balance is equal to MAX_EFFECTIVE_BALANCE
, which corresponds to an actual balance of 32 Ether or more.
This predicate is used during epoch processing to find validators that have acquired the minimum necessary balance, but have not yet been added to the queue for activation. These validators are then marked as eligible for activation by setting the validator.activation_eligibility_epoch
to the next epoch.
Used by | process_registry_updates() |
See also | Validator , FAR_FUTURE_EPOCH , MAX_EFFECTIVE_BALANCE |
is_eligible_for_activation
def is_eligible_for_activation(state: BeaconState, validator: Validator) -> bool:
"""
Check if ``validator`` is eligible for activation.
"""
return (
# Placement in queue is finalized
validator.activation_eligibility_epoch <= state.finalized_checkpoint.epoch
# Has not yet been activated
and validator.activation_epoch == FAR_FUTURE_EPOCH
)
A validator that is_eligible_for_activation()
has had its activation_eligibility_epoch
set, but its activation_epoch
is not yet set.
To avoid any ambiguity or confusion on the validator side about its state, we wait until its eligibility activation epoch has been finalised before adding it to the activation queue by setting its activation_epoch
. Otherwise, it might at one point become active, and then the beacon chain could flip to a fork in which it is not active. This could happen if the latter fork had fewer blocks and had thus processed fewer deposits.
Used by | process_registry_updates() |
See also | Validator , FAR_FUTURE_EPOCH |
is_slashable_validator
def is_slashable_validator(validator: Validator, epoch: Epoch) -> bool:
"""
Check if ``validator`` is slashable.
"""
return (not validator.slashed) and (validator.activation_epoch <= epoch < validator.withdrawable_epoch)
Validators can be slashed only once: the flag validator.slashed
is set when the first correct slashing report for the validator is processed.
An unslashed validator remains eligible to be slashed from when it becomes active right up until it becomes withdrawable. This is MIN_VALIDATOR_WITHDRAWABILITY_DELAY
epochs (around 27 hours) after it has exited from being a validator and ceased validation duties.
Used by | process_proposer_slashing() , process_attester_slashing() |
See also | Validator |
is_slashable_attestation_data
def is_slashable_attestation_data(data_1: AttestationData, data_2: AttestationData) -> bool:
"""
Check if ``data_1`` and ``data_2`` are slashable according to Casper FFG rules.
"""
return (
# Double vote
(data_1 != data_2 and data_1.target.epoch == data_2.target.epoch) or
# Surround vote
(data_1.source.epoch < data_2.source.epoch and data_2.target.epoch < data_1.target.epoch)
)
This predicate is used by process_attester_slashing()
to check that the two sets of alleged conflicting attestation data in an AttesterSlashing
do in fact qualify as slashable.
There are two ways for validators to get slashed under Casper FFG:
- A double vote: voting more than once for the same target epoch, or
- A surround vote: the source–target interval of one attestation entirely contains the source–target interval of a second attestation from the same validator or validators. The reporting block proposer needs to take care to order the
IndexedAttestation
s within theAttesterSlashing
object so that the first set of votes surrounds the second. (The opposite ordering also describes a slashable offence, but is not checked for here.)
Used by | process_attester_slashing() |
See also | AttestationData , AttesterSlashing |
is_valid_indexed_attestation
def is_valid_indexed_attestation(state: BeaconState, indexed_attestation: IndexedAttestation) -> bool:
"""
Check if ``indexed_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature.
"""
# Verify indices are sorted and unique
indices = indexed_attestation.attesting_indices
if len(indices) == 0 or not indices == sorted(set(indices)):
return False
# Verify aggregate signature
pubkeys = [state.validators[i].pubkey for i in indices]
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch)
signing_root = compute_signing_root(indexed_attestation.data, domain)
return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature)
is_valid_indexed_attestation()
is used in attestation processing and attester slashing.
IndexedAttestations differ from Attestations in that the latter record the contributing validators in a bitlist and the former explicitly list the global indices of the contributing validators.
An IndexedAttestation passes this validity test only if all the following apply.
- There is at least one validator index present.
- The list of validators contains no duplicates (the Python
set
function performs deduplication). - The indices of the validators are sorted. (It is not clear to me why this is required. It's used in the duplicate check here, but that could just be replaced by checking the set size.)
- Its aggregated signature verifies against the aggregated public keys of the listed validators.
Verifying the signature uses the magic of aggregated BLS signatures. The indexed attestation contains a BLS signature that is supposed to be the combined individual signatures of each of the validators listed in the attestation. This is verified by passing it to bls.FastAggregateVerify()
along with the list of public keys from the same validators. The verification succeeds only if exactly the same set of validators signed the message (signing_root
) as appear in the list of public keys. Note that get_domain()
mixes in the fork version, so that attestations are not valid across forks.
No check is done here that the attesting_indices
(which are the global validator indices) are all members of the correct committee for this attestation. In process_attestation()
they must be, by construction. In process_attester_slashing()
it doesn't matter: any validator signing conflicting attestations is liable to be slashed.
Used by | process_attester_slashing() , process_attestation() |
Uses | get_domain() , compute_signing_root() , bls.FastAggregateVerify() |
See also | IndexedAttestation, Attestation |
is_valid_merkle_branch
def is_valid_merkle_branch(leaf: Bytes32, branch: Sequence[Bytes32], depth: uint64, index: uint64, root: Root) -> bool:
"""
Check if ``leaf`` at ``index`` verifies against the Merkle ``root`` and ``branch``.
"""
value = leaf
for i in range(depth):
if index // (2**i) % 2:
value = hash(branch[i] + value)
else:
value = hash(value + branch[i])
return value == root
This is the classic algorithm for verifying a Merkle branch (also called a Merkle proof). Nodes are iteratively hashed as the tree is traversed from leaves to root. The bits of index
select whether we are the right or left child of our parent at each level. The result should match the given root
of the tree.
In this way we prove that we know that leaf
is the value at position index
in the list of leaves, and that we know the whole structure of the rest of the tree, as summarised in branch
.
We use this function in process_deposit()
to check whether the deposit data we've received is correct or not. Based on the deposit data they have seen, Eth2 clients build a replica of the Merkle tree of deposits in the deposit contract. The proposer of the block that includes the deposit constructs the Merkle proof using its view of the deposit contract, and all other nodes use is_valid_merkle_branch()
to check that their view matches the proposer's. If any deposit fails Merkle branch verification then the entire block is invalid.
Used by | process_deposit() |
is_merge_transition_complete
def is_merge_transition_complete(state: BeaconState) -> bool:
return state.latest_execution_payload_header != ExecutionPayloadHeader()
A simple test for whether the given beacon state is pre- or post-Merge. If the latest_execution_payload_header
in the state is the default ExecutionPayloadHeader
then the chain is pre-Merge, otherwise it is post-Merge. Upgrades normally occur at a predetermined block height (or epoch number on the beacon chain), and that's the usual way to test for them. The block height of the Merge, however, was unknown ahead of time, so a different kind of test was required.
Although the mainnet beacon chain is decidedly post-Merge now, this remains useful for syncing nodes from pre-Merge starting points.
This function was added in the Bellatrix pre-Merge upgrade.
Used by | process_execution_payload() , is_merge_transition_block() , is_execution_enabled() |
See also | ExecutionPayloadHeader |
is_merge_transition_block
def is_merge_transition_block(state: BeaconState, body: BeaconBlockBody) -> bool:
return not is_merge_transition_complete(state) and body.execution_payload != ExecutionPayload()
If the Merge transition is not complete (meaning that the beacon state still has the default execution payload header in it), yet our block has a non-default execution payload, then this must be the first block we've seen with an execution payload. It is therefore the Merge transition block.
This function was added in the Bellatrix pre-Merge upgrade.
Uses | is_merge_transition_complete() |
Used by | is_execution_enabled() , on_block() (Bellatrix version) |
See also | ExecutionPayload |
is_execution_enabled
def is_execution_enabled(state: BeaconState, body: BeaconBlockBody) -> bool:
return is_merge_transition_block(state, body) or is_merge_transition_complete(state)
If the block that we have is the first block with an execution payload (the Merge transition block), or we know from the state that we have previously seen a block with an execution payload then execution is enabled, the execution and consensus chains have Merged.
This function was added in the Bellatrix pre-Merge upgrade.
Uses | is_merge_transition_block() , is_merge_transition_complete() |
Used by | process_block() |
has_eth1_withdrawal_credential
def has_eth1_withdrawal_credential(validator: Validator) -> bool:
"""
Check if ``validator`` has an 0x01 prefixed "eth1" withdrawal credential.
"""
return validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX
Only validators that have Eth1 withdrawal credentials are eligible for balance withdrawals of any sort.
is_fully_withdrawable_validator
def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool:
"""
Check if ``validator`` is fully withdrawable.
"""
return (
has_eth1_withdrawal_credential(validator)
and validator.withdrawable_epoch <= epoch
and balance > 0
)
A validator is fully withdrawable only when (a) it has an Eth1 withdrawal credential to make the withdrawal to, (b) it has become withdrawable, meaning that its exit has been processed and it has passed through its MIN_VALIDATOR_WITHDRAWABILITY_DELAY
period, and (c) it has a nonzero balance.
is_partially_withdrawable_validator
def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool:
"""
Check if ``validator`` is partially withdrawable.
"""
has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE
has_excess_balance = balance > MAX_EFFECTIVE_BALANCE
return has_eth1_withdrawal_credential(validator) and has_max_effective_balance and has_excess_balance
A partial withdrawal is the withdrawal of excess Ether from an active (non-exited) validator.
A validator has excess Ether only when (a) it's effective balance is at MAX_EFFECTIVE_BALANCE
, (b) its actual balance is greater than MAX_EFFECTIVE_BALANCE, and (c) it has an Eth1 withdrawal credential to make the withdrawal to.
The first of these conditions is related to the hysteresis in the effective balance. If a validator has previously suffered a drop in its balance, it's effective balance might be 31 Ether even while its actual balance is greater than 32 Ether. If we were to start skimming withdrawals in this situation, the validator's balance would never reach the 32.25 Ether necessary to bring its effective balance up to 32 Ether, and it would be forever stuck at 31 ETH. Therefore, only validators with the full effective balance are eligible for the excess to be withdrawn.
Used by | get_expected_withdrawals() |
Uses | has_eth1_withdrawal_credential() |
See also | MAX_EFFECTIVE_BALANCE , hysteresis |