Part 3: Annotated Specification
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.
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
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.
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.
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.
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
IndexedAttestations within the
AttesterSlashingobject so that the first set of votes surrounds the second. (The opposite ordering also describes a slashable offence, but is not checked for here.)
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
setfunction 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.
|See also||IndexedAttestation, Attestation|
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
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.
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.
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.
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.
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.
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.
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.