Part 3: Annotated Specification
class Fork(Container): previous_version: Version current_version: Version epoch: Epoch # Epoch of latest fork
Fork data is stored in the BeaconState to indicate the current and previous fork versions. The fork version gets incorporated into the cryptographic domain in order to invalidate messages from validators on other forks. The previous fork version and the epoch of the change are stored so that pre-fork messages can still be validated (at least until the next fork). This ensures continuity of attestations across fork boundaries.
Note that this is all about planned protocol forks (upgrades), and nothing to do with the fork-choice rule, or inadvertent forks due to errors in the state transition.
class ForkData(Container): current_version: Version genesis_validators_root: Root
ForkData is used only in
compute_fork_data_root(). This is used when distinguishing between chains for the purpose of peer-to-peer gossip, and for domain separation. By including both the current fork version and the genesis validators root, we can cleanly distinguish between, say, mainnet and a testnet. Even if they have the same fork history, the genesis validators roots will differ.
Version is the datatype for a fork version number.
class Checkpoint(Container): epoch: Epoch root: Root
Checkpoints are the points of justification and finalisation used by the Casper FFG protocol. Validators use them to create
AttestationData votes, and the status of recent checkpoints is recorded in
As per the Casper paper, checkpoints contain a height, and a block root. In this implementation of Casper FFG, checkpoints occur whenever the slot number is a multiple of
SLOTS_PER_EPOCH, thus they correspond to
epoch numbers. In particular, checkpoint is the first slot of epoch . The genesis block is checkpoint 0, and starts off both justified and finalised.
root element here is the block root of the first block in the
epoch. (This might be the block root of an earlier block if some slots have been skipped, that is, if there are no blocks for those slots.).
It is very common to talk about justifying and finalising epochs. This is not strictly correct: checkpoints are justified and finalised.
Once a checkpoint has been finalised, the slot it points to and all prior slots will never be reverted.
class Validator(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals effective_balance: Gwei # Balance at stake slashed: boolean # Status epochs activation_eligibility_epoch: Epoch # When criteria for activation were met activation_epoch: Epoch exit_epoch: Epoch withdrawable_epoch: Epoch # When validator can withdraw funds
This is the data structure that stores most of the information about an individual validator, with only validators' balances and inactivity scores stored elsewhere.
Validators' actual balances are stored separately in the
BeaconState structure, and only the slowly changing "effective balance" is stored here. This is because actual balances are liable to change quite frequently (at least every epoch, and sometimes more frequently): the Merkleization process used to calculate state roots means that only the parts that change need to be recalculated; the roots of unchanged parts can be cached. Separating out the validator balances potentially means that only 1/15th (8/121) as much data needs to be rehashed every epoch compared to storing them here, which is an important optimisation.
For similar reasons, validators' inactivity scores are stored outside validator records as well, as they are also updated every epoch.
A validator's record is created when its deposit is first processed. Sending multiple deposits does not create multiple validator records: deposits with the same public key are aggregated in one record. Validator records never expire; they are stored permanently, even after the validator has exited the system. Thus, there is a 1:1 mapping between a validator's index in the list and the identity of the validator (validator records are only ever appended to the list).
Also stored in
pubkeyserves as both the unique identity of the validator and the means of cryptographically verifying messages purporting to have been signed by it. The public key is stored raw, unlike in Eth1, where it is hashed to form the account address. This allows public keys to be aggregated for verifying aggregated attestations.
- Depending on its prefix,
withdrawal_credentialsmight specify an Eth1 account to which withdrawal transactions will be made, or it might be an old-style BLS commitment that needs to be updated before withdrawals can occur for that validator. The withdrawal credentials included in a validator's deposit data are not checked in any way by the consensus layer.
effective_balanceis a topic of its own that we've touched upon already, and will discuss more fully when we look at effective balances updates.
slashedindicates that a validator has been slashed, that is, punished for violating the slashing conditions. A validator can be slashed only once.
- The remaining values are the epochs in which the validator changed, or is due to change state.
A detailed explanation of the stages in a validator's lifecycle is here, and we'll be covering it in detail as we work through the beacon chain logic. But, in simplified form, progress is as follows:
- A 32 ETH deposit has been made on the Ethereum 1 chain. No validator record exists yet.
- The deposit is processed by the beacon chain at some slot. A validator record is created with all epoch fields set to
- At the end of the current epoch, the
activation_eligibility_epochis set to the next epoch.
- After the epoch
activation_eligibility_epochhas been finalised, the validator is added to the activation queue by setting its
activation_epochappropriately, taking into account the per-epoch churn limit and
- On reaching
activation_epochthe validator becomes active, and should carry out its duties.
- At any time after
SHARD_COMMITTEE_PERIODepochs have passed, a validator may request a voluntary exit.
exit_epochis set according to the validator's position in the exit queue and
MIN_VALIDATOR_WITHDRAWABILITY_DELAYepochs after that.
exit_epochonward the validator is no longer active. There is no mechanism for exited validators to rejoin: exiting is permanent.
withdrawable_epoch, the validator's full stake can be withdrawn.
The above does not account for slashing or forced exits due to low balance.
class AttestationData(Container): slot: Slot index: CommitteeIndex # LMD GHOST vote beacon_block_root: Root # FFG vote source: Checkpoint target: Checkpoint
The beacon chain relies on a combination of two different consensus mechanisms: LMD GHOST keeps the chain moving, and Casper FFG brings finalisation. These are documented in the Gasper paper. Attestations from (committees of) validators are used to provide votes simultaneously for each of these consensus mechanisms.
This container is the fundamental unit of attestation data. It provides the following elements.
slot: each active validator should be making exactly one attestation per epoch. Validators have an assigned slot for their attestation, and it is recorded here for validation purposes.
index: there can be several committees active in a single slot. This is the number of the committee that the validator belongs to in that slot. It can be used to reconstruct the committee to check that the attesting validator is a member. Ideally, all (or the majority at least) of the attestations in a slot from a single committee will be identical, and can therefore be aggregated into a single aggregate attestation.
beacon_block_rootis the validator's vote on the head block for that slot after locally running the LMD GHOST fork-choice rule. It may be the root of a block from a previous slot if the validator believes that the current slot is empty.
sourceis the validator's opinion of the best currently justified checkpoint for the Casper FFG finalisation process.
targetis the validator's opinion of the block at the start of the current epoch, also for Casper FFG finalisation.
AttestationData structure gets wrapped up into several other similar but distinct structures:
Attestationis the form in which attestations normally make their way around the network. It is signed and aggregatable, and the list of validators making this attestation is compressed into a bitlist.
IndexedAttestationis used primarily for attester slashing. It is signed and aggregated, with the list of attesting validators being an uncompressed list of indices.
PendingAttestation. In Phase 0, after having their validity checked during block processing,
PendingAttestations were stored in the beacon state pending processing at the end of the epoch. This was reworked in the Altair upgrade and
PendingAttestations are no longer used.
class IndexedAttestation(Container): attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE] data: AttestationData signature: BLSSignature
This is one of the forms in which aggregated attestations – combined identical attestations from multiple validators in the same committee – are handled.
IndexedAttestations contain essentially the same information. The difference being that the list of attesting validators is stored uncompressed in
IndexedAttestations. That is, each attesting validator is referenced by its global validator index, and non-attesting validators are not included. To be valid, the validator indices must be unique and sorted, and the signature must be an aggregate signature from exactly the listed set of validators.
class PendingAttestation(Container): aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE] data: AttestationData inclusion_delay: Slot proposer_index: ValidatorIndex
PendingAttestations were removed in the Altair upgrade and now appear only in the process for upgrading the state during the fork. The following is provided for historical reference.
Prior to Altair,
Attestations received in blocks were verified then temporarily stored in beacon state in the form of
PendingAttestations, pending further processing at the end of the epoch.
PendingAttestation is an
Attestation minus the signature, plus a couple of fields related to reward calculation:
inclusion_delayis the number of slots between the attestation having been made and it being included in a beacon block by the block proposer. Validators are rewarded for getting their attestations included in blocks, but the reward used to decline in inverse proportion to the inclusion delay. This incentivised swift attesting and communicating by validators.
proposer_indexis the block proposer that included the attestation. The block proposer gets a micro reward for every validator's attestation it includes, not just for the aggregate attestation as a whole. This incentivises efficient finding and packing of aggregations, since the number of aggregate attestations per block is capped.
Taken together, these rewards are designed to incentivise the whole network to collaborate to do efficient attestation aggregation (proposers want to include only well-aggregated attestations; validators want to get their attestations included, so will ensure that they get well aggregated).
This whole mechanism was replaced in the Altair upgrade by
class Eth1Data(Container): deposit_root: Root deposit_count: uint64 block_hash: Hash32
Proposers include their view of the Ethereum 1 chain in blocks, and this is how they do it. The beacon chain stores these votes up in the beacon state until there is a simple majority consensus, then the winner is committed to beacon state. This is to allow the processing of Eth1 deposits, and creates a simple "honest-majority" one-way bridge from Eth1 to Eth2. The 1/2 majority assumption for this (rather than 2/3 for committees) is considered safe as the number of validators voting each time is large:
SLOTS_PER_EPOCH = 64
* 32 = 2048.
deposit_rootis the result of the
get_deposit_root()method of the Eth1 deposit contract after executing the Eth1 block being voted on - it's the root of the (incremental) Merkle tree of deposits.
deposit_countis the number of deposits in the deposit contract at that point, the result of the
get_deposit_countmethod on the contract. This will be equal to or greater than (if there are pending unprocessed deposits) the value of
block_hashis the hash of the Eth1 block being voted for. This doesn't have any current use within the Eth2 protocol, but is "too potentially useful to not throw in there", to quote Danny Ryan.
class HistoricalBatch(Container): block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
HistoricalBatch container has been superseded by
HistoricalSummary in the Capella upgrade. It remains in the spec since the
historical_roots list remains in the
BeaconState, albeit it now frozen forever.
HistoricalBatch is no longer used anywhere in the state transition. However, applications validating pre-Capella data against the
historical_roots list will need to use it.
process_historical_summaries_update() for more discussion of this change.
class DepositMessage(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 amount: Gwei
The basic information necessary to either add a validator to the registry, or to top up an existing validator's stake.
pubkey is the unique public key of the validator. If it is already present in the registry (the list of validators in beacon state) then
amount is added to its balance. Otherwise, a new
Validator entry is appended to the list and credited with
Validator container for more on
There are two protections that
DepositMessages get at different points.
DepositDatais included in beacon blocks as a
Deposit, which adds a Merkle proof that the data has been registered with the Eth1 deposit contract.
- When the containing beacon block is processed, deposit messages are stored, pending processing at the end of the epoch, in the beacon state as
DepositData. This includes the pending validator's BLS signature so that the authenticity of the
DepositMessagecan be verified before a validator is added.
class DepositData(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 amount: Gwei signature: BLSSignature # Signing over DepositMessage
DepositMessage. The comment says that the signing is done over
DepositMessage. What actually happens is that a
DepositMessage is constructed from the first three fields; the root of that is combined with
DOMAIN_DEPOSIT in a
SigningData object; finally the root of this is signed and included in
class BeaconBlockHeader(Container): slot: Slot proposer_index: ValidatorIndex parent_root: Root state_root: Root body_root: Root
BeaconBlockHeader is stored in beacon state to record the last processed block header. This is used to ensure that we proceed along a continuous chain of blocks that always point to their predecessor1. See
class SyncCommittee(Container): pubkeys: Vector[BLSPubkey, SYNC_COMMITTEE_SIZE] aggregate_pubkey: BLSPubkey
Sync committees were introduced in the Altair upgrade to support light clients to the beacon chain protocol. The list of committee members for each of the current and next sync committees is stored in the beacon state. Members are updated every
EPOCHS_PER_SYNC_COMMITTEE_PERIOD epochs by
aggregate_pubkey of the sync committee is an optimisation intended to save light clients some work when verifying the sync committee's signature. All the public keys of the committee members (including any duplicates) are aggregated into this single public key. If any signatures are missing from the
SyncAggregate, the light client can "de-aggregate" them by performing elliptic curve subtraction. As long as more than half of the committee contributed to the signature, then this will be faster than constructing the aggregate of participating members from scratch. If less than half contributed to the signature, the light client can start instead with the identity public key and use elliptic curve addition to aggregate those public keys that are present.
class SigningData(Container): object_root: Root domain: Domain
This is just a convenience container, used only in
compute_signing_root() to calculate the hash tree root of an object along with a domain. The resulting root is the message data that gets signed with a BLS signature. The
SigningData object itself is never stored or transmitted.
class Withdrawal(Container): index: WithdrawalIndex validator_index: ValidatorIndex address: ExecutionAddress amount: Gwei
A container for handling validator balance withdrawals from the consensus layer to the execution layer. The
index is a simple count of the total number of withdrawal transactions made since withdrawals were enabled in the Capella upgrade.
As per the type definition of the
amount field, the consensus layer denominates withdrawals in Gwei (as it does all Ether amounts), while the execution layer denominates withdrawals in Wei (as it does all Ether amounts). Care needs to be taken when dealing with withdrawal transactions not to end up a factor of wrong.
class HistoricalSummary(Container): """ `HistoricalSummary` matches the components of the phase0 `HistoricalBatch` making the two hash_tree_root-compatible. """ block_summary_root: Root state_summary_root: Root
This is part of the double batched accumulator mechanism implemented by
process_historical_summaries_update(). It was introduced in the Capella upgrade and supersedes [
HistoricalBatch] as the structure for storing roots of historical data.
The comment here is interesting. It reflects the invariant that the SSZ hash tree root of a container of objects is the same as the hash tree root of a container of the objects' hash tree roots - what I call the magic of Merkleization.
The following code demonstrates this equivalence between the pre- and post-Capella constructions. It should run with no errors.
from eth2spec.capella import mainnet from eth2spec.capella.mainnet import * from eth2spec.utils.ssz.ssz_typing import * # Dummy data roots = [Root('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef')] * SLOTS_PER_HISTORICAL_ROOT block_roots = state_roots = Vector[Root, SLOTS_PER_HISTORICAL_ROOT](*roots) # Pre-Capella historical_batch = HistoricalBatch( block_roots = block_roots, state_roots = state_roots) # Post-Capella historical_summary = HistoricalSummary( block_summary_root = hash_tree_root(block_roots), state_summary_root = hash_tree_root(state_roots)) assert(hash_tree_root(historical_batch) == hash_tree_root(historical_summary))
- It's a blockchain, yo!↩