One Page Annotated Spec
Note: This page is automatically generated from the chapters in Part 3. You may find that some internal links are broken.
Introduction
The beacon chain specification is the guts of the machine. Like the guts of a computer, all the components are showing and the wires are hanging out: everything is on display. In the course of the next sections I will be dissecting the entire core beacon chain specification line by line. My aim is not only to explain how things work, but also to give some historical context, some of the reasoning behind how we ended up where we are today.
Early versions of the specs were written with much more narrative and explanation than today's. Over time they were coded up in Python for better precision and the benefits of being executable. However, in that process, most of the explanation and intuition was removed.1 Vitalik has created his own annotated specifications that covers many of the key insights. It's hard to compete with Vitalik, but my intention here is to go one level deeper in thoroughness and detail. And perhaps to give an independent perspective.
As and when other parts of the book get written I will add links to the specific chapters on each topic (for example on Simple Serialize, consensus, networking).
Note that the online annotated specification is available in two forms:
- divided into chapters in Part 3 of the main book, and
- as a standalone single page that's useful for searching.
The contents of each are identical.
Version information
This edition of Upgrading Ethereum is based on the Altair version of the beacon chain specification, and corresponds to Release v1.1.1, made on the 4th of October 2021.
At the time of writing, there is no single specification document for Altair. Instead, there is the Phase 0 specification and an additional Altair document describing the differences (a kind of text-based diff).
For this work, I have consolidated the two specifications into one, omitting parts that were superseded by Altair. For the most part, I have tried to reflect the existing structure of the documents to make it easier to read side-by-side with the original spec. However, I have included the separate BLS and Altair fork documents into the flow of this one.
References
The main references:
- The Phase 0 beacon chain specification.
- Altair updates to the beacon chain specification.
- Vitalik's annotated specifications, covering Phase 0, Altair, The Merge, and beyond.
Other excellent, but in places outdated references:
- Serenity Design Rationale
- Phase 0 for Humans [v0.10.0]
- Phase 0 design notes (Justin Drake)
- My own Phase 0 annotated specification remains available for historical interest.
Types, Constants, Presets, and Configuration
Preamble
For some, a chapter on constants, presets and parameters will seem drier than the Namib desert, but I've long found these to be a rich and fertile way in to the ideas and mechanisms we'll be unpacking in detail in later chapters. Far from being a desert, this part of the spec bustles with life.
The foundation is laid with a set of custom data types. The beacon chain specification is executable in Python; the data types defined at the top of the spec represent the fundamental quantities that will reappear frequently.
Then – with constants, presets, and parameters – we will examine the numbers that define and constrain the behaviour of the chain. Each of these quantities tells a story. Each parameter encapsulates an insight, or a mechanism, or a compromise. Why is it here? How has it changed over time? Where does its value come from?
Custom Types
The specification defines the following Python custom types, "for type hinting and readability": the data types defined here appear frequently throughout the spec; they are the building blocks for everything else.
Each type has a name, an "SSZ equivalent", and a description. SSZ is the encoding method used to pass data between clients, among other things. Here it can be thought of as just a primitive data type.
Throughout the spec, (almost) all integers are unsigned 64 bit numbers, uint64
, but this hasn't always been the case.
Regarding "unsigned", there was much discussion around whether Eth2 should use signed or unsigned integers, and eventually unsigned was chosen. As a result, it is critical to preserve the order of operations in some places to avoid inadvertently causing underflows since negative numbers are forbidden.
And regarding "64 bit", early versions of the spec used other bit lengths than 64 (a "premature optimisation"), but arithmetic integers are now standardised at 64 bits throughout the spec, the only exception being ParticipationFlags
, introduced in the Altair fork, which has type uint8
, and is really a byte
type.
Name | SSZ equivalent | Description |
---|---|---|
Slot |
uint64 |
a slot number |
Epoch |
uint64 |
an epoch number |
CommitteeIndex |
uint64 |
a committee index at a slot |
ValidatorIndex |
uint64 |
a validator registry index |
Gwei |
uint64 |
an amount in Gwei |
Root |
Bytes32 |
a Merkle root |
Hash32 |
Bytes32 |
a 256-bit hash |
Version |
Bytes4 |
a fork version number |
DomainType |
Bytes4 |
a domain type |
ForkDigest |
Bytes4 |
a digest of the current fork data |
Domain |
Bytes32 |
a signature domain |
BLSPubkey |
Bytes48 |
a BLS12-381 public key |
BLSSignature |
Bytes96 |
a BLS12-381 signature |
ParticipationFlags |
uint8 |
a succinct representation of 8 boolean participation flags |
Slot
Time is divided into fixed length slots. Within each slot, exactly one validator is randomly selected to propose a beacon chain block. The progress of slots is the fundamental heartbeat of the beacon chain.
Epoch
Sequences of slots are combined into fixed-length epochs.
Epoch boundaries are the points at which the chain can be justified and finalised (by the Casper FFG mechanism). They are also the points at which validator balances are updated, validator committees get shuffled, and validator exits, entries, and slashings are processed. That is, the main state-transition work is performed per epoch, not per slot.
Epochs have always felt like a slightly uncomfortable overlay on top of the slot-by-slot progress of the beacon chain, but necessitated by Casper FFG finality. There have been proposals to move away from epochs, and there are possible future developments that could allow us to do away with epochs entirely. But, for the time being, they remain.
Fun fact: Epochs were originally called Cycles.
CommitteeIndex
Validators are organised into committees that collectively vote (make attestations) on blocks. Each committee is active at exactly one slot per epoch, but several committees are active at each slot. The CommitteeIndex
type is an index into the list of committees active at a slot.
The beacon chain's committee-based design is a large part of what makes it practical to implement while maintaining security. If all validators were active all the time, there would be an overwhelming number of messages to deal with. The random shuffling of committees make them very hard to subvert by an attacker without a supermajority of stake.
ValidatorIndex
Each validator making a successful deposit is consecutively assigned a unique validator index number that is permanent, remaining even after the validator exits. It is permanent because the validator's balance is associated with its index, so the data needs to be preserved when the validator exits, at least until the balance is withdrawn at an unknown future time.
Gwei
All Ether amounts are specified in units of Gwei ( Wei, Ether). This is basically a hack to avoid having to use integers wider than 64 bits to store validator balances and while doing calculations, since ( Wei is only 18 Ether. Even so, in some places care needs to be taken to avoid arithmetic overflow when dealing with Ether calculations.
Root
Merkle roots are ubiquitous in the Eth2 protocol. They are a very succinct and tamper-proof way of representing a lot of data, an example of a cryptographic accumulator. Blocks are summarised by their Merkle roots; state is summarised by its Merkle root; the list of Eth1 deposits is summarised by its Merkle root; the digital signature of a message is calculated from the Merkle root of the data structure contained within the message.
Hash32
Merkle roots are constructed with cryptographic hash functions. In the spec, a Hash32
type is used to represent Eth1 block roots (which are also Merkle roots).
I don't know why only the Eth1 block hash has been awarded the Hash32
type: other hashes in the spec remain Bytes32
. In early versions of the spec Hash32
was used for all cryptographic has quantities, but this was changed to Bytes32
.
Anyway, it's worth taking a moment in appreciation of the humble cryptographic hash function. The hash function is arguably the single most important algorithmic innovation underpinning blockchain technology, and in fact most of our online lives. Easily taken for granted, but utterly critical in enabling our modern world.
Version
Unlike Ethereum 12, the beacon chain has an in-protocol concept of a version number. It is expected that the protocol will be updated/upgraded from time to time, a process commonly known as a "hard-fork". For example, the upgrade from Phase 0 to Altair took place on the 27th of October 2021, and was assigned its own fork version.
Version
is used when computing the ForkDigest
.
DomainType
DomainType
is just a cryptographic nicety: messages intended for different purposes are tagged with different domains before being hashed and possibly signed. It's a kind of name-spacing to avoid clashes; probably unnecessary, but considered a best-practice. Ten domain types are defined in Altair.
ForkDigest
ForkDigest
is the unique chain identifier, generated by combining information gathered at genesis with the current chain Version
identifier.
The ForkDigest
serves two purposes.
- Within the consensus protocol to prevent, for example, attestations from validators on one fork (that maybe haven't upgraded yet) being counted on a different fork.
- Within the networking protocol to help to distinguish between useful peers that on the same chain, and useless peers that are on a different chain. This usage is described in the Ethereum 2.0 networking specification, where
ForkDigest
appears frequently.
Specifically, ForkDigest
is the first four bytes of the hash tree root of the ForkData
object containing the current chain Version
and the genesis_validators_root
which was created at beacon chain initialisation. It is computed in compute_fork_digest()
.
Domain
Domain
is used when verifying protocol messages validators. To be valid, a message must have been combined with both the correct domain and the correct fork version. It calculated as the concatenation of the four byte DomainType
and the first 28 bytes of the fork data root.
BLSPubkey
BLS (Boneh-Lynn-Shacham) is the digital signature scheme used by Eth2. It has some very nice properties, in particular the ability to aggregate signatures. This means that many validators can sign the same message (for example, that they support block X), and these signatures can all be efficiently aggregated into a single signature for verification. The ability to do this efficiently makes Eth2 practical as a protocol. Several other protocols have adopted or will adopt BLS, such as Zcash, Chia, Dfinity and Algorand. We are using the BLS signature scheme based on the BLS12-381 (Barreto-Lynn-Scott) elliptic curve.
The BLSPubkey
type holds a validator's public key, or the aggregation of several validators' public keys. This is used to verify messages that are claimed to have come from that validator or group of validators.
In Ethereum 2.0, BLS public keys are elliptic curve points from the BLS12-381 group, thus are 48 bytes long when compressed.
See the section on BLS signatures in part 2 for a more in-depth look at these things.
BLSSignature
As above, we are using BLS signatures over the BLS12-381 elliptic curve in order to sign messages between participants. As with all digital signature schemes, this guarantees both the identity of the sender and the integrity of the contents of any message.
In Ethereum 2.0, BLS signatures are elliptic curve points from the BLS12-381 group, thus are 96 bytes long when compressed.
ParticipationFlags
The ParticipationFlags
type was introduced in the Altair upgrade as part of the accounting reforms.
Prior to Altair, all attestations seen in blocks were stored in state for two epochs. At the end of an epoch, finality calculations, and reward and penalty calculations for each active validator, would be done by processing all of the attestations for the previous epoch as a batch. This created a spike in processing at epoch boundaries, and led to a noticeable increase in late blocks and attestations during the first slots of epochs. With Altair, participation flags are now used to continuously track validators' attestations, reducing the processing load at the end of epochs.
Three of the eight bits are currently used; five are reserved for future use.
As an aside, it might have been more intuitive if ParticipationFlags
were a Bytes1
type, rather than introducing a weird uint8
into the spec. After all, it is not used as an arithmetic integer. However, Bytes1
is a composite type in SSZ, really an alias for Vector[uint8, 1]
, whereas uint8
is a basic type. When computing the hash tree root of a List
type, multiple basic types can be packed into a single leaf, while composite types take a leaf each. This would result in 32 times as many hashing operations for a list of Bytes1
. For similar reasons the type of ParticipationFlags
was changed from bitlist
to uint8
.
References
- A primer on Merkle roots.
- See also Wikipedia on Merkle Trees.
- I have written an intro to the BLS12-381 elliptic curve elsewhere.
Constants
The distinction between "constants", "presets", and "configuration values" is not always clear, and things have moved back and forth between the sections at times3. In essence, "constants" are things that are expected never to change for the beacon chain, no matter what fork or test network it is running.
Miscellaneous
Name | Value |
---|---|
GENESIS_SLOT |
Slot(0) |
GENESIS_EPOCH |
Epoch(0) |
FAR_FUTURE_EPOCH |
Epoch(2**64 - 1) |
DEPOSIT_CONTRACT_TREE_DEPTH |
uint64(2**5) (= 32) |
JUSTIFICATION_BITS_LENGTH |
uint64(4) |
PARTICIPATION_FLAG_WEIGHTS |
[TIMELY_SOURCE_WEIGHT, TIMELY_TARGET_WEIGHT, TIMELY_HEAD_WEIGHT] |
ENDIANNESS |
'little' |
GENESIS_SLOT
The very first slot number for the beacon chain is zero.
Perhaps this seems uncontroversial, but it actually featured heavily in the Great Signedness Wars mentioned previously. The issue was that calculations on unsigned integers might have negative intermediate values, which would cause problems. A proposed work-around for this was to start the chain at a non-zero slot number. It was initially set to 2^19, then 2^63, then 2^32, and finally back to zero. In my humble opinion, this madness only confirms that we should have been using signed integers all along.
GENESIS_EPOCH
As above. When the chain starts, it starts at epoch zero.
FAR_FUTURE_EPOCH
A candidate for the dullest constant. It's used as a default initialiser for validators' activation and exit times before they are properly set. No epoch number will ever be bigger than this one.
DEPOSIT_CONTRACT_TREE_DEPTH
DEPOSIT_CONTRACT_TREE_DEPTH
specifies the size of the (sparse) Merkle tree used by the Eth1 deposit contract to store deposits made. With a value of 32, this allows for = 4.3 billion deposits. Given that the minimum deposit it 1 Ether, that number is clearly enough.
Since deposit receipts contain Merkle proofs, their size depends on the value of this constant.
JUSTIFICATION_BITS_LENGTH
As an optimisation to Casper FFG – the process by which finality is conferred on epochs – the beacon chain uses a "-finality" rule. We will describe this more fully when we look at processing justification and finalisation. For now, this constant is just the number of bits we need to store in state to implement -finality. With , we track the justification status of the last four epochs.
PARTICIPATION_FLAG_WEIGHTS
This array is just a convenient way to access the various weights given to different validator achievements when calculating rewards. The three weights are defined under incentivization weights, and each weight corresponds to a flag stored in state and defined under participation flag indices.
ENDIANNESS
Endianness refers to the order of bytes in the binary representation of a number: most significant byte first is big-endian; least significant byte first is little-endian. For the most part these details are hidden by compilers, and we don't need to worry about endianness. But endianness matters when converting between integers and bytes, which is relevant to shuffling and proposer selection, the RANDAO, and when serialising with SSZ.
The spec began life as big-endian, but the Nimbus team from Status successfully lobbied for it to be changed to little-endian to better match processor hardware implementations, and the endianness of WASM. SSZ was changed first, and then the rest of the spec followed.
Participation flag indices
Name | Value |
---|---|
TIMELY_SOURCE_FLAG_INDEX |
0 |
TIMELY_TARGET_FLAG_INDEX |
1 |
TIMELY_HEAD_FLAG_INDEX |
2 |
Validators making attestations that get included on-chain are rewarded for three things:
- getting attestations included with the correct source checkpoint within 5 slots (
integer_squareroot(SLOTS_PER_EPOCH)
); - getting attestations included with the correct target checkpoint within 32 slots (
SLOTS_PER_EPOCH
); and, - getting attestations included with the correct head within 1 slot (
MIN_ATTESTATION_INCLUSION_DELAY
), basically immediately.
These flags are temporarily recorded in the BeaconState
when attestations are processed, then used at the ends of epochs to update finality and to calculate validator rewards for making attestations.
The mechanism for rewarding timely inclusion of attestations (thus penalising late attestations) differs between Altair and Phase 0. In Phase 0, attestations included within 32 slots would receive the full reward for the votes they got correct (source, target, head), plus a declining reward based on the delay in inclusion: for a two slot delay, for a three slot delay, and so on. With Altair, for each vote, we now have a cliff before which the validator receives the full reward and after which a penalty. The cliffs differ in duration, which is intended to more accurately target incentives at behaviours that genuinely help the chain (there is little value in rewarding a correct head vote made 30 slots late, for example). See get_attestation_participation_flag_indices()
for how this is implemented in code.
Incentivization weights
Name | Value |
---|---|
TIMELY_SOURCE_WEIGHT |
uint64(14) |
TIMELY_TARGET_WEIGHT |
uint64(26) |
TIMELY_HEAD_WEIGHT |
uint64(14) |
SYNC_REWARD_WEIGHT |
uint64(2) |
PROPOSER_WEIGHT |
uint64(8) |
WEIGHT_DENOMINATOR |
uint64(64) |
These weights are used to calculate the reward earned by a validator for performing its duties. There are five duties in total. Three relate to making attestations: attesting to the source epoch, attesting to the target epoch, and attesting to the head block. There are also rewards for proposing blocks, and for participating in sync committees. Note that the sum of five the weights is equal to WEIGHT_DENOMINATOR
.
On a long-term average, a validator can expect to earn a total amount of get_base_reward()
per epoch, with these weights being the relative portions for each of the duties comprising that total. Proposing blocks and participating in sync committees do not happen in every epoch, but are randomly assigned, so over small periods of time validator earnings may differ from get_base_reward()
.
The apportioning of rewards was overhauled in the Altair upgrade to better reflect the importance of each activity within the protocol. The total reward amount remains the same, but sync committee rewards were added, and the relative weights were adjusted. Previously, the weights corresponded to 16 for correct source, 16 for correct target, 16 for correct head, 14 for inclusion (equivalent to correct source), and 2 for block proposals. The factor of four increase in the proposer reward addressed a long-standing spec bug.
Withdrawal Prefixes
Name | Value |
---|---|
BLS_WITHDRAWAL_PREFIX |
Bytes1('0x00') |
ETH1_ADDRESS_WITHDRAWAL_PREFIX |
Bytes1('0x01') |
Withdrawal prefixes relate to the withdrawal credentials provided when deposits are made for validators. The withdrawal credential is a commitment to a private key that may be used later to withdraw funds from the validator's balance on the beacon chain.
Two ways to specify the withdrawal credentials are currently available, versioned with these prefixes, with others such as 0x02
and 0x03
under discussion.
These withdrawal credential prefixes are not yet significant in the core beacon chain spec, but will become significant when withdrawals are enabled in a future upgrade. The withdrawal credentials data is not consensus-critical, and future withdrawal credential types can be added without a hard fork. There are suggestions as to how existing credentials might be changed between methods which would be consensus critical.
The presence of these prefixes in the spec indicates a "social consensus" among the dev teams and protocol designers that we will in future support these methods for making withdrawals.
See the Withdrawals section for discussion on what the mechanism might look like.
BLS_WITHDRAWAL_PREFIX
The beacon chain launched with only BLS-style withdrawal credentials available, so all early stakers used this. The 0x00
prefix on the credential distinguishes this type from the others: it replaces the first byte of the hash of the BLS public key that corresponds to the BLS private key of the staker.
With this type of credential, in addition to a BLS signing key, stakers need a second BLS key that they will later use for withdrawals. The credential registered in the deposit data is the 32 byte SHA256 hash of the validators withdrawal public key, with the first byte set to BLS_WITHDRAWAL_PREFIX
.
ETH1_ADDRESS_WITHDRAWAL_PREFIX
Eth1 withdrawal credentials are much simpler, and were adopted once it became clear that Ethereum 2.0 would not be using a BLS-based address scheme for accounts at any time soon. These provide a commitment that stakers will be able to withdraw their beacon chain funds to a normal Ethereum account (possibly a contract account) at a future date.
Domain types
Name | Value |
---|---|
DOMAIN_BEACON_PROPOSER |
DomainType('0x00000000') |
DOMAIN_BEACON_ATTESTER |
DomainType('0x01000000') |
DOMAIN_RANDAO |
DomainType('0x02000000') |
DOMAIN_DEPOSIT |
DomainType('0x03000000') |
DOMAIN_VOLUNTARY_EXIT |
DomainType('0x04000000') |
DOMAIN_SELECTION_PROOF |
DomainType('0x05000000') |
DOMAIN_AGGREGATE_AND_PROOF |
DomainType('0x06000000') |
DOMAIN_SYNC_COMMITTEE |
DomainType('0x07000000') |
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF |
DomainType('0x08000000') |
DOMAIN_CONTRIBUTION_AND_PROOF |
DomainType('0x09000000') |
These domain types are used in three ways: for seeds, for signatures, and for selecting aggregators.
As seeds
When random numbers are required in-protocol, one way they are generated is by hashing the RANDAO mix with other quantities, one of them being a domain type (see get_seed()
). The original motivation was to avoid occasional collisions between Phase 0 committees and Phase 1 persistent committees, back when they were a thing. So, when computing the beacon block proposer, DOMAIN_BEACON_PROPOSER
is hashed into the seed, when computing attestation committees, DOMAIN_BEACON_ATTESTER
is hashed in, and when computing sync committees, DOMAIN_SYNC_COMMITTEE
is hashed in.
As signatures
In addition, as a cryptographic nicety, each of the protocol's signature types is augmented with the appropriate domain before being signed:
- Signed block proposals incorporate
DOMAIN_BEACON_PROPOSER
- Signed attestations incorporate
DOMAIN_BEACON_ATTESTER
- RANDAO reveals are BLS signatures, and use
DOMAIN_RANDAO
- Deposit data messages incorporate
DOMAIN_DEPOSIT
- Validator voluntary exit messages incorporate
DOMAIN_VOLUNTARY_EXIT
- Sync committee signatures incorporate
DOMAIN_SYNC_COMMITTEE
In each case, except for deposits, the fork version is also incorporated before signing. Deposits are valid across forks, but other messages are not. Note that this would allow validators to participate, if they wish, in two independent forks of the beacon chain without fear of being slashed.
Aggregator selection
The remaining four types, suffixed _PROOF
are not used directly in the beacon chain specification. They were introduced to implement attestation subnet validations for denial of service resistance. The technique was extended to sync committees with the Altair upgrade.
Briefly, at each slot, validators are selected to aggregate attestations from their committees. The selection is done based on the validator's signature over the slot number, mixing in DOMAIN_SELECTION_PROOF
. The validator then signs the whole aggregated attestation, including the previous signature as proof that it was selected to be a validator, using DOMAIN_AGGREGATE_AND_PROOF
. And similarly for sync committees. In this way, everything is verifiable and attributable, making it hard to flood the network with fake messages.
These four are not part of the consensus-critical state-transition, but are nonetheless important to the healthy functioning of the chain.
This mechanism is described in the Phase 0 honest validator spec for attestation aggregation, and in the Altair honest validator spec for sync committee aggregation.
Crypto
Name | Value |
---|---|
G2_POINT_AT_INFINITY |
BLSSignature(b'\xc0' + b'\x00' * 95) |
This is the compressed serialisation of the "point at infinity", the identity point, of the G2 group of the BLS12-381 curve that we are using for signatures. Note that it is in big-endian format (unlike all other constants in the spec).
It was introduced as a convenience when verifying aggregate signatures that contain no public keys in eth_fast_aggregate_verify()
. The underlying FastAggregateVerify function from the BLS signature standard would reject these.
G2_POINT_AT_INFINITY
is described in the separate BLS Extensions document, but included here for convenience.
Preset
The "presets" are consistent collections of configuration variables that are bundled together. The specs repo currently defines two sets of presets, mainnet and minimal. The mainnet configuration is running in production on the beacon chain; minimal is often used for testing. Other configurations are possible. For example, Teku uses a swift configuration for acceptance testing.
All the values discussed below are from the mainnet configuration.
You'll notice that most of these values are powers of two. There's no huge significance to this. Computer scientists think it's neat, and it ensures that things cleanly divide other things in general. There is a view that this practice helps to minimise bike-shedding (endless arguments over trivial matters).
Some of the configuration parameters below are quite technical and perhaps obscure. I'll take the opportunity here to introduce some concepts, and give more detailed explanations when they appear in later chapters.
Misc
Name | Value |
---|---|
MAX_COMMITTEES_PER_SLOT |
uint64(2**6) (= 64) |
TARGET_COMMITTEE_SIZE |
uint64(2**7) (= 128) |
MAX_VALIDATORS_PER_COMMITTEE |
uint64(2**11) (= 2,048) |
SHUFFLE_ROUND_COUNT |
uint64(90) |
MAX_COMMITTEES_PER_SLOT
Validators are organised into committees to do their work. At any one time, each validator is a member of exactly one beacon chain committee, and is called on to make an attestation exactly once per epoch. An attestation is a vote for, or a statement of, the validator's view of the chain at that point in time.
On the beacon chain, up to 64 committees are active in a slot and effectively act as a single committee as far as the fork-choice rule is concerned. They all vote on the proposed block for the slot, and their votes/attestations are pooled. In a similar way, all committees active during an epoch (that is, the whole active validator set) act effectively as a single committee as far as justification and finalisation are concerned.
The number 64 is intended to map to one committee per shard once data shards are deployed, since these committees will also vote on shard crosslinks.
Note that sync committees are a different thing: there is only one sync committee active at any time.
TARGET_COMMITTEE_SIZE
To achieve a desirable level of security, committees need to be larger than a certain size. This makes it infeasible for an attacker to randomly end up with a super-majority in a committee even if they control a significant number of validators. The target here is a kind of lower-bound on committee size. If there are not enough validators for all committees to have at least 128 members, then, as a first measure, the number of committees per slot is reduced to maintain this minimum. Only if there are fewer than SLOTS_PER_EPOCH
* TARGET_COMMITTEE_SIZE
= 4096 validators in total will the committee size be reduced below TARGET_COMMITTEE_SIZE
. With so few validators, the system would be insecure in any case.
For further discussion and an explanation of how the value of TARGET_COMMITTEE_SIZE
was set, see the section on committees.
MAX_VALIDATORS_PER_COMMITTEE
This is just used for sizing some data structures, and is not particularly interesting. Reaching this limit would imply over 4 million active validators, staked with a total of 128 million Ether, which exceeds the total supply today.
SHUFFLE_ROUND_COUNT
The beacon chain implements a rather interesting way of shuffling validators in order to select committees, called the "swap-or-not shuffle". This shuffle proceeds in rounds, and the degree of shuffling is determined by the number of rounds, SHUFFLE_ROUND_COUNT
. The time taken to shuffle is linear in the number of rounds, so for light-weight, non-mainnet configurations, the number of rounds can be reduced.
The value 90 was introduced in Vitalik's initial commit without explanation. The original paper describing the shuffling technique seems to suggest that a cryptographically safe number of rounds is . With 90 rounds, then, we should be good for shuffling 3.3 million validators, which is close to the maximum number possible (given the Ether supply).
Hysteresis parameters
Name | Value |
---|---|
HYSTERESIS_QUOTIENT |
uint64(4) |
HYSTERESIS_DOWNWARD_MULTIPLIER |
uint64(1) |
HYSTERESIS_UPWARD_MULTIPLIER |
uint64(5) |
The parameters prefixed HYSTERESIS_
control the way that effective balance is changed (see EFFECTIVE_BALANCE_INCREMENT
). As described there, the effective balance of a validator follows changes to the actual balance in a step-wise way, with hysteresis applied. This ensures that the effective balance does not change often.
The original hysteresis design had an unintended effect that might have encouraged stakers to over-deposit or make multiple deposits in order to maintain a balance above 32 Ether at all times. If a validator's balance were to drop below 32 Ether soon after depositing, however briefly, the effective balance would have immediately dropped to 31 Ether and taken a long time to recover. This would have resulted in a 3% reduction in rewards for a period.
This problem was addressed by making the hysteresis configurable via these parameters. Specifically, these settings mean:
- if a validators' balance falls 0.25 Ether below its effective balance, then its effective balance is reduced by 1 Ether
- if a validator's balance rises 1.25 Ether above its effective balance, then its effective balance is increased by 1 Ether
These calculations are done in process_effective_balance_updates()
during end of epoch processing.
Gwei values
Name | Value |
---|---|
MIN_DEPOSIT_AMOUNT |
Gwei(2**0 * 10**9) (= 1,000,000,000) |
MAX_EFFECTIVE_BALANCE |
Gwei(2**5 * 10**9) (= 32,000,000,000) |
EFFECTIVE_BALANCE_INCREMENT |
Gwei(2**0 * 10**9) (= 1,000,000,000) |
MIN_DEPOSIT_AMOUNT
MIN_DEPOSIT_AMOUNT
is not actually used anywhere within the beacon chain specification document. Rather, it is enforced in the deposit contract that was deployed to the Ethereum 1 chain. Any amount less than this value sent to the deposit contract is reverted.
Allowing stakers to make deposits smaller than a full stake is useful for topping-up a validator's balance if its effective balance has dropped below 32 Ether, so as to maintain full productivity. However, this actually led to a vulnerability for some staking pools, involving the front-running of deposits. In some circumstances, a front-running attacker could change a genuine depositor's withdrawal credentials to their own.
MAX_EFFECTIVE_BALANCE
There is a concept of "effective balance" for validators: whatever a validator's total balance, its voting power is weighted by its effective balance, even if its actual balance is higher. Effective balance is also the amount on which all rewards, penalties, and slashings are calculated - it's used a lot in the protocol
The MAX_EFFECTIVE_BALANCE
is the highest effective balance that a validator can have: 32 Ether. Any balance above this is ignored. Note that this means that staking rewards don't compound in the usual case (unless a validator's effective balance somehow falls below 32 Ether, in which case rewards kind of compound).
There is a discussion in the Design Rationale of why 32 Ether was chosen as the staking amount. In short, we want enough validators to keep the chain both alive and secure under attack, but not so many that the message overhead on the network becomes too high.
EFFECTIVE_BALANCE_INCREMENT
Throughout the protocol, a quantity called "effective balance" is used instead of the validators' actual balances. Effective balance tracks the actual balance, with two differences: (1) effective balance is capped at MAX_EFFECTIVE_BALANCE
no matter how high the actual balance of a validator is, and (2) effective balance is much more granular - it changes only in steps of EFFECTIVE_BALANCE_INCREMENT
rather than Gwei
.
This discretisation of effective balance is intended to reduce the amount of hashing required when making state updates. The goal is to avoid having to re-calculate the hash tree root of validator records too often. Validators' actual balances, which change frequently, are stored as a contiguous list in BeaconState, outside of validators' records. Effective balances are stored inside validators' individual records, which are more costly to update (more hashing required). So we try to update effective balances relatively infrequently.
Effective balance is changed according to a process with hysteresis to avoid situations where it might change frequently. See HYSTERESIS_QUOTIENT
.
You can read more about effective balance in the Design Rationale and in this article.
Time parameters
Name | Value | Unit | Duration |
---|---|---|---|
MIN_ATTESTATION_INCLUSION_DELAY |
uint64(2**0) (= 1) |
slots | 12 seconds |
SLOTS_PER_EPOCH |
uint64(2**5) (= 32) |
slots | 6.4 minutes |
MIN_SEED_LOOKAHEAD |
uint64(2**0) (= 1) |
epochs | 6.4 minutes |
MAX_SEED_LOOKAHEAD |
uint64(2**2) (= 4) |
epochs | 25.6 minutes |
MIN_EPOCHS_TO_INACTIVITY_PENALTY |
uint64(2**2) (= 4) |
epochs | 25.6 minutes |
EPOCHS_PER_ETH1_VOTING_PERIOD |
uint64(2**6) (= 64) |
epochs | ~6.8 hours |
SLOTS_PER_HISTORICAL_ROOT |
uint64(2**13) (= 8,192) |
slots | ~27 hours |
MIN_ATTESTATION_INCLUSION_DELAY
A design goal of Ethereum 2.0 is not to heavily disadvantage validators that are running on lower-spec systems, or, conversely, to reduce any advantage gained by running on high-spec systems.
One aspect of performance is network bandwidth. When a validator becomes the block proposer, it needs to gather attestations from the rest of its committee. On a low-bandwidth link, this takes longer, and could result in the proposer not being able to include as many past attestations as other better-connected validators might, thus receiving lower rewards.
MIN_ATTESTATION_INCLUSION_DELAY
was an attempt to "level the playing field" by setting a minimum number of slots before an attestation can be included in a beacon block. It was originally set at 4, with a 6 second slot time, allowing 24 seconds for attestations to propagate around the network.
It was later set to one – attestations are included as early as possible – and, now that we plan to crosslink shards every slot, this is the only value that makes sense. So MIN_ATTESTATION_INCLUSION_DELAY
exists today as a kind of relic of the earlier design.
The current slot time of 12 seconds is assumed to allow sufficient time for attestations to propagate and be aggregated sufficiently within one slot.
SLOTS_PER_EPOCH
We currently have 12 second slots and 32 slot epochs. In earlier designs slots were six seconds and there were 64 slots per epoch. So the time between epoch boundaries was unchanged when slots were lengthened.
The choice of 32 slots per epoch is a trade-off between time to finality (we need two epochs to finalise, so we prefer to keep them as short as we can) and being as certain as possible that at least one honest proposer per epoch will make a block to update the RANDAO (for which we prefer longer epochs).
In addition, epoch boundaries are where the heaviest part of the beacon chain state-transition calculation occurs, so that's another reason for not having them too close together.
Since every validator attests one every epoch, there is an interplay between the number of slots per epoch, the number of committees per slot, committee sizes, and the total number of validators.
MIN_SEED_LOOKAHEAD
A random seed is used to select all the committees and proposers for an epoch. During each epoch, the beacon chain accumulates randomness from proposers via the RANDAO and stores it. The seed for the current epoch is based on the RANDAO output from the epoch MIN_SEED_LOOKAHEAD
+
1
ago. With MIN_SEED_LOOKAHEAD
set to one, the effect is that we can know the seed for the current epoch and the next epoch, but not beyond, since the next-but-one epoch depends on randomness from the current epoch that hasn't been accumulated yet.
This mechanism is designed to allow sufficient time for committee members to find each other on the peer-to-peer network, and in future to sync up any shard data they need. But preventing committee makeup being known too far ahead limits the opportunity for coordinated collusion between validators.
MAX_SEED_LOOKAHEAD
The above notwithstanding, if an attacker has a large proportion of the stake, or is, for example, able to DoS block proposers for a while, then it might be possible for the attacker to predict the output of the RANDAO further ahead than MIN_SEED_LOOKAHEAD
would normally allow. This might enable the attacker to manipulate committee memberships to their advantage by performing well-timed exits and activations of their validators.
To prevent this, we assume a maximum feasible lookahead that an attacker might achieve (MAX_SEED_LOOKAHEAD
) and delay all activations and exits by this amount, which allows new randomness to come in via block proposals from honest validators. With MAX_SEED_LOOKAHEAD
set to 4, if only 10% of validators are online and honest, then the chance that an attacker can succeed in forecasting the seed beyond (MAX_SEED_LOOKAHEAD
-
MIN_SEED_LOOKAHEAD
) = 3 epochs is , which is about 1 in 25,000.
MIN_EPOCHS_TO_INACTIVITY_PENALTY
The inactivity penalty is discussed below. This parameter sets the length of time until it kicks in. If the last finalised epoch is longer ago than MIN_EPOCHS_TO_INACTIVITY_PENALTY
, then the beacon chain starts operating in "leak" mode. In this mode, participating validators no longer get rewarded, and validators that are not participating get penalised.
EPOCHS_PER_ETH1_VOTING_PERIOD
In order to safely onboard new validators, the beacon chain needs to take a view on what the Eth1 chain looks like. This is done by collecting votes from beacon block proposers - they are expected to consult an available Eth1 client in order to construct their vote.
EPOCHS_PER_ETH1_VOTING_PERIOD
*
SLOTS_PER_EPOCH
is the total number of votes for Eth1 blocks that are collected. As soon as half of this number of votes are for the same Eth1 block, that block is adopted by the beacon chain and deposit processing can continue.
Rules for how validators select the right block to vote for are set out in the validator guide. ETH1_FOLLOW_DISTANCE
is the (approximate) minimum depth of block that can be considered.
This parameter was increased from 32 to 64 epochs for the beacon chain mainnet. This increase is intended to allow devs more time to respond if there is any trouble on the Eth1 chain, in addition to the eight hours grace provided by ETH1_FOLLOW_DISTANCE
.
For a detailed analysis of these parameters, see this article.
SLOTS_PER_HISTORICAL_ROOT
There have been several redesigns of the way the beacon chain stores its past history. The current design is a double batched accumulator. The block root and state root for every slot are stored in the state for SLOTS_PER_HISTORICAL_ROOT
slots. When that list is full, both lists are Merkleized into a single Merkle root, which is added to the ever-growing state.historical_roots
list.
State list lengths
The following parameters set the sizes of some lists in the beacon chain state. Some lists have natural sizes, others such as the validator registry need an explicit maximum size to guide SSZ serialisation.
Name | Value | Unit | Duration |
---|---|---|---|
EPOCHS_PER_HISTORICAL_VECTOR |
uint64(2**16) (= 65,536) |
epochs | ~0.8 years |
EPOCHS_PER_SLASHINGS_VECTOR |
uint64(2**13) (= 8,192) |
epochs | ~36 days |
HISTORICAL_ROOTS_LIMIT |
uint64(2**24) (= 16,777,216) |
historical roots | ~52,262 years |
VALIDATOR_REGISTRY_LIMIT |
uint64(2**40) (= 1,099,511,627,776) |
validators |
EPOCHS_PER_HISTORICAL_VECTOR
This is the number of epochs of previous RANDAO mixes that are stored (one per epoch). Having access to past randao mixes allows historical shufflings to be recalculated. Since Validator records keep track of the activation and exit epochs of all past validators, we can thus reconstitute past committees as far back as we have the RANDAO values. This information can be used for slashing long-past attestations, for example. It is not clear how the value of this parameter was decided.
EPOCHS_PER_SLASHINGS_VECTOR
In the epoch in which a misbehaving validator is slashed, its effective balance is added to an accumulator in the state. In this way, the state.slashings
list tracks the total effective balance of all validators slashed during the last EPOCHS_PER_SLASHINGS_VECTOR
epochs.
At a time EPOCHS_PER_SLASHINGS_VECTOR
//
2
after being slashed, a further penalty is applied to the slashed validator, based on the total amount of value slashed during the 4096 epochs before and the 4096 epochs after it was originally slashed.
The idea of this is to disproportionately punish coordinated attacks, in which many validators break the slashing conditions around the same time, while only lightly penalising validators that get slashed by making a mistake. Early designs for Eth2 would always slash a validator's entire deposit.
See also PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR
.
HISTORICAL_ROOTS_LIMIT
Every SLOTS_PER_HISTORICAL_ROOT
slots, the list of block roots and the list of state roots in the beacon state are Merkleized and added to state.historical_roots
list. Although state.historical_roots
is in principle unbounded, all SSZ lists must have maximum sizes specified. The size HISTORICAL_ROOTS_LIMIT
will be fine for the next few millennia, after which it will be somebody else's problem. The list grows at less than 10 KB per year.
Storing past roots like this allows Merkle proofs to be constructed about anything in the beacon chain's history if required.
VALIDATOR_REGISTRY_LIMIT
Every time the Eth1 deposit contract processes a deposit from a new validator (as identified by its public key), a new entry is appended to the state.validators
list.
In the current design, validators are never removed from this list, even after exiting from being a validator. This is largely because there is nowhere yet to send a validator's remaining deposit and staking rewards, so they continue to need to be tracked in the beacon chain.
The maximum length of this list is VALIDATOR_REGISTRY_LIMIT
, which is one trillion, so we ought to be OK for a while, especially given that the minimum deposit amount is 1 Ether.
Rewards and penalties
Name | Value |
---|---|
BASE_REWARD_FACTOR |
uint64(2**6) (= 64) |
WHISTLEBLOWER_REWARD_QUOTIENT |
uint64(2**9) (= 512) |
PROPOSER_REWARD_QUOTIENT |
uint64(2**3) (= 8) |
INACTIVITY_PENALTY_QUOTIENT |
uint64(2**26) (= 67,108,864) |
MIN_SLASHING_PENALTY_QUOTIENT |
uint64(2**7) (= 128) |
PROPORTIONAL_SLASHING_MULTIPLIER |
uint64(1) |
INACTIVITY_PENALTY_QUOTIENT_ALTAIR |
uint64(3 * 2**24) (= 50,331,648) |
MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR |
uint64(2**6) (= 64) |
PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR |
uint64(2) |
Note that there are similar constants with different values here, one version with an _ALTAIR
suffix. This is explained in the specs repo as follows:
Variables are not replaced but extended with forks. This is to support syncing from one state to another over a fork boundary, without hot-swapping a config. Instead, for forks that introduce changes in a variable, the variable name is suffixed with the fork name.
So, the unsuffixed versions are the Phase 0 values, and the _ALTAIR
suffixed versions are the values that apply to the current Altair fork.
BASE_REWARD_FACTOR
This is the big knob to turn to change the issuance rate of Eth2. Almost all validator rewards are calculated in terms of a "base reward per increment" which is formulated as,
EFFECTIVE_BALANCE_INCREMENT * BASE_REWARD_FACTOR // integer_squareroot(get_total_active_balance(state))
Thus, the total validator rewards per epoch (the Eth2 issuance rate) could be tuned by increasing or decreasing BASE_REWARD_FACTOR
.
The exception is proposer rewards for including slashing reports in blocks. However, these are more than offset by the amount of stake burnt, so do not increase the overall issuance rate.
WHISTLEBLOWER_REWARD_QUOTIENT
One reward that is not tied to the base reward is the whistleblower reward. This is an amount awarded to the proposer of a block containing one or more proofs that a proposer or attester has violated a slashing condition. The whistleblower reward is set at of the effective balance of the slashed validator.
The whistleblower reward comes from new issuance of Ether on the beacon chain, but is more than offset by the Ether burned due to slashing penalties.
PROPOSER_REWARD_QUOTIENT
PROPOSER_REWARD_QUOTIENT
was removed in the Altair upgrade in favour of PROPOSER_WEIGHT
. It was used to apportion rewards between attesters and proposers when including attestations in blocks.
INACTIVITY_PENALTY_QUOTIENT_ALTAIR
This value supersedes INACTIVITY_PENALTY_QUOTIENT
.
If the beacon chain hasn't finalised a checkpoint for longer than MIN_EPOCHS_TO_INACTIVITY_PENALTY
epochs, then it enters "leak" mode. In this mode, any validator that does not vote (or votes for an incorrect target) is penalised an amount each epoch of (effective_balance * inactivity_score) // (
INACTIVITY_SCORE_BIAS
*
INACTIVITY_PENALTY_QUOTIENT_ALTAIR
)
.
In Altair, inactivity_score
is a per-validator quantity, whereas previously validators were penalised by a globally calculated amount when they missed a duty during a leak. See inactivity penalties for more on the rationale for this and how this score is calculated per validator.
During a leak, no validators receive rewards, and they continue to accrue the normal penalties when they fail to fulfil duties. In addition, for epochs in which validators do not make a correct, timely target vote, they receive a leak penalty.
To examine the effect of the leak on a single validator's balance, assume that during a period of inactivity leak (non-finalisation) the validator is completely offline. At each epoch, the offline validator will be penalised an amount , where is the number of epochs since the leak started, is the validator's effective balance, and is the prevailing INACTIVITY_PENALTY_QUOTIENT
.
The effective balance will remain constant for a while, by design, during which time the total amount of the penalty after epochs would be : the famous "quadratic leak". If were continuously variable, the penalty would satisfy , which can be solved to give . The actual behaviour is somewhere between these two since the effective balance decreases in a step-wise fashion.
In the continuous case, the INACTIVITY_PENALTY_QUOTIENT
, , is the square of the time it takes to reduce the balance of a non-participating validator to , or around 60.7% of its initial value. With the value of INACTIVITY_PENALTY_QUOTIENT_ALTAIR
at 3 * 2**24
, this equates to around seven thousand epochs, or 31.5 days.
The idea for the inactivity leak (aka the quadratic leak) was proposed in the original Casper FFG paper. The problem it addresses is that, if a large fraction of the validator set were to go offline at the same time, it would not be possible to continue finalising checkpoints, since a majority vote from validators representing 2/3 of the total stake is required for finalisation.
In order to recover, the inactivity leak gradually reduces the stakes of validators who are not making attestations until, eventually, the participating validators control 2/3 of the remaining stake. They can then begin to finalise checkpoints once again.
This inactivity penalty mechanism is designed to protect the chain long-term in the face of catastrophic events (sometimes referred to as the ability to survive World War III). The result might be that the beacon chain could permanently split into two independent chains either side of a network partition, and this is assumed to be a reasonable outcome for any problem that can't be fixed in a few weeks. In this sense, the beacon chain formally prioritises availability over consistency. (You can't have both.)
The value of INACTIVITY_PENALTY_QUOTIENT
was increased by a factor of four from 2**24
to 2**26
for the beacon chain launch, with the intention of penalising validators less severely in case of non-finalisation due to implementation problems in the early days. As it happens, there were no instances of non-finalisation during the eleven months of Phase 0 of the beacon chain.
The value was decreased by one quarter in the Altair upgrade from 2**26
to 3 * 2**24
as a step towards eventually setting it to its final value. Decreasing the inactivity penalty quotient speeds up recovery of finalisation in the event of an inactivity leak.
MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR
When a validator is first convicted of a slashable offence, an initial penalty is applied. This is calculated as, validator.effective_balance
//
MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR
.
Thus, the initial slashing penalty is between 0.25 Ether and 0.5 Ether depending on the validator's effective balance (which is between 16 and 32 Ether; note that effective balance is denominated in Gwei).
A further slashing penalty is applied later based on the total amount of balance slashed during a period of EPOCHS_PER_SLASHINGS_VECTOR
.
The value of MIN_SLASHING_PENALTY_QUOTIENT
was increased by a factor of four from 2**5
to 2**7
for the beacon chain launch, anticipating that unfamiliarity with the rules of Ethereum 2.0 staking was likely to result in some unwary users getting slashed. In the event, a total of 157 validators were slashed during Phase 0, all as a result of user error or misconfiguration as far as can be determined.
The value was halved in the Altair upgrade from 2**7
to 2**6
as a step towards eventually setting it to its final value of 2**5
.
PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR
When a validator has been slashed, a further penalty is later applied to the validator based on how many other validators were slashed during a window of size EPOCHS_PER_SLASHINGS_VECTOR
epochs centred on that slashing event (approximately 18 days before and after).
The proportion of the validator's remaining effective balance that will be subtracted is calculated as, PROPORTIONAL_SLASHING_MULTIPLIER
multiplied by the sum of the effective balances of the slashed validators in the window, divided by the total effective balance of all validators. The idea of this mechanism is to punish accidents lightly (in which only a small number of validators were slashed) and attacks heavily (where many validators coordinated to double vote).
To finalise conflicting checkpoints, at least a third of the balance must have voted for both. That's why the "natural" setting of PROPORTIONAL_SLASHING_MULTIPLIER
is three: in the event of an attack that finalises conflicting checkpoints, the attackers lose their entire stake. This provides "the maximal minimum accountable safety margin".
However, for the initial stage of the beacon chain, Phase 0, PROPORTIONAL_SLASHING_MULTIPLIER
was set to one, and increased to two at the Altair upgrade. These lower values provide some insurance against client bugs that might cause mass slashings in the early days. It will eventually be increased to its final value of three in a later upgrade.
Max operations per block
Name | Value |
---|---|
MAX_PROPOSER_SLASHINGS |
2**4 (= 16) |
MAX_ATTESTER_SLASHINGS |
2**1 (= 2) |
MAX_ATTESTATIONS |
2**7 (= 128) |
MAX_DEPOSITS |
2**4 (= 16) |
MAX_VOLUNTARY_EXITS |
2**4 (= 16) |
These parameters are used to size lists in the beacon block bodies for the purposes of SSZ serialisation, as well as constraining the maximum size of beacon blocks so that they can propagate efficiently, and avoid DoS attacks.
Some comments on the chosen values:
- I have suggested elsewhere reducing
MAX_DEPOSITS
from sixteen to one to ensure that more validators must process deposits, which encourages them to run Eth1 clients. - At first sight, there looks to be a disparity between the number of proposer slashings and the number of attester slashings that may be included in a block. But note that an attester slashing (a) can be much larger than a proposer slashing, and (b) can result in many more validators getting slashed than a proposer slashing.
MAX_ATTESTATIONS
is double the value ofMAX_COMMITTEES_PER_SLOT
. This allows there to be an empty slot (with no block proposal), yet still include all the attestations for the empty slot in the next slot. Since, ideally, each committee produces a single aggregate attestation, a block can hold two slots' worth of aggregate attestations.
Sync committee
Name | Value | Unit | Duration |
---|---|---|---|
SYNC_COMMITTEE_SIZE |
uint64(2**9) (= 512) |
Validators | |
EPOCHS_PER_SYNC_COMMITTEE_PERIOD |
uint64(2**8) (= 256) |
epochs | ~27 hours |
Sync committees were introduced by the Altair upgrade to allow light clients to quickly and trustlessly determine the head of the beacon chain.
Why did we need a new committee type? Couldn't this be built on top of existing committees, say committees 0 to 3 at a slot? After all, voting for the head of the chain is already one of their duties. The reason is that it is important for reducing the load on light clients that sync committees do not change very often. Standard committees change every slot; we need something much longer lived here.
Only a single sync committee is active at any one time, and contains a randomly selected subset of size SYNC_COMMITTEE_SIZE
of the total validator set.
A sync committee does its duties (and receives rewards for doing so) for only EPOCHS_PER_SYNC_COMMITTEE_PERIOD
epochs until the next committee takes over.
With 262,144 validators (), the expected time between being selected for sync committee duty is over 19 months. The probability of being in the current sync committee would be 1/512 per validator.
SYNC_COMMITTEE_SIZE
is a trade-off between security (ensuring that enough honest validators are always present) and efficiency for light clients (ensuring that they do not have to handle too much computation). The value 512 is conservative in terms of safety. It would be catastrophic for trustless bridges to other protocols, for example, if a sync committee voted in an invalid block.
EPOCHS_PER_SYNC_COMMITTEE_PERIOD
is around a day, and again is a trade-off between security (short enough that it's hard for an attacker to find and corrupt committee members) and efficiency (reducing the data load on light clients).
Configuration
Genesis Settings
With Altair, beacon chain genesis is long behind us. Nevertheless, the ability to spin-up testnets is useful in all sorts of scenarios, so the Altair spec retains genesis functionality, now called initialisation.
The following parameters refer to the actual mainnet beacon chain genesis and I'll explain them in that context. When starting up new testnets, these will of course be changed. For example, see the configuration file for the Prater testnet.
Name | Value |
---|---|
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT |
uint64(2**14) (= 16,384) |
MIN_GENESIS_TIME |
uint64(1606824000) (Dec 1, 2020, 12pm UTC) |
GENESIS_FORK_VERSION |
Version('0x00000000') |
GENESIS_DELAY |
uint64(604800) (7 days) |
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
is the minimum number of full validator stakes that must have been deposited before the beacon chain can start producing blocks. The number is chosen to ensure a degree of security. It allows for four 128 member committees per slot, rather than the 64 committees per slot eventually desired to support fully operational data shards. Fewer validators means higher rewards per validator, so it is designed to attract early participants to get things bootstrapped.
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
used to be much higher (65,536 = 2 million Ether staked), but was reduced when MIN_GENESIS_TIME
, below, was added.
In the actual event of beacon chain genesis, there were 21,063 participating validators, comfortably exceeding the minimum necessary count.
MIN_GENESIS_TIME
MIN_GENESIS_TIME
is the earliest date that the beacon chain can start.
Having a MIN_GENESIS_TIME
allows us to start the chain with fewer validators than was previously thought necessary. The previous plan was to start the chain as soon as there were MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
validators staked. But there were concerns that with a lowish initial validator count, a single entity could form the majority of them and then act to prevent other validators from entering (a "gatekeeper attack"). A minimum genesis time allows time for all those who wish to make deposits to do so before they could be excluded by a gatekeeper attack.
The beacon chain actually started at 12:00:23 UTC on the 1st of December 2020. The extra 23 seconds comes from the timestamp of the first Eth1 block to meet the genesis criteria, block 11320899. I like to think of this as a little remnant of proof of work forever embedded in the beacon chain's history.
GENESIS_FORK_VERSION
Unlike Ethereum 1.0, the beacon chain gives in-protocol versions to its forks. See the Version custom type for more explanation.
GENESIS_FORK_VERSION
is the fork version the beacon chain starts with at its "genesis" event: the point at which the chain first starts producing blocks. In Altair, this value is used only when computing the cryptographic domain for deposit messages, which are valid across all forks.
ALTAIR_FORK_VERSION
is defined elsewhere.
GENESIS_DELAY
The GENESIS_DELAY
is a grace period to allow nodes and node operators time to prepare for the genesis event. The genesis event cannot occur before MIN_GENESIS_TIME
. If there are not MIN_GENESIS_ACTIVE_VALIDATOR_COUNT
registered validators sufficiently in advance of MIN_GENESIS_TIME
, then Genesis will occur GENESIS_DELAY
seconds after enough validators have been registered.
Seven days' notice was regarded as sufficient to allow client dev teams time to make a release once the genesis parameters were known, and for node operators to upgrade to that release. And, of course, to organise some parties. It was increased from 2 days over time due to lessons learned on some of the pre-genesis testnets.
Time parameters
Name | Value | Unit | Duration |
---|---|---|---|
SECONDS_PER_SLOT |
uint64(12) |
seconds | 12 seconds |
SECONDS_PER_ETH1_BLOCK |
uint64(14) |
seconds | 14 seconds |
MIN_VALIDATOR_WITHDRAWABILITY_DELAY |
uint64(2**8) (= 256) |
epochs | ~27 hours |
SHARD_COMMITTEE_PERIOD |
uint64(2**8) (= 256) |
epochs | ~27 hours |
ETH1_FOLLOW_DISTANCE |
uint64(2**11) (= 2,048) |
Eth1 blocks | ~8 hours |
SECONDS_PER_SLOT
This was originally six seconds, but is now twelve, and has been other values in between.
Network delays are the main limiting factor in shortening the slot length. Three communication activities need to be accomplished within a slot, and it is supposed that four seconds is enough for the vast majority of nodes to have participated in each:
- Blocks are proposed at the start of a slot and should have propagated to most of the network within the first four seconds.
- At four seconds into a slot, committee members create and broadcast attestations, including attesting to this slot's block. During the next four seconds, these attestations are collected by aggregators in each committee.
- At eight seconds into the slot, the aggregators broadcast their aggregate attestations which then have four seconds to reach the validator who is proposing the next block.
This slot length has to account for shard blocks as well in later phases. There was some discussion around having the beacon chain and shards on differing cadences, but the latest sharding design tightly couples the beacon chain with the shards. Shard blocks under this design will be much larger, which led to the extension of the slot to 12 seconds.
There is a general intention to shorten the slot time in future, perhaps to 8 seconds, if it proves possible to do this in practice. Or perhaps to lengthen it to 16 seconds.
SECONDS_PER_ETH1_BLOCK
The assumed block interval on the Eth1 chain, used in conjunction with ETH1_FOLLOW_DISTANCE
when considering blocks on the Eth1 chain, either at genesis, or when voting on the deposit contract state.
The average Eth1 block time since January 2020 has actually been nearer 13 seconds, but never mind. The net effect is that we will be going a little deeper back in the Eth1 chain than ETH1_FOLLOW_DISTANCE
would suggest, which ought to be safer.
MIN_VALIDATOR_WITHDRAWABILITY_DELAY
A validator can stop participating once it has made it through the exit queue. However, its funds remain locked for the duration of MIN_VALIDATOR_WITHDRAWABILITY_DELAY
. Initially, this is to allow some time for any slashable behaviour to be detected and reported so that the validator can still be penalised (in which case the validator's withdrawable time is pushed EPOCHS_PER_SLASHINGS_VECTOR
into the future). When data shards are introduced this delay will also allow for shard rewards to be credited and for proof of custody challenges to be mounted.
Note that, for the time being, there is no mechanism to withdraw a validator's balance in any case. Nonetheless, being in a "withdrawable" state means that a validator has now fully exited from the protocol.
SHARD_COMMITTEE_PERIOD
This really anticipates the implementation of data shards. The idea is that it's bad for the stability of longer-lived committees if validators can appear and disappear very rapidly. Therefore, a validator cannot initiate a voluntary exit until SHARD_COMMITTEE_PERIOD
epochs after it is activated. Note that it could still be ejected by slashing before this time.
ETH1_FOLLOW_DISTANCE
This is used to calculate the minimum depth of block on the Ethereum 1 chain that can be considered by the Eth2 chain: it applies to the Genesis process and the processing of deposits by validators. The Eth1 chain depth is estimated by multiplying this value by the target average Eth1 block time, SECONDS_PER_ETH1_BLOCK
.
The value of ETH1_FOLLOW_DISTANCE
is not based on the expected depth of any reorgs of the Eth1 chain, which are rarely if ever more than 2-3 blocks deep. It is about providing time to respond to an incident on the Eth1 chain such as a consensus failure between clients.
This parameter was increased from 1024 to 2048 blocks for the beacon chain mainnet, to allow devs more time to respond if there were any trouble on the Eth1 chain.
Validator Cycle
Name | Value |
---|---|
EJECTION_BALANCE |
Gwei(2**4 * 10**9) (= 16,000,000,000) |
MIN_PER_EPOCH_CHURN_LIMIT |
uint64(2**2) (= 4) |
CHURN_LIMIT_QUOTIENT |
uint64(2**16) (= 65,536) |
EJECTION_BALANCE
If a validator's effective balance falls to 16 Ether or below then it is exited from the system. This is most likely to happen as a result of the "inactivity leak", which gradually reduces the balances of inactive validators in order to maintain the liveness of the beacon chain.
Note that the dependence on effective balance means that the validator is queued for ejection as soon as its actual balance falls to 16.75 Ether.
MIN_PER_EPOCH_CHURN_LIMIT
Validators are allowed to exit the system and cease validating, and new validators may apply to join at any time. For interesting reasons, a design decision was made to apply a rate-limit to entries (activations) and exits. Basically, it is important in proof of stake protocols that the validator set not change too quickly.
In the normal case, a validator is able to exit fairly swiftly: it just needs to wait MAX_SEED_LOOKAHEAD
(currently four) epochs. However, if there are large numbers of validators wishing to exit at the same time, a queue forms with a limited number of exits allowed per epoch. The minimum number of exits per epoch (the minimum "churn") is MIN_PER_EPOCH_CHURN_LIMIT
, so that validators can always eventually exit. The actual allowed churn per epoch is calculated in conjunction with CHURN_LIMIT_QUOTIENT
.
The same applies to new validator activations, once a validator has been marked as eligible for activation.
In concrete terms, this means that up to four validators can enter or exit the active validator set each epoch (900 per day) until we have 327,680 active validators, at which point the limit rises to five.
The rate at which validators can exit is strongly related to the concept of weak subjectivity, and the weak subjectivity period.
CHURN_LIMIT_QUOTIENT
This is used in conjunction with MIN_PER_EPOCH_CHURN_LIMIT
to calculate the actual number of validator exits and activations allowed per epoch. The number of exits allowed is max(MIN_PER_EPOCH_CHURN_LIMIT, n // CHURN_LIMIT_QUOTIENT)
, where n
is the number of active validators. The same applies to activations.
Inactivity penalties
Name | Value | Description |
---|---|---|
INACTIVITY_SCORE_BIAS |
uint64(2**2) (= 4) |
score points per inactive epoch |
INACTIVITY_SCORE_RECOVERY_RATE |
uint64(2**4) (= 16) |
score points per leak-free epoch |
INACTIVITY_SCORE_BIAS
If the beacon chain hasn't finalised an epoch for longer than MIN_EPOCHS_TO_INACTIVITY_PENALTY
epochs, then it enters "leak" mode. In this mode, any validator that does not vote (or votes for an incorrect target) is penalised an amount each epoch of (effective_balance * inactivity_score) // (INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_ALTAIR)
. See INACTIVITY_PENALTY_QUOTIENT_ALTAIR
for discussion of the inactivity leak itself.
The per-validator inactivity-score
is new in Altair. During Phase 0, inactivity penalties were an increasing global amount applied to all validators that did not participate in an epoch, regardless of their individual track records of participation. So a validator that was able to participate for a significant fraction of the time nevertheless could be quite severely penalised due to the growth of the per-epoch inactivity penalty. Vitalik gives a simplified example: "if fully [off]line validators get leaked and lose 40% of their balance, someone who has been trying hard to stay online and succeeds at 90% of their duties would still lose 4% of their balance. Arguably this is unfair."
In addition, if many validators are able to participate intermittently, it indicates that whatever event has befallen the chain is potentially recoverable (unlike a permanent network partition, or a super-majority network fork, for example). The inactivity leak is intended to bring finality to irrecoverable situations, so prolonging the time to finality if it's not irrecoverable is likely a good thing.
With Altair, each validator has an individual inactivity score in the beacon state which is updated by process_inactivity_updates()
as follows.
- Every epoch, irrespective of the inactivity leak,
- decrease the score by one when the validator makes a correct timely target vote, and
- increase the score by
INACTIVITY_SCORE_BIAS
otherwise.
- When not in an inactivity leak
- decrease every validator's score by
INACTIVITY_SCORE_RECOVERY_RATE
.
- decrease every validator's score by
There is a floor of zero on the score. So, outside a leak, validators' scores will rapidly return to zero and stay there, since INACTIVITY_SCORE_RECOVERY_RATE
is greater than INACTIVITY_SCORE_BIAS
.
When in a leak, if is the participation rate between and , and is INACTIVITY_SCORE_BIAS
, then the expected score after epochs is . For this is . So a validator that is participating 80% of the time or more can maintain a score that is bounded near zero. With less than 80% average participation, its score will increase unboundedly.
INACTIVITY_SCORE_RECOVERY_RATE
When not in an inactivity leak, validators' inactivity scores are reduced by INACTIVITY_SCORE_RECOVERY_RATE
+
1
per epoch when they make a timely target vote, and by INACTIVITY_SCORE_RECOVERY_RATE
-
INACTIVITY_SCORE_BIAS
when they don't. So, even for non-performing validators, scores decrease three times faster than they increase.
The new scoring system means that some validators will continue to be penalised due to the leak, even after finalisation starts again. This is intentional. When the leak causes the beacon chain to finalise, at that point we have just 2/3 of the stake online. If we immediately stop the leak (as we used to), then the amount of stake online would remain close to 2/3 and the chain would be vulnerable to flipping in and out of finality as small numbers of validators come and go. We saw this behaviour on some of the testnets prior to launch. Continuing the leak after finalisation serves to increase the balances of participating validators to greater than 2/3, providing a margin that should help to prevent such behaviour.
See the section on the Inactivity Leak for some more analysis of the inactivity score and some graphs of its effect.
Containers
Preamble
We are about to see our first Python code in the executable spec. For specification purposes, these Container data structures are just Python data classes that are derived from the base SSZ Container
class.
SSZ is the serialisation and Merkleization format used everywhere in Ethereum 2.0. It is not self-describing, so you need to know ahead of time what you are unpacking when deserialising. SSZ deals with basic types and composite types. Classes like the below are handled as SSZ containers, a composite type defined as an "ordered heterogeneous collection of values".
Client implementations in different languages will obviously use their own paradigms to represent these data structures.
Two notes directly from the spec:
- The definitions are ordered topologically to facilitate execution of the spec.
- Fields missing in container instantiations default to their zero value.
Misc dependencies
Fork
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.
ForkData
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.
Checkpoint
class Checkpoint(Container):
epoch: Epoch
root: Root
Checkpoint
s 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 BeaconState
.
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.
Thus, the 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.
Validator
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 (every epoch): 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 Validator
:
pubkey
serves 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.- Validators actually have two private/public key pairs, the one above used for signing protocol messages, and a separate "withdrawal key".
withdrawal_credentials
is a commitment generated from the validator's withdrawal key so that, at some time in the future, a validator can prove it owns the funds and will be able to withdraw them. There are two types of withdrawal credential currently defined, one corresponding to BLS keys, and one corresponding to standard Ethereum ECDSA keys. effective_balance
is a topic of its own that we've touched upon already, and will discuss more fully when we look at effective balances updates.slashed
indicates 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
FAR_FUTURE_EPOCH
. - At the end of the current epoch, the
activation_eligibility_epoch
is set to the next epoch. - After the epoch
activation_eligibility_epoch
has been finalised, the validator is added to the activation queue by setting itsactivation_epoch
appropriately, taking into account the per-epoch churn limit andMAX_SEED_LOOKAHEAD
. - On reaching
activation_epoch
the validator becomes active, and should carry out its duties. - At any time after
SHARD_COMMITTEE_PERIOD
epochs have passed, a validator may request a voluntary exit.exit_epoch
is set according to the validator's position in the exit queue andMAX_SEED_LOOKAHEAD
, andwithdrawable_epoch
is setMIN_VALIDATOR_WITHDRAWABILITY_DELAY
epochs after that. - From
exit_epoch
onward the validator is no longer active. There is no mechanism for exited validators to rejoin: exiting is permanent. - After
withdrawable_epoch
, the validator's balance can in principle be withdrawn, although there is no mechanism for doing this for the time being.
The above does not account for slashing or forced exits due to low balance.
AttestationData
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_root
is 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.source
is the validator's opinion of the best currently justified checkpoint for the Casper FFG finalisation process.target
is the validator's opinion of the block at the start of the current epoch, also for Casper FFG finalisation.
This AttestationData
structure gets wrapped up into several other similar but distinct structures:
Attestation
is 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.IndexedAttestation
is 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,PendingAttestation
s were stored in the beacon state pending processing at the end of the epoch. This was reworked in the Altair upgrade andPendingAttestation
s are no longer used.
IndexedAttestation
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.
Attestation
s and IndexedAttestation
s contain essentially the same information. The difference being that the list of attesting validators is stored uncompressed in IndexedAttestation
s. 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.
IndexedAttestation
s are primarily used when reporting attester slashing. An Attestation
can be converted to an IndexedAttestation
using get_indexed_attestation()
.
PendingAttestation
class PendingAttestation(Container):
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
data: AttestationData
inclusion_delay: Slot
proposer_index: ValidatorIndex
PendingAttestation
s 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, Attestation
s received in blocks were verified then temporarily stored in beacon state in the form of PendingAttestation
s, pending further processing at the end of the epoch.
A PendingAttestation
is an Attestation
minus the signature, plus a couple of fields related to reward calculation:
inclusion_delay
is 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_index
is 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).
With Altair, this whole mechanism has been replaced by ParticipationFlags
.
Eth1Data
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: EPOCHS_PER_ETH1_VOTING_PERIOD
*
SLOTS_PER_EPOCH
= 64 *
32 = 2048.
deposit_root
is the result of theget_deposit_root()
method of the Eth1 deposit contract after executing the Eth1 block being voted on - it's the root of the (sparse) Merkle tree of deposits.deposit_count
is the number of deposits in the deposit contract at that point, the result of theget_deposit_count
method on the contract. This will be equal to or greater than (if there are pending unprocessed deposits) the value ofstate.eth1_deposit_index
.block_hash
is 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.
HistoricalBatch
class HistoricalBatch(Container):
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
This is used to implement part of the double batched accumulator for the past history of the chain. Once SLOTS_PER_HISTORICAL_ROOT
block roots and the same number of state roots have been accumulated in the beacon state, they are put in a HistoricalBatch
object and the hash tree root of that is appended to the historical_roots
list in beacon state. The corresponding block and state root lists in the beacon state are circular and just get overwritten in the next period. See process_historical_roots_update()
.
DepositMessage
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 amount
.
See the Validator
container for more on withdrawal_credentials
.
There are two protections that DepositMessages
get at different points.
DepositData
is included in beacon blocks as aDeposit
, 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 theDepositMessage
can be verified before a validator is added.
DepositData
class DepositData(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32
amount: Gwei
signature: BLSSignature # Signing over DepositMessage
A signed 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 DepositData
.
BeaconBlockHeader
class BeaconBlockHeader(Container):
slot: Slot
proposer_index: ValidatorIndex
parent_root: Root
state_root: Root
body_root: Root
A standalone version of a beacon block header: BeaconBlock
s contain their own header. It is identical to BeaconBlock
, except that body
is replaced by body_root
. It is BeaconBlock
-lite.
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 predecessor4. See process_block_header()
.
The signed version is used in proposer slashings.
SyncCommittee
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 get_next_sync_committee()
.
Including the 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.
See also SYNC_COMMITTEE_SIZE
.
SigningData
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.
Beacon operations
The following are the various protocol messages that can be transmitted in a block` on the beacon chain.
For most of these, the proposer is rewarded either explicitly or implicitly for including the object in a block.
The proposer receives explicit in-protocol rewards for including the following in blocks:
ProposerSlashing
s,AttesterSlashing
s,Attestation
s, andSyncAggregate
s.
Including Deposit
objects in blocks is only implicitly rewarded, in that, if there are pending deposits that the block proposer does not include then the block is invalid, so the proposer receives no reward.
There is no direct reward for including VoluntaryExit
objects. However, for each validator exited, rewards for the remaining validators increase very slightly, so it's still beneficial for proposers not to ignore VoluntaryExit
s.
ProposerSlashing
class ProposerSlashing(Container):
signed_header_1: SignedBeaconBlockHeader
signed_header_2: SignedBeaconBlockHeader
ProposerSlashing
s may be included in blocks to prove that a validator has broken the rules and ought to be slashed. Proposers receive a reward for correctly submitting these.
In this case, the rule is that a validator may not propose two different blocks at the same height, and the payload is the signed headers of the two blocks that evidence the crime. The signatures on the SignedBeaconBlockHeader
s are checked to verify that they were both signed by the accused validator.
AttesterSlashing
class AttesterSlashing(Container):
attestation_1: IndexedAttestation
attestation_2: IndexedAttestation
AttesterSlashing
s may be included in blocks to prove that one or more validators in a committee has broken the rules and ought to be slashed. Proposers receive a reward for correctly submitting these.
The contents of the IndexedAttestation
s are checked against the attester slashing conditions in is_slashable_attestation_data()
. If there is a violation, then any validator that attested to both attestation_1
and attestation_2
is slashed, see process_attester_slashing()
.
AttesterSlashing
s can be very large since they could in principle list the indices of all the validators in a committee. However, in contrast to proposer slashings, many validators can be slashed as a result of a single report.
Attestation
class Attestation(Container):
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
data: AttestationData
signature: BLSSignature
This is the form in which attestations make their way around the network. It is designed to be easily aggregatable: Attestations
containing identical AttestationData
can be combined into a single attestation by aggregating the signatures.
Attestation
s contain the same information as IndexedAttestation
s, but use knowledge of the validator committees at slots to compress the list of attesting validators down to a bitlist. Thus, Attestation
s are at least 5 times smaller than IndexedAttestation
s, and up to 35 times smaller (with 128 or 2048 validators per committee, respectively).
When a validator first broadcasts its attestation to the network, the aggregation_bits
list will contain only a single bit set, and calling get_attesting_indices()
on it will return a list containing only a single entry, the validator's own index.
Deposit
class Deposit(Container):
proof: Vector[Bytes32, DEPOSIT_CONTRACT_TREE_DEPTH + 1] # Merkle path to deposit root
data: DepositData
This container is used to include deposit data from prospective validators in beacon blocks so that they can be processed into beacon state.
The proof
is a Merkle proof constructed by the block proposer that the DepositData
corresponds to the previously agreed deposit root of the Eth1 contract's deposit tree. It is verified in process_deposit()
by is_valid_merkle_branch()
.
VoluntaryExit
class VoluntaryExit(Container):
epoch: Epoch # Earliest epoch when voluntary exit can be processed
validator_index: ValidatorIndex
Voluntary exit messages are how a validator signals that it wants to cease being a validator. Blocks containing VoluntaryExit
data for an epoch later than the current epoch are invalid, so nodes should buffer or ignore any future-dated exits they see.
VoluntaryExit
objects are never used naked; they are always wrapped up into a SignedVoluntaryExit
object.
SyncAggregate
class SyncAggregate(Container):
sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE]
sync_committee_signature: BLSSignature
The prevailing sync committee is stored in the beacon state, so the SyncAggregate
s included in blocks need only use a bit vector to indicate which committee members signed off on the message.
The sync_committee_signature
is the aggregate signature of all the validators referenced in the bit vector over the block root of the previous slot.
SyncAggregate
s are handled by process_sync_aggregate()
.
Beacon blocks
BeaconBlockBody
class BeaconBlockBody(Container):
randao_reveal: BLSSignature
eth1_data: Eth1Data # Eth1 data vote
graffiti: Bytes32 # Arbitrary data
# Operations
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
attestations: List[Attestation, MAX_ATTESTATIONS]
deposits: List[Deposit, MAX_DEPOSITS]
voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
sync_aggregate: SyncAggregate # [New in Altair]
The two fundamental data structures for nodes are the BeaconBlock
and the BeaconState
. The BeaconBlock
is how the leader (the chosen proposer in a slot) communicates network updates to all the other validators, and those validators update their own BeaconState
by applying BeaconBlock
s. The idea is that (eventually) all validators on the network come to agree on the same BeaconState
.
Validators are randomly selected to propose beacon blocks, and there ought to be exactly one beacon block per slot if things are running correctly. If a validator is offline, or misses its slot, or proposes an invalid block, or has its block orphaned, then a slot can be empty.
The following objects are always present in a valid beacon block.
randao_reveal
: the block is invalid if the RANDAO reveal does not verify correctly against the proposer's public key. This is the block proposer's contribution to the beacon chain's randomness. The proposer generates it by signing the current epoch number (combined withDOMAIN_RANDAO
) with its private key. To the best of anyone's knowledge, the result is indistinguishable from random. This gets mixed into the beacon state RANDAO.- See Eth1Data for
eth1_data
. In principle, this is mandatory, but it is not checked, and there is no penalty for making it up. graffiti
is left free for the proposer to insert whatever data it wishes. It has no protocol level significance. It can be left as zero; most clients set the client name and version string as their own default graffiti value.sync_aggregate
is a record of which validators in the current sync committee voted for the chain head in the previous slot.
Deposits are a special case. They are mandatory only if there are pending deposits to be processed. There is no explicit reward for including deposits, except that a block is invalid without any that ought to be there.
deposits
: if the block does not contain either all the outstandingDeposit
s, orMAX_DEPOSITS
of them in deposit order, then it is invalid.
Including any of the remaining objects is optional. They are handled, if present, in the process_operations()
function.
The proposer earns rewards for including any of the following. Rewards for attestations and sync aggregates are available every slot. Slashings, however, are very rare.
proposer_slashings
: up toMAX_PROPOSER_SLASHINGS
ProposerSlashing
objects may be included.attester_slashings
: up toMAX_ATTESTER_SLASHINGS
AttesterSlashing
objects may be included.attestations
: up toMAX_ATTESTATIONS
(aggregated)Attestation
objects may be included. The block proposer is incentivised to include well-packed aggregate attestations, as it receives a micro reward for each unique attestation. In a perfect world, with perfectly aggregated attestations,MAX_ATTESTATIONS
would be equal toMAX_COMMITTEES_PER_SLOT
; in our configuration it is double. This provides capacity in blocks to catch up with attestations after skip slots, and also room to include some imperfectly aggregated attestations.
Including voluntary exits is optional, and there are no explicit rewards for including them.
voluntary_exits
: up toMAX_VOLUNTARY_EXITS
SignedVoluntaryExit
objects may be included.
BeaconBlock
class BeaconBlock(Container):
slot: Slot
proposer_index: ValidatorIndex
parent_root: Root
state_root: Root
body: BeaconBlockBody
BeaconBlock
just adds some blockchain paraphernalia to BeaconBlockBody
. It is identical to BeaconBlockHeader
, except that the body_root
is replaced by the actual block body
.
slot
is the slot the block is proposed for.
proposer_index
was added to avoid a potential DoS vector, and to allow clients without full access to the state to still know useful things.
parent_root
is used to make sure that this block is a direct child of the last block we processed.
In order to calculate state_root
, the proposer is expected to run the state transition with the block before propagating it. After the beacon node has processed the block, the state roots are compared to ensure they match. This is the mechanism for tying the whole system together and making sure that all validators and beacon nodes are always working off the same version of state (absent any short-term forks).
If any of these is incorrect, then the block is invalid with respect to the current beacon state and will be ignored.
Beacon state
BeaconState
class BeaconState(Container):
# Versioning
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork
# History
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
# Eth1
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64
# Registry
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
# Randomness
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
# Slashings
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances
# Participation
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [Modified in Altair]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [Modified in Altair]
# Finality
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
# Inactivity
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] # [New in Altair]
# Sync
current_sync_committee: SyncCommittee # [New in Altair]
next_sync_committee: SyncCommittee # [New in Altair]
All roads lead to the BeaconState
. Maintaining this data structure is the sole purpose of all the apparatus in all of the spec documents. This state is the focus of consensus among the beacon nodes; it is what everybody, eventually, must agree on.
The beacon chain's state is monolithic: everything is bundled into a single state object (sometimes referred to as the "God object"). Some have argued for more granular approaches that might be more efficient, but at least the current approach is simple.
Let's break this thing down.
# Versioning
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork
How do we know which chain we're on, and where we are on it? The information here ought to be sufficient. A continuous path back to the genesis block would also suffice.
genesis_validators_root
is calculated at Genesis time (when the chain starts) and is fixed for the life of the chain. This, combined with the fork
identifier, should serve to uniquely identify the chain that we are on.
The fork choice rule uses genesis_time
to work out what slot we're in.
The fork
object is manually updated as part of beacon chain upgrades, also called hard forks. This invalidates blocks and attestations from validators not following the new fork.
# History
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
latest_block_header
is only used to make sure that the next block we process is a direct descendent of the previous block. It's a blockchain thing.
Past block_roots
and state_roots
are stored in lists here until the lists are full. Once they are full, the Merkle root is taken of both the lists together and appended to historical_roots
. historical_roots
effectively grows without bound (HISTORICAL_ROOTS_LIMIT
is large), but at a rate of only 10KB per year. Keeping this data is useful for light clients, and also allows Merkle proofs to be created against past states, for example historical deposit data.
# Eth1
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64
eth1_data
is the latest agreed upon state of the Eth1 chain and deposit contract. eth1_data_votes
accumulates Eth1Data
from blocks until there is an overall majority in favour of one Eth1 state. If a majority is not achieved by the time the list is full then it is cleared down and voting starts again from scratch. eth1_deposit_index
is the total number of deposits that have been processed by the beacon chain (which is greater than or equal to the number of validators, as a deposit can top-up the balance of an existing validator).
# Registry
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
The registry of Validator
s and their balances. The balances
list is separated out as it changes much more frequently than the validators
list. Roughly speaking, balances of active validators are updated every epoch, while the validators
list has only minor updates per epoch. When combined with SSZ tree hashing, this results in a big saving in the amount of data to be rehashed on registry updates. See also validator inactivity scores under Inactivity which we treat similarly.
# Randomness
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
Past randao mixes are stored in a fixed-size circular list for EPOCHS_PER_HISTORICAL_VECTOR
epochs (~290 days). These can be used to recalculate past committees, which allows slashing of historical attestations. See EPOCHS_PER_HISTORICAL_VECTOR
for more information.
# Slashings
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]
A fixed-size circular list of past slashed amounts. Each epoch, the total effective balance of all validators slashed in that epoch is stored as an entry in this list. When the final slashing penalty for a slashed validator is calculated, it is weighted with the sum of this list. This mechanism is designed to less heavily penalise one-off slashings that are most likely accidental, and more heavily penalise mass slashings during a window of time, which are more likely to be a coordinated attack.
# Participation
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [Modified in Altair]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] # [Modified in Altair]
These lists record which validators participated in attesting during the current and previous epochs by recording flags for timely votes for the correct source, the correct target and the correct head. We store two epochs' worth since Validators have up to 32 slots to include a correct target vote. The flags are used to calculate finality and to assign rewards at the end of epochs.
Previously, during Phase 0, we stored two epochs' worth of actual attestations in the state and processed them en masse at the end of epochs. This was slow, and was thought to be contributing to observed late block production in the first slots of epochs. The change to the new scheme was implemented in the Altair upgrade under the title of Accounting Reforms.
# Finality
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
Ethereum 2.0 uses the Casper FFG finality mechanism, with a k-finality optimisation, where k = 2. The above objects in the state are the data that need to be tracked in order to apply the finality rules.
justification_bits
is only four bits long. It tracks the justification status of the last four epochs: 1 if justified, 0 if not. This is used when calculating whether we can finalise an epoch.- Outside of the finality calculations,
previous_justified_checkpoint
andcurrent_justified_checkpoint
are used to filter attestations: valid blocks include only attestations with a source checkpoint that matches the justified checkpoint in the state for the attestation's epoch. finalized_checkpoint
: the network has agreed that the beacon chain state at or before that epoch will never be reverted. So, for one thing, the fork choice rule doesn't need to go back any further than this. The Casper FFG mechanism is specifically constructed so that two conflicting finalized checkpoints cannot be created without at least one third of validators being slashed.
# Inactivity
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] # [New in Altair]
This is logically part of "Registry", above, and would be better placed there. It is a per-validator record of inactivity scores that is updated every epoch. This list is stored outside the main list of Validator objects since it is updated very frequently. See the Registry for more explanation.
# Sync
current_sync_committee: SyncCommittee # [New in Altair]
next_sync_committee: SyncCommittee # [New in Altair]
Sync committees were introduced in the Altair upgrade. The next sync committee is calculated and stored so that participating validators can prepare in advance by subscribing to the required p2p subnets.
Historical Note
There was a period during which beacon state was split into "crystallized state" and "active state". The active state was constantly changing; the crystallized state changed only once per epoch (or what passed for epochs back then). Separating out the fast-changing state from the slower-changing state was an attempt to avoid having to constantly rehash the whole state every slot. With the introduction of SSZ tree hashing, this was no longer necessary, as the roots of the slower changing parts could simply be cached, which was a nice simplification. There remains an echo of this approach, however, in the splitting out of validator balances and inactivity scores into different structures withing the beacon state.
Signed envelopes
The following are just wrappers for more basic types, with an added signature.
SignedVoluntaryExit
class SignedVoluntaryExit(Container):
message: VoluntaryExit
signature: BLSSignature
A voluntary exit is currently signed with the validator's online signing key.
There has been some discussion about changing this to also allow signing of a voluntary exit with the validator's offline withdrawal key. The introduction of multiple types of withdrawal credential makes this more complex, however, and it is no longer likely to be practical.
SignedBeaconBlock
class SignedBeaconBlock(Container):
message: BeaconBlock
signature: BLSSignature
BeaconBlock
s are signed by the block proposer and unwrapped for block processing.
This signature is what makes proposing a block "accountable". If two correctly signed conflicting blocks turn up, the signatures guarantee that the same proposer produced them both, and is thus subject to being slashed. This is also why stakers need to closely guard their signing keys.
SignedBeaconBlockHeader
class SignedBeaconBlockHeader(Container):
message: BeaconBlockHeader
signature: BLSSignature
This is used only when reporting proposer slashing, within a ProposerSlashing
object.
Through the magic of SSZ hash tree roots, a valid signature for a SignedBeaconBlock
is also a valid signature for a SignedBeaconBlockHeader
. Proposer slashing makes use of this to save space in slashing reports.
Helper Functions
Preamble
Note: The definitions below are for specification purposes and are not necessarily optimal implementations.
This note in the spec is super important for implementers! There are many, many optimisations of the below routines that are being used in practice; a naive implementation would be impractically slow for mainnet configurations. As long as the optimised code produces identical results to the code here, then all is fine.
Math
integer_squareroot
def integer_squareroot(n: uint64) -> uint64:
"""
Return the largest integer ``x`` such that ``x**2 <= n``.
"""
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
Validator rewards scale with the reciprocal of the square root of the total active balance of all validators. This is calculated in get_base_reward_per_increment()
.
In principle integer_squareroot
is also used in get_attestation_participation_flag_indices()
, to specify the maximum delay for source votes to receive a reward. But this is just the constant, integer_squareroot(SLOTS_PER_EPOCH)
, which is 5
.
Newton's method is used which has pretty good convergence properties, but implementations may use any method that gives identical results.
Used by | get_base_reward_per_increment() , get_attestation_participation_flag_indices() |
xor
def xor(bytes_1: Bytes32, bytes_2: Bytes32) -> Bytes32:
"""
Return the exclusive-or of two 32-byte strings.
"""
return Bytes32(a ^ b for a, b in zip(bytes_1, bytes_2))
The bitwise xor
of two 32-byte quantities is defined here in Python terms.
This is used only in process_randao()
when mixing in the new randao reveal.
Fun fact: if you xor
two byte
types in Java, the result is a 32 bit (signed) integer. This is one reason we need to define the "obvious" here. But mainly, because the spec is executable, we need to tell Python what it doesn't already know.
Used by | process_randao() |
uint_to_bytes
def uint_to_bytes(n: uint) -> bytes
is a function for serializing theuint
type object to bytes inENDIANNESS
-endian. The expected length of the output is the byte-length of theuint
type.
For the most part, integers are integers and bytes are bytes, and they don't mix much. But there are a few places where we need to convert from integers to bytes:
- several times in the
compute_shuffled_index()
algorithm; - in
compute_proposer_index()
for selecting a proposer weighted by stake; - in
get_seed()
to mix the epoch number into the randao mix; - in
get_beacon_proposer_index()
to mix the slot number into the per-epoch randao seed; and - in
get_next_sync_committee_indices()
.
You'll note that in every case, the purpose of the conversion is for the integer to form part of a byte string that is hashed to create (pseudo-)randomness.
The result of this conversion is dependent on our arbitrary choice of endianness, that is, how we choose to represent integers as strings of bytes. For Eth2, we have chosen little-endian: see the discussion of ENDIANNESS
for more background.
The uint_to_bytes()
function is not given an explicit implementation in the specification, which is unusual. This to avoid exposing the innards of the Python SSZ implementation (of uint
) to the rest of the spec. When running the spec as an executable, it uses the definition in the SSZ utilities.
Used by | compute_shuffled_index() , compute_proposer_index() , get_seed() , get_beacon_proposer_index() , get_next_sync_committee_indices() |
See also | ENDIANNESS , SSZ utilities |
bytes_to_uint64
def bytes_to_uint64(data: bytes) -> uint64:
"""
Return the integer deserialization of ``data`` interpreted as ``ENDIANNESS``-endian.
"""
return uint64(int.from_bytes(data, ENDIANNESS))
bytes_to_uint64()
is the inverse of uint_to_bytes()
, and is used by the shuffling algorithm to create a random index from the output of a hash.
It is also used in the validator specification when selecting validators to aggregate attestations, and sync committee messages.
int.from_bytes
is a built-in Python 3 method. The uint64
cast is provided by the spec's SSZ implementation.
Used by | compute_shuffled_index |
See also | attestation aggregator selection, sync committee aggregator selection |
Crypto
hash
def hash(data: bytes) -> Bytes32
is SHA256.
SHA256 was chosen as the protocol's base hash algorithm for easier cross-chain interoperability: many other chains use SHA256, and Eth1 has a SHA256 precompile.
There was a lot of discussion about this choice early in the design process. The original plan had been to use the BLAKE2b-512 hash function – that being a modern hash function that's faster than SHA3 – and to move to a STARK/SNARK friendly hash function at some point (such as MiMC). However, to keep interoperability with Eth1, in particular for the implementation of the deposit contract, the hash function was changed to Keccak256. Finally, we settled on SHA256 as having even broader compatibility.
The hash function serves two purposes within the protocol. The main use, computationally, is in Merkleization, the computation of hash tree roots, which is ubiquitous in the protocol. Its other use is to harden the randomness used in various places.
Used by | hash_tree_root , is_valid_merkle_branch() , compute_shuffled_index() , compute_proposer_index() , get_seed() , get_beacon_proposer_index() , get_next_sync_committee_indices() , process_randao() |
hash_tree_root
def hash_tree_root(object: SSZSerializable) -> Root
is a function for hashing objects into a single root by utilizing a hash tree structure, as defined in the SSZ spec.
The development of the tree hashing process was transformational for the Ethereum 2.0 specification, and it is now used everywhere.
The naive way to create a digest of a data structure is to serialise it and then just run a hash function over the result. In tree hashing, the basic idea is to treat each element of an ordered, compound data structure as the leaf of a Merkle tree, recursively if necessary until a primitive type is reached, and to return the Merkle root of the resulting tree.
At first sight, this all looks quite inefficient. Twice as much data needs to be hashed when tree hashing, and actual speeds are 4-6 times slower compared with the linear hash. However, it is good for supporting light clients, because it allows Merkle proofs to be constructed easily for subsets of the full state.
The breakthrough insight was realising that much of the re-hashing work can be cached: if part of the state data structure has not changed, that part does not need to be re-hashed: the whole subtree can be replaced with its cached hash. This turns out to be a huge efficiency boost, allowing the previous design, with cumbersome separate crystallised and active state, to be simplified into a single state object.
Merkleization, the process of calculating the hash_tree_root()
of an object, is defined in the SSZ specification, and explained further in the section on SSZ.
BLS signatures
See the main write-up on BLS Signatures for a more in-depth exploration of this topic.
The IETF BLS signature draft standard v4 with ciphersuite
BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_
defines the following functions:
def Sign(privkey: int, message: Bytes) -> BLSSignature
def Verify(pubkey: BLSPubkey, message: Bytes, signature: BLSSignature) -> bool
def Aggregate(signatures: Sequence[BLSSignature]) -> BLSSignature
def FastAggregateVerify(pubkeys: Sequence[BLSPubkey], message: Bytes, signature: BLSSignature) -> bool
def AggregateVerify(pubkeys: Sequence[BLSPubkey], messages: Sequence[Bytes], signature: BLSSignature) -> bool
def KeyValidate(pubkey: BLSPubkey) -> bool
The above functions are accessed through the
bls
module, e.g.bls.Verify
.
The detailed specification of the cryptographic functions underlying Ethereum 2.0's BLS signing scheme is delegated to the draft IETF standard as described in the spec. This includes specifying the elliptic curve BLS12-381 as our domain of choice.
Our intention in conforming to the in-progress standard is to provide for maximal interoperability with other chains, applications, and cryptographic libraries. Ethereum Foundation researchers and Eth2 developers had input to the development of the standard. Nevertheless, there were some challenges involved in trying to keep up as the standard evolved. For example, the Hashing to Elliptic Curves standard was still changing rather late in the beacon chain testing phase. In the end, everything worked out fine.
The following two functions are described in the separate BLS Extensions document, but included here for convenience.
eth_aggregate_pubkeys
def eth_aggregate_pubkeys(pubkeys: Sequence[BLSPubkey]) -> BLSPubkey:
"""
Return the aggregate public key for the public keys in ``pubkeys``.
NOTE: the ``+`` operation should be interpreted as elliptic curve point addition, which takes as input
elliptic curve points that must be decoded from the input ``BLSPubkey``s.
This implementation is for demonstrative purposes only and ignores encoding/decoding concerns.
Refer to the BLS signature draft standard for more information.
"""
assert len(pubkeys) > 0
# Ensure that the given inputs are valid pubkeys
assert all(bls.KeyValidate(pubkey) for pubkey in pubkeys)
result = copy(pubkeys[0])
for pubkey in pubkeys[1:]:
result += pubkey
return result
Stand-alone aggregation of public keys is not defined by the BLS signature standard. In the standard, public keys are aggregated only in the context of performing an aggregate signature verification via AggregateVerify()
or FastAggregateVerify()
.
The eth_aggregate_pubkeys()
function was added in the Altair upgrade to implement an optimisation for light clients when verifying the signatures on SyncAggregate
s.
Used by | get_next_sync_committee() |
Uses | bls.KeyValidate() |
eth_fast_aggregate_verify
def eth_fast_aggregate_verify(pubkeys: Sequence[BLSPubkey], message: Bytes32, signature: BLSSignature) -> bool:
"""
Wrapper to ``bls.FastAggregateVerify`` accepting the ``G2_POINT_AT_INFINITY`` signature when ``pubkeys`` is empty.
"""
if len(pubkeys) == 0 and signature == G2_POINT_AT_INFINITY:
return True
return bls.FastAggregateVerify(pubkeys, message, signature)
The specification of FastAggregateVerify()
in the BLS signature standard returns INVALID
if there are zero public keys given.
This function was introduced in Altair to handle SyncAggregate
s that no sync committee member had signed off on, in which case the G2_POINT_AT_INFINITY
can be considered a "correct" signature (in our case, but not according to the standard).
The networking and validator specs were later clarified to require that SyncAggregates
have at least one signature. But this requirement is not enforced in the consensus layer (in process_sync_aggregate()
), so we need to retain this eth_fast_aggregate_verify()
wrapper to allow the empty signature to be valid.
Used by | process_sync_aggregate() |
Uses | FastAggregateVerify() |
See also | G2_POINT_AT_INFINITY |
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 been 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 of 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. It is a consensus failure if there is a mismatch, perhaps due to one client considering a deposit valid while another considers it invalid for some reason.
Used by | process_deposit() |
Misc
compute_shuffled_index
def compute_shuffled_index(index: uint64, index_count: uint64, seed: Bytes32) -> uint64:
"""
Return the shuffled index corresponding to ``seed`` (and ``index_count``).
"""
assert index < index_count
# Swap or not (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf)
# See the 'generalized domain' algorithm on page 3
for current_round in range(SHUFFLE_ROUND_COUNT):
pivot = bytes_to_uint64(hash(seed + uint_to_bytes(uint8(current_round)))[0:8]) % index_count
flip = (pivot + index_count - index) % index_count
position = max(index, flip)
source = hash(
seed
+ uint_to_bytes(uint8(current_round))
+ uint_to_bytes(uint32(position // 256))
)
byte = uint8(source[(position % 256) // 8])
bit = (byte >> (position % 8)) % 2
index = flip if bit else index
return index
Selecting random, distinct committees of validators is a big part of Ethereum 2.0; it is foundational for both its scalability and security. This selection is done by shuffling.
Shuffling a list of objects is a well understood problem in computer science. Notice, however, that this routine manages to shuffle a single index to a new location, knowing only the total length of the list. To use the technical term for this, it is oblivious. To shuffle the whole list, this routine needs to be called once per validator index in the list. By construction, each input index maps to a distinct output index. Thus, when applied to all indices in the list, it results in a permutation, also called a shuffling.
Why do this rather than a simpler, more efficient, conventional shuffle? It's all about light clients. Beacon nodes will generally need to know the whole shuffling, but light clients will often be interested only in a small number of committees. Using this technique allows the composition of a single committee to be calculated without having to shuffle the entire set: potentially a big saving on time and memory.
As stated in the code comments, this is an implementation of the "swap-or-not" shuffle, described in the cited paper. Vitalik kicked off a search for a shuffle with these properties in late 2018. With the help of Professor Dan Boneh of Stanford University, the swap-or-not was identified as a candidate a couple of months later, and adopted into the spec.
The algorithm breaks down as follows. For each iteration (each round), we start with a current index
.
- Pseudo-randomly select a pivot. This is a 64-bit integer based on the seed and current round number. This domain is large enough that any non-uniformity caused by taking the modulus in the next step is entirely negligible.
- Use
pivot
to find another index in the list of validators,flip
, which ispivot - index
accounting for wrap-around in the list. - Calculate a single pseudo-random bit based on the seed, the current round number, and some bytes from either
index
orflip
depending on which is greater. - If our bit is zero, we keep
index
unchanged; if it is one, we setindex
toflip
.
We are effectively swapping cards in a deck based on a deterministic algorithm.
The way that position
is broken down is worth noting:
- Bits 0-2 (3 bits) are used to select a single bit from the eight bits of
byte
. - Bits 3-7 (5 bits) are used to select a single byte from the thirty-two bytes of
source
. - Bits 8-39 (32 bits) are used in generating
source
. Note that the upper two bytes of this will always be zero in practice, due to limits on the number of active validators.
SHUFFLE_ROUND_COUNT
is, and always has been, 90 in the mainnet configuration, as explained there.
See the section on Shuffling for a more structured exposition and analysis of this algorithm (with diagrams!).
In practice, full beacon node implementations will run this once per epoch using an optimised version that shuffles the whole list, and cache the result of that for the epoch.
Used by | compute_committee() , compute_proposer_index() , get_next_sync_committee_indices() |
Uses | bytes_to_uint64() |
See also | SHUFFLE_ROUND_COUNT |
compute_proposer_index
def compute_proposer_index(state: BeaconState, indices: Sequence[ValidatorIndex], seed: Bytes32) -> ValidatorIndex:
"""
Return from ``indices`` a random index sampled by effective balance.
"""
assert len(indices) > 0
MAX_RANDOM_BYTE = 2**8 - 1
i = uint64(0)
total = uint64(len(indices))
while True:
candidate_index = indices[compute_shuffled_index(i % total, total, seed)]
random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32]
effective_balance = state.validators[candidate_index].effective_balance
if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte:
return candidate_index
i += 1
There is exactly one beacon block proposer per slot, selected randomly from among all the active validators. The seed parameter is set in get_beacon_proposer_index
based on the epoch and slot. Note that there is a small but finite probability of the same validator being called on to propose a block more than once in an epoch.
A validator's chance of being the proposer is weighted by its effective balance: a validator with a 32 Ether effective balance is twice as likely to be chosen as a validator with a 16 Ether effective balance.
To account for the need to weight by effective balance, this function implements as a try-and-increment algorithm. A counter i
starts at zero. This counter does double duty:
- First
i
is used to uniformly select a candidate proposer with probability where, is the number of active validators. This is done by using thecompute_shuffled_index
routine to shuffle indexi
to a new location, which is then thecandidate_index
. - Then
i
is used to generate a pseudo-random byte using the hash function as a seeded PRNG with at least 256 bits of output. The lower 5 bits ofi
select a byte in the hash function, and the upper bits salt the seed. (An obvious optimisation is that the output of the hash changes only once every 32 iterations.)
The if
test is where the weighting by effective balance is done. If the candidate has MAX_EFFECTIVE_BALANCE
, it will always pass this test and be returned as the proposer. If the candidate has a fraction of MAX_EFFECTIVE_BALANCE
then that fraction is the probability of being returned as proposer.
If the candidate is not chosen, then i
is incremented and we try again. Since the minimum effective balance is half of the maximum, then this ought to terminate fairly swiftly. In the worst case, all validators have 16 Ether effective balance and the chance of having to do another iteration is 50%, in which case there is a one in a million chance of having to do 20 iterations.
Note that this dependence on the validators' effective balances, which are updated at the end of each epoch, means that proposer assignments are valid only in the current epoch. This is different from attestation committee assignments, which are valid with a one epoch look-ahead.
Used by | get_beacon_proposer_index() |
Uses | compute_shuffled_index() |
See also | MAX_EFFECTIVE_BALANCE |
compute_committee
def compute_committee(indices: Sequence[ValidatorIndex],
seed: Bytes32,
index: uint64,
count: uint64) -> Sequence[ValidatorIndex]:
"""
Return the committee corresponding to ``indices``, ``seed``, ``index``, and committee ``count``.
"""
start = (len(indices) * index) // count
end = (len(indices) * uint64(index + 1)) // count
return [indices[compute_shuffled_index(uint64(i), uint64(len(indices)), seed)] for i in range(start, end)]
compute_committee
is used by get_beacon_committee()
to find the specific members of one of the committees at a slot.
Every epoch, a fresh set of committees is generated; during an epoch, the committees are stable.
Looking at the parameters in reverse order:
count
is the total number of committees in an epoch. This isSLOTS_PER_EPOCH
times the output ofget_committee_count_per_slot()
.index
is the committee number within the epoch, running from0
tocount - 1
. It is calculated in (get_beacon_committee()
from the committee number in the slotindex
and the slot number as(slot % SLOTS_PER_EPOCH) * committees_per_slot + index
.seed
is the seed value for computing the pseudo-random shuffling, based on the epoch number and a domain parameter (get_beacon_committee()
usesDOMAIN_BEACON_ATTESTER
).indices
is the list of validators eligible for inclusion in committees, namely the whole list of indices of active validators.
Random sampling among the validators is done by taking a contiguous slice of array indices from start
to end
and seeing where each one gets shuffled to by compute_shuffled_index()
. Note that ValidatorIndex(i)
is a type-cast in the above: it just turns i
into a ValidatorIndex type for input into the shuffling. The output value of the shuffling is then used as an index into the indices
list. There is much here that client implementations will optimise with caching and batch operations.
It may not be immediately obvious, but not all committees returned will be the same size (they can vary by one), and every validator in indices
will be a member of exactly one committee. As we increment index
from zero, clearly start
for index == j + 1
is end
for index == j
, so there are no gaps. In addition, the highest index
is count - 1
, so every validator in indices
finds its way into a committee.5
This method of selecting committees is light client friendly. Light clients can compute only the committees that they are interested in without needing to deal with the entire validator set. See the section on Shuffling for explanation of how this works.
Sync committees are assigned by a different process that is more akin to repeatedly performing compute_proposer_index()
.
Used by | get_beacon_committee |
Uses | compute_shuffled_index() |
compute_epoch_at_slot
def compute_epoch_at_slot(slot: Slot) -> Epoch:
"""
Return the epoch number at ``slot``.
"""
return Epoch(slot // SLOTS_PER_EPOCH)
This is trivial enough that I won't explain it. But note that it does rely on GENESIS_SLOT
and GENESIS_EPOCH
being zero. The more pernickety among us might prefer it to read,
return GENESIS_EPOCH + Epoch((slot - GENESIS_SLOT) // SLOTS_PER_EPOCH)
compute_start_slot_at_epoch
def compute_start_slot_at_epoch(epoch: Epoch) -> Slot:
"""
Return the start slot of ``epoch``.
"""
return Slot(epoch * SLOTS_PER_EPOCH)
Maybe should read,
return GENESIS_SLOT + Slot((epoch - GENESIS_EPOCH) * SLOTS_PER_EPOCH))
Used by | get_block_root() |
See also | SLOTS_PER_EPOCH , GENESIS_SLOT , GENESIS_EPOCH |
compute_activation_exit_epoch
def compute_activation_exit_epoch(epoch: Epoch) -> Epoch:
"""
Return the epoch during which validator activations and exits initiated in ``epoch`` take effect.
"""
return Epoch(epoch + 1 + MAX_SEED_LOOKAHEAD)
When queuing validators for activation or exit in process_registry_updates()
and initiate_validator_exit()
respectively, the activation or exit is delayed until the next epoch, plus MAX_SEED_LOOKAHEAD
epochs, currently 4.
See MAX_SEED_LOOKAHEAD
for the details, but in short it is designed to make it extremely hard for an attacker to manipulate the make up of committees via activations and exits.
Used by | initiate_validator_exit() , process_registry_updates() |
See also | MAX_SEED_LOOKAHEAD |
compute_fork_data_root
def compute_fork_data_root(current_version: Version, genesis_validators_root: Root) -> Root:
"""
Return the 32-byte fork data root for the ``current_version`` and ``genesis_validators_root``.
This is used primarily in signature domains to avoid collisions across forks/chains.
"""
return hash_tree_root(ForkData(
current_version=current_version,
genesis_validators_root=genesis_validators_root,
))
The fork data root serves as a unique identifier for the chain that we are on. genesis_validators_root
identifies our unique genesis event, and current_version
our own hard fork subsequent to that genesis event. This is useful, for example, to differentiate between a testnet and mainnet: both might have the same fork versions, but will definitely have different genesis validator roots.
It is used by compute_fork_digest()
and compute_domain()
.
Used by | compute_fork_digest() , compute_domain() |
Uses | hash_tree_root() |
See also | ForkData |
compute_fork_digest
def compute_fork_digest(current_version: Version, genesis_validators_root: Root) -> ForkDigest:
"""
Return the 4-byte fork digest for the ``current_version`` and ``genesis_validators_root``.
This is a digest primarily used for domain separation on the p2p layer.
4-bytes suffices for practical separation of forks/chains.
"""
return ForkDigest(compute_fork_data_root(current_version, genesis_validators_root)[:4])
Extracts the first four bytes of the fork data root as a ForkDigest
type. It is primarily used for domain separation on the peer-to-peer networking layer.
compute_fork_digest()
is used extensively in the Ethereum 2.0 networking specification to distinguish between independent beacon chain networks or forks: it is important that activity on one chain does not interfere with other chains.
Uses | compute_fork_data_root() |
See also | ForkDigest |
compute_domain
def compute_domain(domain_type: DomainType, fork_version: Version=None, genesis_validators_root: Root=None) -> Domain:
"""
Return the domain for the ``domain_type`` and ``fork_version``.
"""
if fork_version is None:
fork_version = GENESIS_FORK_VERSION
if genesis_validators_root is None:
genesis_validators_root = Root() # all bytes zero by default
fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root)
return Domain(domain_type + fork_data_root[:28])
When dealing with signed messages, the signature "domains" are separated according to three independent factors:
- All signatures include a
DomainType
relevant to the message's purpose, which is just some cryptographic hygiene in case the same message is to be signed for different purposes at any point. - All but signatures on deposit messages include the fork version. This ensures that messages across different forks of the chain become invalid, and that validators won't be slashed for signing attestations on two different chains (this is allowed).
- And, now, the root hash of the validator Merkle tree at Genesis is included. Along with the fork version this gives a unique identifier for our chain.
This function is mainly used by get_domain()
. It is also used in deposit processing, in which case fork_version
and genesis_validators_root
take their default values since deposits are valid across forks.
Fun fact: this function looks pretty simple, but I found a subtle bug in the way tests were generated in a previous implementation.
Used by | get_domain() , process_deposit() |
Uses | compute_fork_data_root() |
See also | Domain , DomainType GENESIS_FORK_VERSION |
compute_signing_root
def compute_signing_root(ssz_object: SSZObject, domain: Domain) -> Root:
"""
Return the signing root for the corresponding signing data.
"""
return hash_tree_root(SigningData(
object_root=hash_tree_root(ssz_object),
domain=domain,
))
This is a pre-processor for signing objects with BLS signatures:
- calculate the hash tree root of the object;
- combine the hash tree root with the
Domain
inside a temporarySigningData
object; - return the hash tree root of that, which is the data to be signed.
The domain
is usually the output of get_domain()
, which mixes in the cryptographic domain, the fork version, and the genesis validators root to the message hash. For deposits, it is the output of compute_domain()
, ignoring the fork version and genesis validators root.
This is exactly equivalent to adding the domain to an object and taking the hash tree root of the whole thing. Indeed, this function used to be called compute_domain_wrapper_root()
.
Used by | Many places |
Uses | hash_tree_root() |
See also | SigningData , Domain |
Participation flags
These two simple utilities were added in the Altair upgrade.
add_flag
def add_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags:
"""
Return a new ``ParticipationFlags`` adding ``flag_index`` to ``flags``.
"""
flag = ParticipationFlags(2**flag_index)
return flags | flag
This is simple and self-explanatory. The 2**flag_index
is a bit Pythonic. In a C-like language it would use a bit-shift:
1 << flag_index
Used by | process_attestation() , translate_participation() |
See also | ParticipationFlags |
has_flag
def has_flag(flags: ParticipationFlags, flag_index: int) -> bool:
"""
Return whether ``flags`` has ``flag_index`` set.
"""
flag = ParticipationFlags(2**flag_index)
return flags & flag == flag
Move along now, nothing to see here.
Used by | get_unslashed_participating_indices() , process_attestation() |
See also | ParticipationFlags |
Beacon State Accessors
As the name suggests, these functions access the beacon state to calculate various useful things, without modifying it.
get_current_epoch
def get_current_epoch(state: BeaconState) -> Epoch:
"""
Return the current epoch.
"""
return compute_epoch_at_slot(state.slot)
A getter for the current epoch, as calculated by compute_epoch_at_slot()
.
Used by | Everywhere |
Uses | compute_epoch_at_slot() |
get_previous_epoch
def get_previous_epoch(state: BeaconState) -> Epoch:
"""`
Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``).
"""
current_epoch = get_current_epoch(state)
return GENESIS_EPOCH if current_epoch == GENESIS_EPOCH else Epoch(current_epoch - 1)
Return the previous epoch number as an Epoch
type. Returns GENESIS_EPOCH
if we are in the GENESIS_EPOCH
, since it has no prior, and we don't do negative numbers.
Used by | Everywhere |
Uses | get_current_epoch() |
See also | GENESIS_EPOCH |
get_block_root
def get_block_root(state: BeaconState, epoch: Epoch) -> Root:
"""
Return the block root at the start of a recent ``epoch``.
"""
return get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch))
The Casper FFG part of consensus deals in Checkpoint
s that are the first slot of an epoch. get_block_root
is a specialised version of get_block_root_at_slot()
that returns the block root of the checkpoint, given only an epoch.
Used by | get_attestation_participation_flag_indices() , weigh_justification_and_finalization() |
Uses | get_block_root_at_slot() , compute_start_slot_at_epoch() |
See also | Root |
get_block_root_at_slot
def get_block_root_at_slot(state: BeaconState, slot: Slot) -> Root:
"""
Return the block root at a recent ``slot``.
"""
assert slot < state.slot <= slot + SLOTS_PER_HISTORICAL_ROOT
return state.block_roots[slot % SLOTS_PER_HISTORICAL_ROOT]
Recent block roots are stored in a circular list in state, with a length of SLOTS_PER_HISTORICAL_ROOT
(currently ~27 hours).
get_block_root_at_slot()
is used by get_attestation_participation_flag_indices()
to check whether an attestation has voted for the correct chain head. It is also used in process_sync_aggregate()
to find the block that the sync committee is signing-off on.
Used by | get_block_root() , get_attestation_participation_flag_indices() , process_sync_aggregate() |
See also | SLOTS_PER_HISTORICAL_ROOT , Root |
get_randao_mix
def get_randao_mix(state: BeaconState, epoch: Epoch) -> Bytes32:
"""
Return the randao mix at a recent ``epoch``.
"""
return state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR]
RANDAO mixes are stored in a circular list of length EPOCHS_PER_HISTORICAL_VECTOR
. They are used when calculating the seed for assigning beacon proposers and committees.
Used by | get_seed , process_randao_mixes_reset() , process_randao() |
See also | EPOCHS_PER_HISTORICAL_VECTOR |
get_active_validator_indices
def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]:
"""
Return the sequence of active validator indices at ``epoch``.
"""
return [ValidatorIndex(i) for i, v in enumerate(state.validators) if is_active_validator(v, epoch)]
Steps through the entire list of validators and returns the list of only the active ones. That is, the list of validators that have been activated but not exited as determined by is_active_validator()
.
This function is heavily used and I'd expect it to be memoised in practice.
Used by | Many places |
Uses | is_active_validator() |
get_validator_churn_limit
def get_validator_churn_limit(state: BeaconState) -> uint64:
"""
Return the validator churn limit for the current epoch.
"""
active_validator_indices = get_active_validator_indices(state, get_current_epoch(state))
return max(MIN_PER_EPOCH_CHURN_LIMIT, uint64(len(active_validator_indices)) // CHURN_LIMIT_QUOTIENT)
The "churn limit" applies when activating and exiting validators and acts as a rate-limit on changes to the validator set. The value returned by this function provides the number of validators that may become active in an epoch, and the number of validators that may exit in an epoch.
Some small amount of churn is always allowed, set by MIN_PER_EPOCH_CHURN_LIMIT
, and the amount of per-epoch churn allowed increases by one for every extra CHURN_LIMIT_QUOTIENT
validators that are currently active (once the minimum has been exceeded).
In concrete terms, this means that up to four validators can enter or exit the active validator set each epoch (900 per day) until we have 327,680 active validators, at which point the limit rises to five.
Used by | initiate_validator_exit() , process_registry_updates() |
Uses | get_active_validator_indices() |
See also | MIN_PER_EPOCH_CHURN_LIMIT , CHURN_LIMIT_QUOTIENT |
get_seed
def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Bytes32:
"""
Return the seed at ``epoch``.
"""
mix = get_randao_mix(state, Epoch(epoch + EPOCHS_PER_HISTORICAL_VECTOR - MIN_SEED_LOOKAHEAD - 1)) # Avoid underflow
return hash(domain_type + uint_to_bytes(epoch) + mix)
Used in get_beacon_committee()
, get_beacon_proposer_index()
, and get_next_sync_committee_indices()
to provide the randomness for computing proposers and committees. domain_type
is DOMAIN_BEACON_ATTESTER
, DOMAIN_BEACON_PROPOSER
, and DOMAIN_SYNC_COMMITTEE
respectively.
RANDAO mixes are stored in a circular list of length EPOCHS_PER_HISTORICAL_VECTOR
. The seed for an epoch is based on the randao mix from MIN_SEED_LOOKAHEAD
epochs ago. This is to limit the forward visibility of randomness: see the explanation there.
The seed returned is not based only on the domain and the randao mix, but the epoch number is also mixed in. This is to handle the pathological case of no blocks being seen for more than two epochs, in which case we run out of randao updates. That could lock in forever a non-participating set of block proposers. Mixing in the epoch number means that fresh committees and proposers can continue to be selected.
Used by | get_beacon_committee() , get_beacon_proposer_index() , get_next_sync_committee_indices() |
Uses | get_randao_mix() |
See also | EPOCHS_PER_HISTORICAL_VECTOR , MIN_SEED_LOOKAHEAD |
get_committee_count_per_slot
def get_committee_count_per_slot(state: BeaconState, epoch: Epoch) -> uint64:
"""
Return the number of committees in each slot for the given ``epoch``.
"""
return max(uint64(1), min(
MAX_COMMITTEES_PER_SLOT,
uint64(len(get_active_validator_indices(state, epoch))) // SLOTS_PER_EPOCH // TARGET_COMMITTEE_SIZE,
))
Every slot in a given epoch has the same number of beacon committees, as calculated by this function.
As far as the LMD GHOST consensus protocol is concerned, all the validators attesting in a slot effectively act as a single large committee. However, organising them into multiple committees gives two benefits.
- Having multiple smaller committees reduces the load on the aggregators that collect and aggregate the attestations from committee members. This is important, as validating the signatures and aggregating them takes time. The downside is that blocks need to be larger, as, in the best case, there are up to 64 aggregate attestations to store per block rather than a single large aggregate signature over all attestations.
- It maps well onto the future plans for data shards, when each committee will be responsible for committing to a block on one shard in addition to its current duties.
There is always at least one committee per slot, and never more than MAX_COMMITTEES_PER_SLOT
, currently 64.
Subject to these constraints, the actual number of committees per slot is , where is the total number of active validators.
The intended behaviour looks like this:
- The ideal case is that there are
MAX_COMMITTEES_PER_SLOT
= 64 committees per slot. This maps to one committee per slot per shard once data sharding has been implemented. These committees will be responsible for voting on shard crosslinks. There must be at least 262,144 active validators to achieve this. - If there are fewer active validators, then the number of committees per shard is reduced below 64 in order to maintain a minimum committee size of
TARGET_COMMITTEE_SIZE
= 128. In this case, not every shard will get crosslinked at every slot (once sharding is in place). - Finally, only if the number of active validators falls below 4096 will the committee size be reduced to less than 128. With so few validators, the chain has no meaningful security in any case.
Used by | get_beacon_committee() , process_attestation() |
Uses | get_active_validator_indices() |
See also | MAX_COMMITTEES_PER_SLOT , TARGET_COMMITTEE_SIZE |
get_beacon_committee
def get_beacon_committee(state: BeaconState, slot: Slot, index: CommitteeIndex) -> Sequence[ValidatorIndex]:
"""
Return the beacon committee at ``slot`` for ``index``.
"""
epoch = compute_epoch_at_slot(slot)
committees_per_slot = get_committee_count_per_slot(state, epoch)
return compute_committee(
indices=get_active_validator_indices(state, epoch),
seed=get_seed(state, epoch, DOMAIN_BEACON_ATTESTER),
index=(slot % SLOTS_PER_EPOCH) * committees_per_slot + index,
count=committees_per_slot * SLOTS_PER_EPOCH,
)
Beacon committees vote on the beacon block at each slot via attestations. There are up to MAX_COMMITTEES_PER_SLOT
beacon committees per slot, and each committee is active exactly once per epoch.
This function returns the list of committee members given a slot number and an index within that slot to select the desired committee, relying on compute_committee()
to do the heavy lifting.
Note that, since this uses get_seed()
, we can obtain committees only up to EPOCHS_PER_HISTORICAL_VECTOR
epochs into the past (minus MIN_SEED_LOOKAHEAD
).
get_beacon_committee
is used by get_attesting_indices()
and process_attestation()
when processing attestations coming from a committee, and by validators when checking their committee assignments and aggregation duties.
get_beacon_proposer_index
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
"""
Return the beacon proposer index at the current slot.
"""
epoch = get_current_epoch(state)
seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + uint_to_bytes(state.slot))
indices = get_active_validator_indices(state, epoch)
return compute_proposer_index(state, indices, seed)
Each slot, exactly one of the active validators is randomly chosen to be the proposer of the beacon block for that slot. The probability of being selected is weighted by the validator's effective balance in compute_proposer_index()
.
The chosen block proposer does not need to be a member of one of the beacon committees for that slot: it is chosen from the entire set of active validators for that epoch.
The RANDAO seed returned by get_seed()
is updated once per epoch. The slot number is mixed into the seed using a hash to allow us to choose a different proposer at each slot. This also protects us in the case that there is an entire epoch of empty blocks. If that were to happen the RANDAO would not be updated, but we would still be able to select a different set of proposers for the next epoch via this slot number mix-in process.
There is a chance of the same proposer being selected in two consecutive slots, or more than once per epoch. If every validator has the same effective balance, then the probability of being selected in a particular slot is simply independent of any other slot, where is the number of active validators in the epoch corresponding to the slot.
get_total_balance
def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei:
"""
Return the combined effective balance of the ``indices``.
``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero.
Math safe up to ~10B ETH, afterwhich this overflows uint64.
"""
return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([state.validators[index].effective_balance for index in indices])))
A simple utility that returns the total balance of all validators in the list, indices
, passed in.
As an aside, there is an interesting example of some fragility in the spec lurking here. This function used to return a minimum of 1 Gwei to avoid a potential division by zero in the calculation of rewards and penalties. However, the rewards calculation was modified to avoid a possible integer overflow condition, without modifying this function, which re-introduced the possibility of a division by zero. This was later fixed by returning a minimum of EFFECTIVE_BALANCE_INCREMENT
. The formal verification of the specification is helpful in avoiding issues like this.
Used by | get_total_active_balance() , get_flag_index_deltas() , process_justification_and_finalization() |
See also | EFFECTIVE_BALANCE_INCREMENT |
get_total_active_balance
def get_total_active_balance(state: BeaconState) -> Gwei:
"""
Return the combined effective balance of the active validators.
Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero.
"""
return get_total_balance(state, set(get_active_validator_indices(state, get_current_epoch(state))))
Uses get_total_balance()
to calculate the sum of the effective balances of all active validators in the current epoch.
This quantity is frequently used in the spec. For example, Casper FFG uses the total active balance to judge whether the 2/3 majority threshold of attestations has been reached in justification and finalisation. And it is a fundamental part of the calculation of rewards and penalties. The base reward is proportional to the reciprocal of the square root of the total active balance. Thus, validator rewards are higher when little balance is at stake (few active validators) and lower when much balance is at stake (many active validators).
Since it is calculated from effective balances, total active balance does not change during an epoch, so is a great candidate for being cached.
get_domain
def get_domain(state: BeaconState, domain_type: DomainType, epoch: Epoch=None) -> Domain:
"""
Return the signature domain (fork version concatenated with domain type) of a message.
"""
epoch = get_current_epoch(state) if epoch is None else epoch
fork_version = state.fork.previous_version if epoch < state.fork.epoch else state.fork.current_version
return compute_domain(domain_type, fork_version, state.genesis_validators_root)
get_domain()
pops up whenever signatures need to be verified, since a DomainType
is always mixed in to the signed data. For the science behind domains, see Domain types and compute_domain()
.
With the exception of DOMAIN_DEPOSIT
, domains are always combined with the fork version before being used in signature generation. This is to distinguish messages from different chains, and ensure that validators don't get slashed if they choose to participate on two independent forks. (That is, deliberate forks, aka hard-forks. Participating on both branches of temporary consensus forks is punishable: that's basically the whole point of slashing.)
Note that a message signed under one fork version will be valid during the next fork version, but not thereafter. So, for example, voluntary exit messages signed during Altair will be valid after the Bellatrix beacon chain upgrade, but not after the Capella upgrade (the one after Bellatrix). Voluntary exit messages signed during Phase 0 are valid under Altair but will be made invalid by the Bellatrix upgrade.
get_indexed_attestation
def get_indexed_attestation(state: BeaconState, attestation: Attestation) -> IndexedAttestation:
"""
Return the indexed attestation corresponding to ``attestation``.
"""
attesting_indices = get_attesting_indices(state, attestation.data, attestation.aggregation_bits)
return IndexedAttestation(
attesting_indices=sorted(attesting_indices),
data=attestation.data,
signature=attestation.signature,
)
Lists of validators within committees occur in two forms in the specification.
- They can be compressed into a bitlist, in which each bit represents the presence or absence of a validator from a particular committee. The committee is referenced by slot, and committee index within that slot. This is how sets of validators are represented in
Attestation
s. - Or they can be listed explicitly by their validator indices, as in
IndexedAttestation
s. Note that the list of indices is sorted: an attestation is invalid if not.
get_indexed_attestation()
converts from the former representation to the latter. The slot number and the committee index are provided by the AttestationData
and are used to reconstruct the committee members via get_beacon_committee()
. The supplied bitlist will have come from an Attestation
.
Attestations are aggregatable, which means that attestations from multiple validators making the same vote can be rolled up into a single attestation through the magic of BLS signature aggregation. However, in order to be able to verify the signature later, a record needs to be kept of which validators actually contributed to the attestation. This is so that those validators' public keys can be aggregated to match the construction of the signature.
The conversion from the bit-list format to the list format is performed by get_attesting_indices()
, below.
Used by | process_attestation() |
Uses | get_attesting_indices() |
See also | Attestation , IndexedAttestation |
get_attesting_indices
def get_attesting_indices(state: BeaconState,
data: AttestationData,
bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]) -> Set[ValidatorIndex]:
"""
Return the set of attesting indices corresponding to ``data`` and ``bits``.
"""
committee = get_beacon_committee(state, data.slot, data.index)
return set(index for i, index in enumerate(committee) if bits[i])
As described under get_indexed_attestation()
, lists of validators come in two forms. This routine converts from the compressed form, in which validators are represented as a subset of a committee with their presence or absence indicated by a 1 or 0 bit respectively, to an explicit list of ValidatorIndex
types.
Used by | get_indexed_attestation() , process_attestation() , translate_participation() |
Uses | get_beacon_committee() |
See also | AttestationData , IndexedAttestation |
get_next_sync_committee_indices
def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]:
"""
Return the sync committee indices, with possible duplicates, for the next sync committee.
"""
epoch = Epoch(get_current_epoch(state) + 1)
MAX_RANDOM_BYTE = 2**8 - 1
active_validator_indices = get_active_validator_indices(state, epoch)
active_validator_count = uint64(len(active_validator_indices))
seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE)
i = 0
sync_committee_indices: List[ValidatorIndex] = []
while len(sync_committee_indices) < SYNC_COMMITTEE_SIZE:
shuffled_index = compute_shuffled_index(uint64(i % active_validator_count), active_validator_count, seed)
candidate_index = active_validator_indices[shuffled_index]
random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32]
effective_balance = state.validators[candidate_index].effective_balance
if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte:
sync_committee_indices.append(candidate_index)
i += 1
return sync_committee_indices
get_next_sync_committee_indices()
is used to select the subset of validators that will make up a sync committee. The committee size is SYNC_COMMITTEE_SIZE
, and the committee is allowed to contain duplicates, that is, the same validator more than once. This is to handle gracefully the situation of there being fewer active validators than SYNC_COMMITTEE_SIZE
.
Similarly to being chosen to propose a block, the probability of any validator being selected for a sync committee is proportional to its effective balance. Thus, the algorithm is almost the same as that of compute_proposer_index()
, except that this one exits only after finding SYNC_COMMITTEE_SIZE
members, rather than exiting as soon as a candidate is found. Both routines use the try-and-increment method to weight the probability of selection with the validators' effective balances.
It's fairly clear why block proposers are selected with a probability proportional to their effective balances: block production is subject to slashing, and proposers with less at stake have less to slash, so we reduce their influence accordingly. It is not so clear why the probability of being in a sync committee is also proportional to a validator's effective balance; sync committees are not subject to slashing. It has to do with keeping calculations for light clients simple. We don't want to burden light clients with summing up validators' balances to judge whether a 2/3 supermajority of stake in the committee has voted for a block. Ideally, they can just count the participation flags. To make this somewhat reliable, we weight the probability that a validator participates in proportion to its effective balance.
Used by | get_next_sync_committee() |
Uses | get_active_validator_indices() , get_seed() , compute_shuffled_index() , uint_to_bytes() |
See also | SYNC_COMMITTEE_SIZE , compute_proposer_index() |
get_next_sync_committee
Note: The function
get_next_sync_committee
should only be called at sync committee period boundaries and when upgrading state to Altair.
The random seed that generates the sync committee is based on the number of the next epoch. get_next_sync_committee_indices()
doesn't contain any check that the epoch corresponds to a sync-committee change boundary, which allowed the timing of the Altair upgrade to be more flexible. But a consequence is that you will get an incorrect committee if you call get_next_sync_committee()
at the wrong time.
def get_next_sync_committee(state: BeaconState) -> SyncCommittee:
"""
Return the next sync committee, with possible pubkey duplicates.
"""
indices = get_next_sync_committee_indices(state)
pubkeys = [state.validators[index].pubkey for index in indices]
aggregate_pubkey = eth_aggregate_pubkeys(pubkeys)
return SyncCommittee(pubkeys=pubkeys, aggregate_pubkey=aggregate_pubkey)
get_next_sync_committee()
is a simple wrapper around get_next_sync_committee_indices()
that packages everything up into a nice SyncCommittee
object.
See the SyncCommittee
type for an explanation of how the aggregate_pubkey
is intended to be used.
Used by | process_sync_committee_updates() , initialize_beacon_state_from_eth1() , upgrade_to_altair() |
Uses | get_next_sync_committee_indices() , eth_aggregate_pubkeys() |
See also | SyncCommittee |
get_unslashed_participating_indices
def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epoch: Epoch) -> Set[ValidatorIndex]:
"""
Return the set of validator indices that are both active and unslashed for the given ``flag_index`` and ``epoch``.
"""
assert epoch in (get_previous_epoch(state), get_current_epoch(state))
if epoch == get_current_epoch(state):
epoch_participation = state.current_epoch_participation
else:
epoch_participation = state.previous_epoch_participation
active_validator_indices = get_active_validator_indices(state, epoch)
participating_indices = [i for i in active_validator_indices if has_flag(epoch_participation[i], flag_index)]
return set(filter(lambda index: not state.validators[index].slashed, participating_indices))
get_unslashed_participating_indices()
returns the list of validators that made a timely attestation with the type flag_index
during the epoch
in question.
It is used with the TIMELY_TARGET_FLAG_INDEX
flag in process_justification_and_finalization()
to calculate the proportion of stake that voted for the candidate checkpoint in the current and previous epochs.
It is also used with the TIMELY_TARGET_FLAG_INDEX
for applying inactivity penalties in process_inactivity_updates()
and get_inactivity_penalty_deltas()
. If a validator misses a correct target vote during an inactivity leak then it is considered not to have participated at all (it is not contributing anything useful).
And it is used in get_flag_index_deltas()
for calculating rewards due for each type of correct vote.
Slashed validators are ignored. Once slashed, validators no longer receive rewards or participate in consensus, although they are subject to penalties until they have finally been exited.
get_attestation_participation_flag_indices
def get_attestation_participation_flag_indices(state: BeaconState,
data: AttestationData,
inclusion_delay: uint64) -> Sequence[int]:
"""
Return the flag indices that are satisfied by an attestation.
"""
if data.target.epoch == get_current_epoch(state):
justified_checkpoint = state.current_justified_checkpoint
else:
justified_checkpoint = state.previous_justified_checkpoint
# Matching roots
is_matching_source = data.source == justified_checkpoint
is_matching_target = is_matching_source and data.target.root == get_block_root(state, data.target.epoch)
is_matching_head = is_matching_target and data.beacon_block_root == get_block_root_at_slot(state, data.slot)
assert is_matching_source
participation_flag_indices = []
if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH):
participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX)
if is_matching_target and inclusion_delay <= SLOTS_PER_EPOCH:
participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX)
if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY:
participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX)
return participation_flag_indices
This is called by process_attestation()
during block processing, and is the heart of the mechanism for recording validators' votes as contained in their attestations. It filters the given attestation against the beacon state's current view of the chain, and returns participation flag indices only for the votes that are both correct and timely.
data
is an AttestationData
object that contains the source, target, and head votes of the validators that contributed to the attestation. The attestation may represent the votes of one or more validators.
inclusion_delay
is the difference between the current slot on the beacon chain and the slot for which the attestation was created. For the block containing the attestation to be valid, inclusion_delay
must be between MIN_ATTESTATION_INCLUSION_DELAY
and SLOTS_PER_EPOCH
inclusive. In other words, attestations must be included in the next block, or in any block up to 32 slots later, after which they are ignored.
Since the attestation may be up to 32 slots old, it might have been generated in the current epoch or the previous epoch, so the first thing we do is to check the attestation's target vote epoch to see which epoch we should be looking at in the beacon state.
Next, we check whether each of the votes in the attestation are correct:
- Does the attestation's source vote match what we believe to be the justified checkpoint in the epoch in question?
- If so, does the attestation's target vote match the head block at the epoch's checkpoint, that is, the first slot of the epoch?
- If so, does the attestation's head vote match what we believe to be the head block at the attestation's slot? Note that the slot may not contain a block – it may be a skip slot – in which case the last known block is considered to be the head.
These three build on each other, so that it is not possible to have a correct target vote without a correct source vote, and it is not possible to have a correct head vote without a correct target vote.
The assert
statement is interesting. If an attestation does not have the correct source vote, the block containing it is invalid and is discarded. Having an incorrect source vote means that the block proposer disagrees with me about the last justified checkpoint, which is an irreconcilable difference.
After checking the validity of the votes, the timeliness of each vote is checked. Let's take them in reverse order.
- Correct head votes must be included immediately, that is, in the very next slot.
- Head votes, used for LMD GHOST consensus, are not useful after one slot.
- Correct target votes must be included within 32 slots, one epoch.
- Target votes are useful at any time, but it is simpler if they don't span more than a couple of epochs, so 32 slots is a reasonable limit. This check is actually redundant since attestations in blocks cannot be older than 32 slots.
- Correct source votes must be included within 5 slots (
integer_squareroot(32)
).- This is the geometric mean of 1 (the timely head threshold) and 32 (the timely target threshold). This is an arbitrary choice. Vitalik's view6 is that, with this setting, the cumulative timeliness rewards most closely match an exponentially decreasing curve, which "feels more logical".
The timely inclusion requirements are new in Altair. In Phase 0, all correct votes received a reward, and there was an additional reward for inclusion the was proportional to the reciprocal of the inclusion distance. This led to a oddity where it was always more profitable to vote for a correct head, even if that meant waiting longer and risking not being included in the next slot.
get_flag_index_deltas
def get_flag_index_deltas(state: BeaconState, flag_index: int) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
Return the deltas for a given ``flag_index`` by scanning through the participation flags.
"""
rewards = [Gwei(0)] * len(state.validators)
penalties = [Gwei(0)] * len(state.validators)
previous_epoch = get_previous_epoch(state)
unslashed_participating_indices = get_unslashed_participating_indices(state, flag_index, previous_epoch)
weight = PARTICIPATION_FLAG_WEIGHTS[flag_index]
unslashed_participating_balance = get_total_balance(state, unslashed_participating_indices)
unslashed_participating_increments = unslashed_participating_balance // EFFECTIVE_BALANCE_INCREMENT
active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT
for index in get_eligible_validator_indices(state):
base_reward = get_base_reward(state, index)
if index in unslashed_participating_indices:
if not is_in_inactivity_leak(state):
reward_numerator = base_reward * weight * unslashed_participating_increments
rewards[index] += Gwei(reward_numerator // (active_increments * WEIGHT_DENOMINATOR))
elif flag_index != TIMELY_HEAD_FLAG_INDEX:
penalties[index] += Gwei(base_reward * weight // WEIGHT_DENOMINATOR)
return rewards, penalties
This function is used during epoch processing to assign rewards and penalties to individual validators based on their voting record in the previous epoch. Rewards for block proposers for including attestations are calculated during block processing. The "deltas" in the function name are the separate lists of rewards and penalties returned. Rewards and penalties are always treated separately to avoid negative numbers.
The function is called once for each of the flag types corresponding to correct attestation votes: timely source, timely target, timely head.
The list of validators returned by get_unslashed_participating_indices()
contains the ones that will be rewarded for making this vote type in a timely and correct manner. That routine uses the flags set in state for each validator by process_attestation()
during block processing and returns the validators for which the corresponding flag is set.
Every active validator is expected to make an attestation exactly once per epoch, so we then cycle through the entire set of active validators, rewarding them if they appear in unslashed_participating_indices
, as long as we are not in an inactivity leak. If we are in a leak, no validator is rewarded for any of its votes, but penalties still apply to non-participating validators.
Notice that the reward is weighted with unslashed_participating_increments
, which is proportional to the total stake of the validators that made a correct vote with this flag. This means that, if participation by other validators is lower, then my rewards are lower, even if I perform my duties perfectly. The reason for this is to do with discouragement attacks (see also this nice explainer). In short, with this mechanism, validators are incentivised to help each other out (e.g. by forwarding gossip messages, or aggregating attestations well) rather than to attack or censor one-another.
Validators that did not make a correct and timely vote are penalised with a full weighted base reward for each flag that they missed, except for missing the head vote. Head votes have only a single slot to get included, so a missing block in the next slot is sufficient to cause a miss, but is completely outside the attester's control. Thus head votes are only rewarded, not penalised. This also allows perfectly performing validators to break even during an inactivity leak, when we expect at least a third of blocks to be missing: they receive no rewards, but ideally no penalties either.
Untangling the arithmetic, the maximum total issuance due to rewards for attesters in an epoch, , comes out as follows, in the notation described later.
Beacon State Mutators
increase_balance
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!
Used by | slash_validator() , process_rewards_and_penalties() , process_attestation() , process_deposit() , process_sync_aggregate() |
See also | decrease_balance() |
decrease_balance
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.
Used by | slash_validator() , process_rewards_and_penalties() , process_slashings() , process_sync_aggregate() |
See also | increase_balance() |
initiate_validator_exit
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 EJECTION_BALANCE
threshold.
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.
slash_validator
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
decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR)
# 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_epoch
for a validator (the point at which a validator's stake is in principle unlocked) is set toMIN_VALIDATOR_WITHDRAWABILITY_DELAY
epochs after it exits. When a validator is slashed, a much longer period of lock-up applies, namelyEPOCHS_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_ALTAIR
is applied. With current values, this is a maximum of 0.5 Ether, increased from 0.25 Ether in Phase 0. The plan is to increase this to 1 Ether at The Merge. - 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.
Beacon Chain State Transition Function
Preamble
The post-state corresponding to a pre-state
state
and a signed blocksigned_block
is defined asstate_transition(state, signed_block)
. State transitions that trigger an unhandled exception (e.g. a failedassert
or an out-of-range list access) are considered invalid. State transitions that cause auint64
overflow or underflow are also considered invalid.
This is a very important statement of how the spec deals with invalid conditions and errors. Basically, if any block is processed that would trigger any kind of exception in the Python code of the specification, then that block is invalid and must be rejected. That means having to undo any state modifications already made in the course of processing the block.
People who do formal verification of the specification don't much like this, as having assert statements in running code is an anti-pattern: it is better to ensure that your code can simply never fail.
Anyway, the beacon chain state transition has three elements:
- slot processing, which is performed for every slot regardless of what else is happening;
- epoch processing, which happens every
SLOTS_PER_EPOCH
(32) slots, again regardless of whatever else is going on; and, - block processing, which happens only in slots for which a beacon block has been received.
def state_transition(state: BeaconState, signed_block: SignedBeaconBlock, validate_result: bool=True) -> None:
block = signed_block.message
# Process slots (including those with no blocks) since block
process_slots(state, block.slot)
# Verify signature
if validate_result:
assert verify_block_signature(state, signed_block)
# Process block
process_block(state, block)
# Verify state root
if validate_result:
assert block.state_root == hash_tree_root(state)
As the spec is written, a state transition is triggered by receiving a block to process. That means that we first need to fast forward from our current slot number in the state (which is the slot at which we last processed a block) to the slot of the block we are processing. We treat intervening slots, if any, as empty. This "fast-forward" is done by process_slots()
, which also triggers epoch processing as required.
In actual client implementations, state updates will usually be time-based, triggered by moving to the next slot if a block has not been received. However, the fast-forward functionality will be used when exploring different forks in the block tree.
The validate_result
parameter defaults to True
, meaning that the block's signature will be checked, and that the result of applying the block to the state results in the same state root that the block claims it does (the "post-states" must match). When creating blocks, however, proposers can set validate_result
to False
to allow the state root to be calculated, else we'd have a circular dependency. The signature over the initial candidate block is omitted to avoid bad interactions with slashing protection when signing twice in a slot.
Uses | process_slots() , verify_block_signature , process_block |
def verify_block_signature(state: BeaconState, signed_block: SignedBeaconBlock) -> bool:
proposer = state.validators[signed_block.message.proposer_index]
signing_root = compute_signing_root(signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER))
return bls.Verify(proposer.pubkey, signing_root, signed_block.signature)
Check that the signature on the block matches the block's contents and the public key of the claimed proposer of the block. This ensures that blocks cannot be forged, or tampered with in transit. All the public keys for validators are stored in the Validator
s list in state.
Used by | state_transition() |
Uses | compute_signing_root() , get_domain() , bls.Verify() |
See also | DOMAIN_BEACON_PROPOSER |
def process_slots(state: BeaconState, slot: Slot) -> None:
assert state.slot < slot
while state.slot < slot:
process_slot(state)
# Process epoch on the start slot of the next epoch
if (state.slot + 1) % SLOTS_PER_EPOCH == 0:
process_epoch(state)
state.slot = Slot(state.slot + 1)
Updates the state from its current slot up to the given slot number assuming that all the intermediate slots are empty (that they do not contain blocks). Iteratively calls process_slot()
to apply the empty slot state-transition.
This is where epoch processing is triggered when required. Empty slot processing is extremely light weight, but any epoch transitions that need to be processed require the full rewards and penalties, and justification–finalisation apparatus.
Used by | state_transition() |
Uses | process_slot() , process_epoch() |
See also | SLOTS_PER_EPOCH |
def process_slot(state: BeaconState) -> None:
# Cache state root
previous_state_root = hash_tree_root(state)
state.state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root
# Cache latest block header state root
if state.latest_block_header.state_root == Bytes32():
state.latest_block_header.state_root = previous_state_root
# Cache block root
previous_block_root = hash_tree_root(state.latest_block_header)
state.block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root
Apply a single slot state-transition (but updating the slot number, and any required epoch processing is handled by process_slots()
). This is done at each slot whether or not there is a block present; if there is no block present then it is the only thing that is done.
Slot processing is almost trivial and consists only of calculating the updated state and block hash tree roots (as necessary), and storing them in the historical lists in the state. In a circular way, the state roots only change over an the empty slot state transition due to updating the lists of state and block roots.
SLOTS_PER_HISTORICAL_ROOT
is a multiple of SLOTS_PER_EPOCH
, so there is no danger of overwriting the circular lists of state_roots
and block_roots
. These will be dealt with correctly during epoch processing.
The only curiosity here is the lines,
if state.latest_block_header.state_root == Bytes32():
state.latest_block_header.state_root = previous_state_root
This logic was introduced to avoid a circular dependency while also keeping the state transition clean. Each block that we receive contains a post-state root, but as part of state processing we store the block in the state (in state.latest_block_header
), thus changing the post-state root.
Therefore, to be able to verify the state transition, we use the convention that the state root of the incoming block, and the state root that we calculate after inserting the block into the state, are both based on a temporary block header that has a stubbed state root, namely Bytes32()
. This allows the block's claimed post-state root to validated without the circularity. The next time that process_slots()
is called, the block's stubbed state root is updated to the actual post-state root, as above.
Used by | process_slots() |
Uses | hash_tree_root |
See also | SLOTS_PER_HISTORICAL_ROOT |
Epoch processing
def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state) # [Modified in Altair]
process_inactivity_updates(state) # [New in Altair]
process_rewards_and_penalties(state) # [Modified in Altair]
process_registry_updates(state)
process_slashings(state) # [Modified in Altair]
process_eth1_data_reset(state)
process_effective_balance_updates(state)
process_slashings_reset(state)
process_randao_mixes_reset(state)
process_historical_roots_update(state)
process_participation_flag_updates(state) # [New in Altair]
process_sync_committee_updates(state) # [New in Altair]
The long laundry list of things that need to be done at the end of an epoch. You can see from the comments that a bunch of extra work was added in Altair.
Used by | process_slots() |
Uses | All the things below |
Justification and finalization
def process_justification_and_finalization(state: BeaconState) -> None:
# Initial FFG checkpoint values have a `0x00` stub for `root`.
# Skip FFG updates in the first two epochs to avoid corner cases that might result in modifying this stub.
if get_current_epoch(state) <= GENESIS_EPOCH + 1:
return
previous_indices = get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state))
current_indices = get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, get_current_epoch(state))
total_active_balance = get_total_active_balance(state)
previous_target_balance = get_total_balance(state, previous_indices)
current_target_balance = get_total_balance(state, current_indices)
weigh_justification_and_finalization(state, total_active_balance, previous_target_balance, current_target_balance)
I believe the corner cases mentioned in the comments are related to Issue 8497. In any case, skipping justification and finalisation calculations during the first two epochs definitely simplifies things.
For the purposes of the Casper FFG finality calculations, we want attestations that have both source and target votes we agree with. If the source vote is incorrect, then the attestation is never processed into the state, so we just need the validators that voted for the correct target, according to their participation flag indices.
Since correct target votes can be included up to 32 slots after they are made, we collect votes from both the previous epoch and the current epoch to ensure that we have them all.
Once we know which validators voted for the correct source and head in the current and previous epochs, we add up their effective balances (not actual balances). total_active_balance
is the sum of the effective balances for all validators that ought to have voted during the current epoch. Slashed, but not exited validators are not included in these calculations.
These aggregate balances are passed to weigh_justification_and_finalization()
to do the actual work of updating justification and finalisation.
Used by | process_epoch() |
Uses | get_unslashed_participating_indices() , get_total_active_balance() , get_total_balance() , weigh_justification_and_finalization() |
See also | participation flag indices |
def weigh_justification_and_finalization(state: BeaconState,
total_active_balance: Gwei,
previous_epoch_target_balance: Gwei,
current_epoch_target_balance: Gwei) -> None:
previous_epoch = get_previous_epoch(state)
current_epoch = get_current_epoch(state)
old_previous_justified_checkpoint = state.previous_justified_checkpoint
old_current_justified_checkpoint = state.current_justified_checkpoint
# Process justifications
state.previous_justified_checkpoint = state.current_justified_checkpoint
state.justification_bits[1:] = state.justification_bits[:JUSTIFICATION_BITS_LENGTH - 1]
state.justification_bits[0] = 0b0
if previous_epoch_target_balance * 3 >= total_active_balance * 2:
state.current_justified_checkpoint = Checkpoint(epoch=previous_epoch,
root=get_block_root(state, previous_epoch))
state.justification_bits[1] = 0b1
if current_epoch_target_balance * 3 >= total_active_balance * 2:
state.current_justified_checkpoint = Checkpoint(epoch=current_epoch,
root=get_block_root(state, current_epoch))
state.justification_bits[0] = 0b1
# Process finalizations
bits = state.justification_bits
# The 2nd/3rd/4th most recent epochs are justified, the 2nd using the 4th as source
if all(bits[1:4]) and old_previous_justified_checkpoint.epoch + 3 == current_epoch:
state.finalized_checkpoint = old_previous_justified_checkpoint
# The 2nd/3rd most recent epochs are justified, the 2nd using the 3rd as source
if all(bits[1:3]) and old_previous_justified_checkpoint.epoch + 2 == current_epoch:
state.finalized_checkpoint = old_previous_justified_checkpoint
# The 1st/2nd/3rd most recent epochs are justified, the 1st using the 3rd as source
if all(bits[0:3]) and old_current_justified_checkpoint.epoch + 2 == current_epoch:
state.finalized_checkpoint = old_current_justified_checkpoint
# The 1st/2nd most recent epochs are justified, the 1st using the 2nd as source
if all(bits[0:2]) and old_current_justified_checkpoint.epoch + 1 == current_epoch:
state.finalized_checkpoint = old_current_justified_checkpoint
This routine handles justification first, and then finalisation.
Justification
A supermajority link is a vote with a justified source checkpoint and a target checkpoint that was made by validators controlling more than two-thirds of the stake. If a checkpoint has a supermajority link pointing to it then we consider it justified. So, if more than two-thirds of the validators agree that checkpoint 3 was justified (their source vote) and have checkpoint 4 as their target vote, then we justify checkpoint 4.
We know that all the attestations have source votes that we agree with. The first if
statement tries to justify the previous epoch's checkpoint seeing if the (source, target) pair is a supermajority. The second if
statement tries to justify the current epoch's checkpoint. Note that the previous epoch's checkpoint might already have been justified; this is not checked but does not affect the logic.
The justification status of the last four epochs is stored in an array of bits in the state. After shifting the bits along by one at the outset of the routine, the justification status of the current epoch is stored in element 0, the previous in element 1, and so on.
Note that the total_active_balance
is the current epoch's total balance, so it may not be strictly correct for calculating the supermajority for the previous epoch. However, the rate at which the validator set can change between epochs is tightly constrained, so this is not a significant issue.
Finalisation
The version of Casper FFG described in the Gasper paper uses -finality, which extends the handling of finality in the original Casper FFG paper.
In -finality, if we have a consecutive set of justified checkpoints , and a supermajority link from to , then is finalised. Also note that this justifies , by the rules above.
The Casper FFG version of this is -finality. So, a supermajority link from a justified checkpoint to the very next checkpoint both justifies and finalises .
On the beacon chain we are using -finality, since target votes may be included up to an epoch late. In -finality, we keep records of checkpoint justification status for four epochs and have the following conditions for finalisation, where the checkpoint for the current epoch is . Note that we have already updated the justification status of and in this routine, which implies the existence of supermajority links pointing to them if the corresponding bits are set, respectively.
- Checkpoints and are justified, and there is a supermajority link from to : finalise .
- Checkpoint is justified, and there is a supermajority link from to : finalise . This is equivalent to -finality applied to the previous epoch.
- Checkpoints and are justified, and there is a supermajority link from to : finalise .
- Checkpoint is justified, and there is a supermajority link from to : finalise . This is equivalent to -finality applied to the current epoch.
Almost always we would expect to see only the -finality cases, in particular, case 4. The -finality cases would occur only in situations where many attestations are delayed, or when we are very close to the 2/3rds participation threshold. Note that these evaluations stack, so it is possible for rule 2 to finalise and then for rule 4 to immediately finalise , for example.
For the uninitiated, in Python's array slice syntax, bits[1:4]
means bits 1, 2, and 3 (but not 4). This always trips me up.
Used by | process_justification_and_finalization() |
Uses | get_block_root() |
See also | JUSTIFICATION_BITS_LENGTH , Checkpoint |
Inactivity scores
def process_inactivity_updates(state: BeaconState) -> None:
# Skip the genesis epoch as score updates are based on the previous epoch participation
if get_current_epoch(state) == GENESIS_EPOCH:
return
for index in get_eligible_validator_indices(state):
# Increase the inactivity score of inactive validators
if index in get_unslashed_participating_indices(state, TIMELY_TARGET_FLAG_INDEX, get_previous_epoch(state)):
state.inactivity_scores[index] -= min(1, state.inactivity_scores[index])
else:
state.inactivity_scores[index] += INACTIVITY_SCORE_BIAS
# Decrease the inactivity score of all eligible validators during a leak-free epoch
if not is_in_inactivity_leak(state):
state.inactivity_scores[index] -= min(INACTIVITY_SCORE_RECOVERY_RATE, state.inactivity_scores[index])
With Altair, each validator has an individual inactivity score in the beacon state which is updated as follows.
- Every epoch, irrespective of the inactivity leak,
- decrease the score by one when the validator makes a correct timely target vote, and
- increase the score by
INACTIVITY_SCORE_BIAS
otherwise. Note thatget_eligible_validator_indices()
includes slashed but not yet withdrawable validators: slashed validators are treated as not participating, whatever they actually do.
- When not in an inactivity leak
- decrease all validators' scores by
INACTIVITY_SCORE_RECOVERY_RATE
.
- decrease all validators' scores by
There is a floor of zero on the score. So, outside a leak, validators' scores will rapidly return to zero and stay there, since INACTIVITY_SCORE_RECOVERY_RATE
is greater than INACTIVITY_SCORE_BIAS
.
Reward and penalty calculations
Without wanting to go full Yellow Paper on you, I am going to adopt a little notation to help analyse the rewards.
We will define a base reward that we will see turns out to be the expected long-run average income of an optimally performing validator per epoch (ignoring validator set size changes). The total number of active validators is .
The base reward is calculated from a base reward per increment, . An "increment" is a unit of effective balance in terms of EFFECTIVE_BALANCE_INCREMENT
. because MAX_EFFECTIVE_BALANCE
= 32
*
EFFECTIVE_BALANCE_INCREMENT
Other quantities we will use in rewards calculation are the incentivization weights: , , , and being the weights for correct source, target, head, and sync committee votes respectively; being the proposer weight; and the weight denominator which is the sum of the weights.
Issuance for regular rewards happens in four ways:
- is the maximum total reward for all validators attesting in an epoch;
- is the maximum reward issued to proposers in an epoch for including attestations;
- is the maximum total reward for all sync committee participants in an epoch; and
- is the maximum reward issued to proposers in an epoch for including sync aggregates;
Under get_flag_index_deltas()
, process_attestation()
, and process_sync_aggregate()
we find that these work out as follows in terms of and :
To find the total optimal issuance per epoch, we can first sum and ,
Now adding in the proposer rewards,