Nodes and Validators Nodes compose the Ethereum network’s backbone. They communicate with each other and are responsible for validating consensus adherence. Validators are attached to nodes, and despite what their name suggests they are not in charge of any validation. Validators carry out consensus by proposing and voting on new blocks, while validators’ output is validated by nodes to ensure that blocks and transactions adhere to the network’s rules. We will see in the following chapters the validators’ duties in detail.
One peculiar property of PoS to keep in mind is that the set of active validators is known: this will be key to achieve finality, as we can identify when we have achieved a majority vote of participants.
Blocks and Attestations Strict time management is an important property of Ethereum’s Proof of Stake. The two key intervals in PoS are the slot which is exactly 12 seconds, and the epoch which spans 32 slots.
At every slot, exactly one validator is selected to propose a block. At every epoch, every validator gets to share its view of the world exactly once, in the form of an attestation. An attestation contains votes for the head of the chain that will be used by the LMD GHOST protocol, and votes for checkpoints that will be used by the Casper FFG protocol, where FFG stands for “Friendly Finality Gadget,”.
Attestation sharing is bandwidth intensive, so it’s taken on every epoch instead of every block to spread the necessary workload and keep it manageable.
The protocol incentivizes block and attestation production and accuracy via a system of rewards and penalties for validators, but it tolerates empty slots and attestations which can happen for both organic (e.g. node went offline) and profit-driven reasons - we will expand on this in the later section about timing games.
LMD Ghost GHOST is a fork-choice algorithm for selecting the latest block of the chain. It doesn’t follow the heaviest chain as the Bitcoin fork-choice algorithm does, but the heaviest subtree.
The ratio is that, in a PoS system we have way more consensus data than in PoW because every validator votes once per epoch. So we want to use all this information to select the latest block - i.e. the head block - of the chain.
Basically every vote for a block is not only a vote for that block, but also for every ancestor of that block. And the fork choice rule is not only driven by block proposers, but by every validator’s attestations, as the Message Driven (MD) of LMD can suggest. Latest (the L in LMD) means that we only consider the last vote of every validator, discarding old ones.
The whole idea of LMD-Ghost can be described with the following points:
- Every validator casts a vote once per epoch about what he thinks it’s the head of the chain at the moment he publishes the attestation.
- Every block has a score (or weight) obtained by the sum of every validator that votes for that block as the head block of their local chain.
- This score is recursively applied to all branches that root that block.
- Every validator always assumes that the subtree with the heaviest weight is the “right” one.

Now let’s see how LMD-Ghost works in practice.
We have a subtree of blocks, starting from a root block and the weights for every block. We know the weights of every block through attestation messages received via gossip from other validators or directly included in the blocks. In fact, by having other validators’ attestations we can calculate the score of every block by summing the votes that each block has received from all the validators.
The algorithm proceeds recursively, starting from the root block and selecting the branch with the highest weight, until it stops on a leaf node that doesn’t have any descendents. Of course if a block has only one descendent, there’s no choice to be made by the algorithm, while if there is a scenario where two or more branches have an equal weight, LMD-Ghost arbitrarily chooses the branch rooted at the child block with the highest block hash value.
Remember that the universal view of the network, such as being able to see everything that’s going on on the whole network, doesn’t exist. Every validator has its own local view and it runs LMD-Ghost on top of it. The idea is that honest validators build their blocks on the best head they see and cast their votes according to it. And by having a majority (more than 51%) of validators behaving correctly, we can reach a unique consensus about the history of the blockchain.
Let’s see an example. We are now a validator that has to propose the next block or simply has to publish an attestation. We need to run LMD-Ghost on our own local view to get the last head block so that we can then produce the next block on top of it or vote for that head block if we’re just publishing an attestation
First of all, from the latest attestations in our storage, we need to calculate the weight of votes for each block in the tree.

Then, from the weight of the blocks we can derive the weight of each branch.

Last, we start from the root block (block A in this example) and each time we select the branch with the highest score, until we get to a leaf node, that is our head block returned by the fork-choice algorithm.

Note how we first selected the branch that goes to block C even if block C has less votes than block B, because we only care about total votes for the branch, and that includes all the votes for block C’s children. The idea is that the majority of validators the node knows about think that that subtree is the correct chain of blocks to follow.
Incentives Block proposers are implicitly incentivised to produce a block on top of the correct head block, otherwise they risk their block to be orphaned and will not receive any reward. In fact all transactions’ fees of a block, plus a dynamic amount of inflated reward, are directed to the proposer of that block.
Attesters are explicitly incentivised to vote correctly by being rewarded if their vote is added into next blocks. Actually the maximum amount of reward an attester can receive is when his attestation is added into the immediately next slot from when he publishes it. Block proposers are also explicitly incentivised to add as more attestations as possible by receiving a small reward per each vote they add into a block.
Slashing An important concern that Proof of Stake systems have to solve is how they can punish bad behaviors. In PoW systems dishonest participants are implicitly punished because they have to waste time, energy and real money in order to do those bad behaviors.
Think about a Bitcoin miner that wants to create an heaviest chain that contains a double spend. He has to create blocks faster than all other miners together. If he tries to create those blocks and doesn’t succeed, he’s already implicitly punished by the protocol by having wasted his time, his energy and real money to even try and mine those blocks. If he doesn’t end up to be on the chain all other nodes consider to be valid, he actually spent time and money for nothing.
In PoS systems it is almost free for a validator to equivocate by publishing multiple contradictory messages. This is also called the “nothing at stake” problem.
The solution is quite simple but very powerful. Every participant must have a minimum amount of ETH in order to become a validator. Right now this value is fixed at 32 ETH which is more than $60.000 at the time of writing this. The system is able to detect when a validator has equivocated and punishes it by removing a portion of the amount he previously used to become a validator, and ejecting him from the protocol. Since validators digitally sign every message, it’s very easy to detect a proof of misbehavior.
Casper FFG: The Finality Gadget Casper FFG is a kind of meta-consensus protocol. It is an overlay that can be run on top of an underlying consensus protocol in order to add finality to it.
In Ethereum’s proof of stake consensus, the underlying protocol is LMD GHOST which does not provide finality. Finality ensures that blocks once confirmed in the chain cannot be reversed, they will be part of the chain forever. So in essence Casper FFG functions as a “finality gadget”, and we use it to add finality to LMD GHOST.
Casper FFG takes advantage of the fact that, as a proof of stake protocol, we know who our participants are: the validators that manage the staked Ether. This means that we can use vote counting to judge when we have seen a majority of the votes of honest validators. More precisely, votes from validators that manage the majority of the stake - in everything that follows, every validator’s vote is weighted by the value of the stake that it manages, but for simplicity we won’t spell it out every time.
Casper FFG, like all classic Byzantine fault tolerant (BFT) protocols, can ensure finality as long as less than a third of validators are faulty or adversarial. Once a majority of honest validators have declared a block final, all honest validators agree, making that block irreversible. By requiring that honest validators constitute over two-thirds of the total, the system ensures that the consensus accurately represents the honest majority’s view.
Notably, Casper FFG distinguishes itself from traditional BFT protocols by offering economic finality even if more than a third of validators are compromised.
Casper FFG ensures consensus by requiring votes from over two-thirds of validators within an epoch, dividing voting across 32 slots to manage the large validator set efficiently. Validators vote once per epoch on a checkpoint, the first slot, to maintain a unified voting focus. This process, incorporating both Casper FFG and LMD GHOST votes for efficiency, aims at finalizing checkpoints, not entire epochs, clarifying that finality extends to the checkpoint and its preceding content.

Justification and Finalization Casper FFG, like traditional Byzantine Fault Tolerance protocols, secures network agreement in two stages. Initially, validators broadcast and gather views on a proposed checkpoint. If a significant majority agrees, the checkpoint is justified, signaling a tentative agreement. In the subsequent round, if validators confirm widespread support for the justified checkpoint, it achieves finalization, meaning it’s unanimously agreed upon and irreversible. This process underlines the collaborative effort to ensure network consistency and security, aiming for checkpoints to be justified and then finalized within specific timeframes, improving the reliability of the consensus mechanism.
Sources and Targets, Links and Conflicts In Casper FFG, votes comprise source and target checkpoints, representing validators’ commitments to the blockchain’s state at different points. These votes are cast as a linked pair, indicating a validator’s current and proposed points of consensus. The source vote reflects a validator’s acknowledgment of widespread support for a checkpoint, while the target vote represents a conditional commitment to a new checkpoint, dependent on similar support from others. This dual-vote system facilitates a structured progression towards finalizing blocks, ensuring network integrity and continuity.
Supermajority Links In Casper FFG, a supermajority link between source and target checkpoints, s→t, is established when more than two-thirds of validators, by stake weight, endorse the same link, with their votes timely included in the blockchain. This mechanism ensures consensus and security by validating the sequence of checkpoints through widespread validator agreement.
Justification In Casper FFG, when a node observes a majority of validators agreeing on a transition from one checkpoint to another, it justifies the old checkpoint. This signifies that the node has seen evidence of consensus from a significant portion of the validator set, making a commitment not to revert to a previous state unless overwhelming consensus is shown for an alternative path.
Finalization When a node observes a consensus (a supermajority link) from one justified checkpoint to its direct child, it finalizes the parent checkpoint. This indicates a network-wide commitment not to revert from this point, backed by a strong majority of validator support. Finalization ensures network stability and security by making the blockchain history immutable past that checkpoint, preventing reversals without significant consequences for validators.

Fork Choice Rule Casper FFG modifies the traditional fork choice rule, mandating that nodes prioritize the chain with the highest justified checkpoint. This adaptation, which is an evolution from the LMD GHOST protocol’s approach, ensures the network achieves finality by committing to checkpoints that have been agreed upon by a supermajority of validators. It effectively guarantees that once a checkpoint is justified, the network cannot revert beyond it, reinforcing the security and stability of the blockchain. This rule is also designed to maintain network liveness, aligning with Casper’s foundational goals.
Consensus Algorithm
Entrypoint: on_block(store: Store, signed_block: SignedBeaconBlock) -> None
Everything below is DFS on this function.
def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
"""
Run ``on_block`` upon receiving a new block.
"""
block = signed_block.message
# Parent block must be known
assert block.parent_root in store.block_states
# If this block builds on the parent's full payload, that payload must
# have been verified by on_execution_payload_envelope
if is_parent_node_full(store, block):
assert is_payload_verified(store, block.parent_root)
# Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past.
current_slot = get_current_slot(store)
assert current_slot >= block.slot
# Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor)
finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
assert block.slot > finalized_slot
# Check block is a descendant of the finalized block at the checkpoint finalized slot
finalized_checkpoint_block = get_checkpoint_block(
store,
block.parent_root,
store.finalized_checkpoint.epoch,
)
assert store.finalized_checkpoint.root == finalized_checkpoint_block
# Make a copy of the state to avoid mutability issues
state = copy(store.block_states[block.parent_root])
# Check the block is valid and compute the post-state
block_root = hash_tree_root(block)
state_transition(state, signed_block, validate_result=True)
# Compute head before applying the block
head = get_head(store)
# Add new block to the store
store.blocks[block_root] = block
# Add new state for this block to the store
store.block_states[block_root] = state
# Add a new PTC voting for this block to the store
store.payload_timeliness_vote[block_root] = [None] * PTC_SIZE
store.payload_data_availability_vote[block_root] = [None] * PTC_SIZE
# Notify the store about the payload_attestations in the block
notify_ptc_messages(store, state, block.body.payload_attestations)
record_block_timeliness(store, block_root)
update_proposer_boost_root(store, head.root, block_root)
# Update checkpoints in store if necessary
update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint)
# Eagerly compute unrealized justification and finality.
compute_pulled_up_tip(store, block_root)assert block.parent_root in store.block_states:storeessentially just a database for the specs.
@dataclass
class Store:
time: uint64
genesis_time: uint64
justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
unrealized_justified_checkpoint: Checkpoint
unrealized_finalized_checkpoint: Checkpoint
proposer_boost_root: Root
equivocating_indices: Set[ValidatorIndex]
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
block_states: Dict[Root, BeaconState] = field(default_factory=dict)
# [Modified in Gloas:EIP7732]
block_timeliness: Dict[Root, list[boolean]] = field(default_factory=dict)
checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict)
unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict)
# [New in Gloas:EIP7732]
payloads: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict)
# [New in Gloas:EIP7732]
payload_timeliness_vote: Dict[Root, list[Optional[boolean]]] = field(default_factory=dict)
# [New in Gloas:EIP7732]
payload_data_availability_vote: Dict[Root, list[Optional[boolean]]] = field(
default_factory=dict
)So, store.block_states is here instead of reverting the state in case of reorgs. We just collect all the states and use the parent’s one when a new block comes to build on top of it.
if is_parent_node_full(store, block): check if the new block’sparent_block_hashmatches parent’sblock_hash. If it does, then this block build on a full (i.e. a beacon block has an eth1 block inside; later we’ll see how it can be otherwise) node. Anodeis just an entity used in Gasper. Then we check that the payload is verified, i.e. downloaded (+ blobs) and checked with execution layer.current_slot = get_current_slot(store): Ethereum always moves forward and in the actual client you wouldn’t need to fetch the current slot, because there can be only one, the number of current 12 second iteration.
State Transition:
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)Here we just do some bookkeeping: save the roots of the previous slot for light client to be able to verify the data.
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)
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
# [New in Gloas:EIP7732]
# Unset the next payload availability
state.execution_payload_availability[(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT] = 0b0Notice how throughout the state transition we handle the previous slots and epochs in the current ones, i.e. on slot 1 of epoch 10, we handle slot 32 of epoch 9.
def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state)
process_inactivity_updates(state)
process_rewards_and_penalties(state)
process_registry_updates(state)
process_slashings(state)
process_eth1_data_reset(state)
# [Modified in Gloas:EIP8061]
process_pending_deposits(state)
process_pending_consolidations(state)
# [New in Gloas:EIP7732]
process_builder_pending_payments(state)
process_effective_balance_updates(state)
process_slashings_reset(state)
process_randao_mixes_reset(state)
process_historical_summaries_update(state)
process_participation_flag_updates(state)
process_sync_committee_updates(state)
process_proposer_lookahead(state)
# [New in Gloas:EIP7732]
process_ptc_window(state)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
)
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_checkpointWe look at the votes for justification of previous epoch’s slot 1 and if their weight is more than 2/3 of the network, put the justification bit. total_active_balance is for the current epoch, but activation/exit queues are capped, so the difference does not really matter in practice.
The same thing with the current epoch’s slot 1.
case 4: The network is perfectly healthy. Validators are online, blocks are propagating instantly, and there is no severe latency. Epoch N-1 was successfully justified. When validators voted in Epoch N, they saw that N-1 was justified, used it as their source, and successfully justified Epoch N. The chain finalizes smoothly.case 3: Brief network latency or a mini-partition. During Epoch N-1, the network did successfully generate enough votes to justify it. However, because of network lag or a temporary geographical split, the validators assigned to vote in Epoch N did not receive the news that N-1 was justified in time. Playing it safe, the Epoch N validators anchored their votes to the last checkpoint they knew was safe: Epoch N-2. The network still successfully justified Epoch N. The protocol detects this “grandchild” link (N skipping N-1 to link to N-2) and safely finalizes N-2.case 2: In this scenario, Epoch N was a mess. Validators were offline, or there was a bug, and Epoch N completely failed to justify. However, during the chaos of Epoch N, a massive backlog of delayed votes from the previous epoch (Epoch N-1) finally trickled into the network. Those delayed votes pushed Epoch N-1 over the 66% threshold retroactively. The state realizes that N-1 has now been justified using N-2 as a source, so it finalizes N-2, even though the current epoch (N) failed.case 1: Validators in Epoch N-1 did not know N-2 was justified due to extreme latency, so they voted using N-3 as their source. Furthermore, Epoch N-1 failed to get enough votes during its own time limit. Finally, during Epoch N, the network started fixing itself. The delayed votes for N-1 flooded in, retroactively justifying N-1.
Rewards and Penalties
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] += config.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(
config.INACTIVITY_SCORE_RECOVERY_RATE, state.inactivity_scores[index]
)
def process_rewards_and_penalties(state: BeaconState) -> None:
# No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch
if get_current_epoch(state) == GENESIS_EPOCH:
return
flag_deltas = [
get_flag_index_deltas(state, flag_index)
for flag_index in range(len(PARTICIPATION_FLAG_WEIGHTS))
]
deltas = flag_deltas + [get_inactivity_penalty_deltas(state)]
for rewards, penalties in deltas:
for index in range(len(state.validators)):
increase_balance(state, ValidatorIndex(index), rewards[index])
decrease_balance(state, ValidatorIndex(index), penalties[index])
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
def get_inactivity_penalty_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence[Gwei]]:
"""
Return the inactivity penalty deltas by considering timely target participation flags and inactivity scores.
"""
rewards = [Gwei(0) for _ in range(len(state.validators))]
penalties = [Gwei(0) for _ in range(len(state.validators))]
previous_epoch = get_previous_epoch(state)
matching_target_indices = get_unslashed_participating_indices(
state, TIMELY_TARGET_FLAG_INDEX, previous_epoch
)
for index in get_eligible_validator_indices(state):
if index not in matching_target_indices:
penalty_numerator = (
state.validators[index].effective_balance * state.inactivity_scores[index]
)
# [Modified in Bellatrix]
penalty_denominator = config.INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_BELLATRIX
penalties[index] += Gwei(penalty_numerator // penalty_denominator)
return rewards, penaltiesAsk AI for explanation of the formulas of Ethereum rewards and penalties
process_inactivity_updates:inactivity_scoresis a list of uint64 and we update it to punish an inactive validator more and more with time. But we apply this penalty and increase the score only when we are in the inactivity leak (failed to finalize a bloc for 4 epochs). Once the network is healthy, inactive validators rapidly lose this score. When the network is healthy, essentially, list is full of zeros.INACTIVITY_SCORE_RECOVERY_RATE= 16;INACTIVITY_SCORE_BIAS= 4.get_flag_index_deltas: When we handle an attestation, we set the flags for each validator if they are timely with their vote. Slashed validators are eligible, but cannot vote, so they fall underelif flag_index != TIMELY_HEAD_FLAG_INDEXin every epoch until they are fully withdrawable.
Registry and Effective Balance
def process_registry_updates(state: BeaconState) -> None:
current_epoch = get_current_epoch(state)
activation_epoch = compute_activation_exit_epoch(current_epoch)
# Process activation eligibility, ejections, and activations
for index, validator in enumerate(state.validators):
# [Modified in Electra:EIP7251]
if is_eligible_for_activation_queue(validator):
validator.activation_eligibility_epoch = current_epoch + 1
elif (
is_active_validator(validator, current_epoch)
and validator.effective_balance <= config.EJECTION_BALANCE
):
# [Modified in Electra:EIP7251]
initiate_validator_exit(state, ValidatorIndex(index))
elif is_eligible_for_activation(state, validator):
validator.activation_epoch = activation_epoch
def is_eligible_for_activation_queue(validator: Validator) -> bool:
"""
Check if ``validator`` is eligible to be placed into the activation queue.
"""
return (
validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
# [Modified in Electra:EIP7251]
and validator.effective_balance >= MIN_ACTIVATION_BALANCE
)
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
)
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 [Modified in Electra:EIP7251]
exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance)
# Set validator exit epoch and withdrawable epoch
validator.exit_epoch = exit_queue_epoch
validator.withdrawable_epoch = Epoch(validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch:
earliest_exit_epoch = max(
state.earliest_exit_epoch, compute_activation_exit_epoch(get_current_epoch(state))
)
# [Modified in Gloas:EIP8061]
per_epoch_churn = get_exit_churn_limit(state)
# New epoch for exits.
if state.earliest_exit_epoch < earliest_exit_epoch:
exit_balance_to_consume = per_epoch_churn
else:
exit_balance_to_consume = state.exit_balance_to_consume
# Exit doesn't fit in the current earliest epoch.
if exit_balance > exit_balance_to_consume:
balance_to_process = exit_balance - exit_balance_to_consume
additional_epochs = (balance_to_process - 1) // per_epoch_churn + 1
earliest_exit_epoch += additional_epochs
exit_balance_to_consume += additional_epochs * per_epoch_churn
# Consume the balance and update state variables.
state.exit_balance_to_consume = exit_balance_to_consume - exit_balance
state.earliest_exit_epoch = earliest_exit_epoch
return state.earliest_exit_epoch
def get_exit_churn_limit(state: BeaconState) -> Gwei:
"""
Per-epoch churn limit for exits, rounded to
``EFFECTIVE_BALANCE_INCREMENT``.
"""
churn = max(
config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA,
get_total_active_balance(state) // config.CHURN_LIMIT_QUOTIENT_GLOAS,
)
return churn - churn % EFFECTIVE_BALANCE_INCREMENTSlashing
def process_slashings(state: BeaconState) -> None:
epoch = get_current_epoch(state)
total_balance = get_total_active_balance(state)
adjusted_total_slashing_balance = min(
sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX, total_balance
)
increment = (
EFFECTIVE_BALANCE_INCREMENT # Factored out from total balance to avoid uint64 overflow
)
penalty_per_effective_balance_increment = adjusted_total_slashing_balance // (
total_balance // increment
)
for index, validator in enumerate(state.validators):
if (
validator.slashed
and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch
):
effective_balance_increments = validator.effective_balance // increment
# [Modified in Electra:EIP7251]
penalty = penalty_per_effective_balance_increment * effective_balance_increments
decrease_balance(state, ValidatorIndex(index), penalty)EPOCHS_PER_SLASHINGS_VECTOR == 8192 epochs (36 days).
The logic is, if lots of validators were found malicious within 36 days, 18 days after slashing this one, we check it and punish proportionally. 1/3 of all the validators == massive attack, therefore full nullification of the slashed validators’ balances. 0 other validators were slashed == punishment is 0. Penalties for inactivity and immediate ones are still there, of course.
Deposits, Consilidations and Builder Payments
def process_pending_deposits(state: BeaconState) -> None:
next_epoch = Epoch(get_current_epoch(state) + 1)
# [Modified in Gloas:EIP8061]
# Deposits still consume the activation-only churn budget in Gloas.
available_for_processing = state.deposit_balance_to_consume + get_activation_churn_limit(state)
processed_amount = 0
next_deposit_index = 0
deposits_to_postpone = []
is_churn_limit_reached = False
finalized_slot = compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)
for deposit in state.pending_deposits:
# Check if deposit has been finalized, otherwise, stop processing.
if deposit.slot > finalized_slot:
break
# Check if number of processed deposits has not reached the limit, otherwise, stop processing.
if next_deposit_index >= MAX_PENDING_DEPOSITS_PER_EPOCH:
break
# Read validator state
is_validator_exited = False
is_validator_withdrawn = False
validator_pubkeys = [v.pubkey for v in state.validators]
if deposit.pubkey in validator_pubkeys:
validator = state.validators[ValidatorIndex(validator_pubkeys.index(deposit.pubkey))]
is_validator_exited = validator.exit_epoch < FAR_FUTURE_EPOCH
is_validator_withdrawn = validator.withdrawable_epoch < next_epoch
if is_validator_withdrawn:
# Deposited balance will never become active. Increase balance but do not consume churn
apply_pending_deposit(state, deposit)
elif is_validator_exited:
# Validator is exiting, postpone the deposit until after withdrawable epoch
deposits_to_postpone.append(deposit)
else:
# Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch.
is_churn_limit_reached = processed_amount + deposit.amount > available_for_processing
if is_churn_limit_reached:
break
# Consume churn and apply deposit.
processed_amount += deposit.amount
apply_pending_deposit(state, deposit)
# Regardless of how the deposit was handled, we move on in the queue.
next_deposit_index += 1
state.pending_deposits = state.pending_deposits[next_deposit_index:] + deposits_to_postpone
# Accumulate churn only if the churn limit has been hit.
if is_churn_limit_reached:
state.deposit_balance_to_consume = available_for_processing - processed_amount
else:
state.deposit_balance_to_consume = Gwei(0)
def apply_pending_deposit(state: BeaconState, deposit: PendingDeposit) -> None:
"""
Applies ``deposit`` to the ``state``.
"""
validator_pubkeys = [v.pubkey for v in state.validators]
if deposit.pubkey not in validator_pubkeys:
# Verify the deposit signature (proof of possession) which is not checked by the deposit contract
if is_valid_deposit_signature(
deposit.pubkey, deposit.withdrawal_credentials, deposit.amount, deposit.signature
):
add_validator_to_registry(
state, deposit.pubkey, deposit.withdrawal_credentials, deposit.amount
)
else:
validator_index = ValidatorIndex(validator_pubkeys.index(deposit.pubkey))
increase_balance(state, validator_index, deposit.amount)
def add_validator_to_registry(
state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64
) -> None:
index = get_index_for_new_validator(state)
# [Modified in Electra:EIP7251]
validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount)
set_or_append_list(state.validators, index, validator)
set_or_append_list(state.balances, index, amount)
set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000))
set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000))
set_or_append_list(state.inactivity_scores, index, uint64(0))def process_pending_consolidations(state: BeaconState) -> None:
next_epoch = Epoch(get_current_epoch(state) + 1)
next_pending_consolidation = 0
for pending_consolidation in state.pending_consolidations:
source_validator = state.validators[pending_consolidation.source_index]
if source_validator.slashed:
next_pending_consolidation += 1
continue
if source_validator.withdrawable_epoch > next_epoch:
break
# Calculate the consolidated balance
source_effective_balance = min(
state.balances[pending_consolidation.source_index], source_validator.effective_balance
)
# Move active balance to target. Excess balance is withdrawable.
decrease_balance(state, pending_consolidation.source_index, source_effective_balance)
increase_balance(state, pending_consolidation.target_index, source_effective_balance)
next_pending_consolidation += 1
state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:]def process_builder_pending_payments(state: BeaconState) -> None:
"""
Processes the builder pending payments from the previous epoch.
"""
quorum = get_builder_payment_quorum_threshold(state)
for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]:
if payment.weight >= quorum:
state.builder_pending_withdrawals.append(payment.withdrawal)
old_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:]
new_payments = [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)]
state.builder_pending_payments = old_payments + new_payments
def get_builder_payment_quorum_threshold(state: BeaconState) -> uint64:
"""
Calculate the quorum threshold for builder payments.
"""
per_slot_balance = get_total_active_balance(state) // SLOTS_PER_EPOCH
quorum = per_slot_balance * BUILDER_PAYMENT_THRESHOLD_NUMERATOR
return uint64(quorum // BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)In ePBS, a block builder promises to pay a proposer a fee for choosing their block. However, the protocol cannot simply transfer the money instantly at the moment the block is proposed. If it did, a malicious proposer could take the payment even if the builder failed to actually reveal the payload (DoS attack by the proposer on the builder, so that he will not be able to propagate the block to the network and the beacon block would be empty of transactions), or if the block was later reorganized out of the chain (i.e. the proposer sees the contents of the block, changes them to take that MEV for themselves, equivocates in the first 4 seconds and sends “header B” to more peers in the network than “header A”, pays the slashing penalty but keeps the MEV).
Bookkeeping
def process_eth1_data_reset(state: BeaconState) -> None:
next_epoch = Epoch(get_current_epoch(state) + 1)
# Reset eth1 data votes
if next_epoch % EPOCHS_PER_ETH1_VOTING_PERIOD == 0:
state.eth1_data_votes = []
def process_slashings_reset(state: BeaconState) -> None:
next_epoch = Epoch(get_current_epoch(state) + 1)
# Reset slashings
state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0)
def process_randao_mixes_reset(state: BeaconState) -> None:
current_epoch = get_current_epoch(state)
next_epoch = Epoch(current_epoch + 1)
# Set randao mix
state.randao_mixes[next_epoch % EPOCHS_PER_HISTORICAL_VECTOR] = get_randao_mix(
state, current_epoch
)
def process_historical_summaries_update(state: BeaconState) -> None:
# Set historical block root accumulator.
next_epoch = Epoch(get_current_epoch(state) + 1)
if next_epoch % (SLOTS_PER_HISTORICAL_ROOT // SLOTS_PER_EPOCH) == 0:
historical_summary = HistoricalSummary(
block_summary_root=hash_tree_root(state.block_roots),
state_summary_root=hash_tree_root(state.state_roots),
)
state.historical_summaries.append(historical_summary)
def process_participation_flag_updates(state: BeaconState) -> None:
state.previous_epoch_participation = state.current_epoch_participation
state.current_epoch_participation = [
ParticipationFlags(0b0000_0000) for _ in range(len(state.validators))
]
def process_sync_committee_updates(state: BeaconState) -> None:
next_epoch = get_current_epoch(state) + Epoch(1)
if next_epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD == 0:
state.current_sync_committee = state.next_sync_committee
state.next_sync_committee = get_next_sync_committee(state)
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)
def process_proposer_lookahead(state: BeaconState) -> None:
last_epoch_start = len(state.proposer_lookahead) - SLOTS_PER_EPOCH
# Shift out proposers in the first epoch
state.proposer_lookahead[:last_epoch_start] = state.proposer_lookahead[SLOTS_PER_EPOCH:]
# Fill in the last epoch with new proposer indices
last_epoch_proposers = get_beacon_proposer_indices(
state, Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1)
)
state.proposer_lookahead[last_epoch_start:] = last_epoch_proposers
def get_beacon_proposer_indices(
state: BeaconState, epoch: Epoch
) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]:
"""
Return the proposer indices for the given ``epoch``.
"""
# [Modified in Gloas:EIP8045]
indices = [
index
for index in get_active_validator_indices(state, epoch)
if not state.validators[index].slashed
]
seed = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER)
return compute_proposer_indices(state, epoch, seed, indices)
def process_ptc_window(state: BeaconState) -> None:
"""
Update the cached PTC window.
"""
# Shift all epochs forward by one
state.ptc_window[: len(state.ptc_window) - SLOTS_PER_EPOCH] = state.ptc_window[SLOTS_PER_EPOCH:]
# Fill in the last epoch
next_epoch = Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1)
start_slot = compute_start_slot_at_epoch(next_epoch)
state.ptc_window[len(state.ptc_window) - SLOTS_PER_EPOCH :] = [
compute_ptc(state, Slot(slot)) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH)
]
def compute_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
"""
Get the payload timeliness committee, with possible duplicates, for the given ``slot``.
"""
epoch = compute_epoch_at_slot(slot)
seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
indices: List[ValidatorIndex] = []
# Concatenate all committees for this slot in order
committees_per_slot = get_committee_count_per_slot(state, epoch)
for i in range(committees_per_slot):
committee = get_beacon_committee(state, slot, CommitteeIndex(i))
indices.extend(committee)
return compute_balance_weighted_selection(
state, indices, seed, size=PTC_SIZE, shuffle_indices=False
)Nothing interesting: clear the flags, compute the new committees, save the roots so that anyone could verify the data about the state and block, etc.
Process Block
def process_block(state: BeaconState, block: BeaconBlock) -> None:
# [New in Gloas:EIP7732]
process_parent_execution_payload(state, block)
process_block_header(state, block)
# [Modified in Gloas:EIP7732]
process_withdrawals(state)
# [Modified in Gloas:EIP7732]
# Removed `process_execution_payload`
# [New in Gloas:EIP7732]
process_execution_payload_bid(state, block)
process_randao(state, block.body)
process_eth1_data(state, block.body)
# [Modified in Gloas:EIP7732]
process_operations(state, block.body)
process_sync_aggregate(state, block.body.sync_aggregate)def process_block_header(state: BeaconState, block: BeaconBlock) -> None:
# Verify that the slots match
assert block.slot == state.slot
# Verify that the block is newer than latest block header
assert block.slot > state.latest_block_header.slot
# Verify that proposer index is the correct index
assert block.proposer_index == get_beacon_proposer_index(state)
# Verify that the parent matches
assert block.parent_root == hash_tree_root(state.latest_block_header)
# Cache current block as the new latest block
state.latest_block_header = BeaconBlockHeader(
slot=block.slot,
proposer_index=block.proposer_index,
parent_root=block.parent_root,
state_root=Bytes32(), # Overwritten in the next process_slot call
body_root=hash_tree_root(block.body),
)
# Verify proposer is not slashed
proposer = state.validators[block.proposer_index]
assert not proposer.slashedProcess Execution Requests
def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None:
bid = block.body.signed_execution_payload_bid.message
parent_bid = state.latest_execution_payload_bid
requests = block.body.parent_execution_requests
if bid.parent_block_hash != parent_bid.block_hash:
# Parent was EMPTY -- no execution requests expected
assert requests == ExecutionRequests()
return
# Parent was FULL -- verify the bid commitment and apply the payload
assert hash_tree_root(requests) == parent_bid.execution_requests_root
apply_parent_execution_payload(state, requests)
def apply_parent_execution_payload(
state: BeaconState,
requests: ExecutionRequests,
) -> None:
parent_bid = state.latest_execution_payload_bid
parent_slot = parent_bid.slot
parent_epoch = compute_epoch_at_slot(parent_slot)
# Process execution requests from parent's payload. The execution
# requests are processed at state.slot (child's slot), not the parent's slot.
def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
for operation in operations:
fn(state, operation)
for_ops(requests.deposits, process_deposit_request)
for_ops(requests.withdrawals, process_withdrawal_request)
for_ops(requests.consolidations, process_consolidation_request)
# Settle the builder payment
if parent_epoch == get_current_epoch(state):
payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH
settle_builder_payment(state, payment_index)
elif parent_epoch == get_previous_epoch(state):
payment_index = parent_slot % SLOTS_PER_EPOCH
settle_builder_payment(state, payment_index)
elif parent_bid.value > 0:
# Parent is older than the previous epoch, its payment entry has been
# evicted from builder_pending_payments. Append the withdrawal directly.
state.builder_pending_withdrawals.append(
BuilderPendingWithdrawal(
fee_recipient=parent_bid.fee_recipient,
amount=parent_bid.value,
builder_index=parent_bid.builder_index,
)
)
# Update parent payload availability and latest block hash
state.execution_payload_availability[parent_slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1
state.latest_block_hash = parent_bid.block_hashelif parent_bid.value > 0:: if network goes complete offline for an epoch, then the new block would reference to a parent block out of builder_pending_payments’ range.
def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None:
# [New in Gloas:EIP7732]
builder_pubkeys = [b.pubkey for b in state.builders]
validator_pubkeys = [v.pubkey for v in state.validators]
# [New in Gloas:EIP7732]
# Regardless of the withdrawal credentials prefix, if a builder/validator
# already exists with this pubkey, apply the deposit to their balance
is_builder = deposit_request.pubkey in builder_pubkeys
is_validator = deposit_request.pubkey in validator_pubkeys
if is_builder or (
is_builder_withdrawal_credential(deposit_request.withdrawal_credentials)
and not is_validator
and not is_pending_validator(state.pending_deposits, deposit_request.pubkey)
):
# Apply builder deposits immediately
apply_deposit_for_builder(
state,
deposit_request.pubkey,
deposit_request.withdrawal_credentials,
deposit_request.amount,
deposit_request.signature,
state.slot,
)
return
# Add validator deposits to the queue
state.pending_deposits.append(
PendingDeposit(
pubkey=deposit_request.pubkey,
withdrawal_credentials=deposit_request.withdrawal_credentials,
amount=deposit_request.amount,
signature=deposit_request.signature,
slot=state.slot,
)
)
def apply_deposit_for_builder(
state: BeaconState,
pubkey: BLSPubkey,
withdrawal_credentials: Bytes32,
amount: uint64,
signature: BLSSignature,
slot: Slot,
) -> None:
builder_pubkeys = [b.pubkey for b in state.builders]
if pubkey not in builder_pubkeys:
# Verify the deposit signature (proof of possession) which is not checked by the deposit contract
if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature):
add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot)
else:
# Increase balance by deposit amount
builder_index = builder_pubkeys.index(pubkey)
state.builders[builder_index].balance += amountdef process_withdrawal_request(state: BeaconState, withdrawal_request: WithdrawalRequest) -> None:
amount = withdrawal_request.amount
is_full_exit_request = amount == FULL_EXIT_REQUEST_AMOUNT
# If partial withdrawal queue is full, only full exits are processed
if (
len(state.pending_partial_withdrawals) == PENDING_PARTIAL_WITHDRAWALS_LIMIT
and not is_full_exit_request
):
return
validator_pubkeys = [v.pubkey for v in state.validators]
# Verify pubkey exists
request_pubkey = withdrawal_request.validator_pubkey
if request_pubkey not in validator_pubkeys:
return
index = ValidatorIndex(validator_pubkeys.index(request_pubkey))
validator = state.validators[index]
# Verify withdrawal credentials
has_correct_credential = has_execution_withdrawal_credential(validator)
is_correct_source_address = (
validator.withdrawal_credentials[12:] == withdrawal_request.source_address
)
if not (has_correct_credential and is_correct_source_address):
return
# Verify the validator is active
if not is_active_validator(validator, get_current_epoch(state)):
return
# Verify exit has not been initiated
if validator.exit_epoch != FAR_FUTURE_EPOCH:
return
# Verify the validator has been active long enough
if get_current_epoch(state) < validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD:
return
pending_balance_to_withdraw = get_pending_balance_to_withdraw(state, index)
if is_full_exit_request:
# Only exit validator if it has no pending withdrawals in the queue
if pending_balance_to_withdraw == 0:
initiate_validator_exit(state, index)
return
has_sufficient_effective_balance = validator.effective_balance >= MIN_ACTIVATION_BALANCE
has_excess_balance = (
state.balances[index] > MIN_ACTIVATION_BALANCE + pending_balance_to_withdraw
)
# Only allow partial withdrawals with compounding withdrawal credentials
if (
has_compounding_withdrawal_credential(validator)
and has_sufficient_effective_balance
and has_excess_balance
):
to_withdraw = min(
state.balances[index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw, amount
)
exit_queue_epoch = compute_exit_epoch_and_update_churn(state, to_withdraw)
withdrawable_epoch = Epoch(exit_queue_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
state.pending_partial_withdrawals.append(
PendingPartialWithdrawal(
validator_index=index,
amount=to_withdraw,
withdrawable_epoch=withdrawable_epoch,
)
)def process_consolidation_request(
state: BeaconState, consolidation_request: ConsolidationRequest
) -> None:
if is_valid_switch_to_compounding_request(state, consolidation_request):
validator_pubkeys = [v.pubkey for v in state.validators]
request_source_pubkey = consolidation_request.source_pubkey
source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey))
switch_to_compounding_validator(state, source_index)
return
# Verify that source != target, so a consolidation cannot be used as an exit
if consolidation_request.source_pubkey == consolidation_request.target_pubkey:
return
# If the pending consolidations queue is full, consolidation requests are ignored
if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT:
return
# If there is too little available consolidation churn limit, consolidation requests are ignored
if get_consolidation_churn_limit(state) <= MIN_ACTIVATION_BALANCE:
return
validator_pubkeys = [v.pubkey for v in state.validators]
# Verify pubkeys exists
request_source_pubkey = consolidation_request.source_pubkey
request_target_pubkey = consolidation_request.target_pubkey
if request_source_pubkey not in validator_pubkeys:
return
if request_target_pubkey not in validator_pubkeys:
return
source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey))
target_index = ValidatorIndex(validator_pubkeys.index(request_target_pubkey))
source_validator = state.validators[source_index]
target_validator = state.validators[target_index]
# Verify source withdrawal credentials
has_correct_credential = has_execution_withdrawal_credential(source_validator)
is_correct_source_address = (
source_validator.withdrawal_credentials[12:] == consolidation_request.source_address
)
if not (has_correct_credential and is_correct_source_address):
return
# Verify that target has compounding withdrawal credentials
if not has_compounding_withdrawal_credential(target_validator):
return
# Verify the source and the target are active
current_epoch = get_current_epoch(state)
if not is_active_validator(source_validator, current_epoch):
return
if not is_active_validator(target_validator, current_epoch):
return
# Verify exits for source and target have not been initiated
if source_validator.exit_epoch != FAR_FUTURE_EPOCH:
return
if target_validator.exit_epoch != FAR_FUTURE_EPOCH:
return
# Verify the source has been active long enough
if current_epoch < source_validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD:
return
# Verify the source has no pending withdrawals in the queue
if get_pending_balance_to_withdraw(state, source_index) > 0:
return
# Initiate source validator exit and append pending consolidation
source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn(
state, source_validator.effective_balance
)
source_validator.withdrawable_epoch = Epoch(
source_validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY
)
state.pending_consolidations.append(
PendingConsolidation(source_index=source_index, target_index=target_index)
)
def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None:
validator = state.validators[index]
validator.withdrawal_credentials = (
COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:]
)
queue_excess_active_balance(state, index)
def queue_excess_active_balance(state: BeaconState, index: ValidatorIndex) -> None:
balance = state.balances[index]
if balance > MIN_ACTIVATION_BALANCE:
excess_balance = balance - MIN_ACTIVATION_BALANCE
state.balances[index] = MIN_ACTIVATION_BALANCE
validator = state.validators[index]
# Use bls.G2_POINT_AT_INFINITY as a signature field placeholder
# and GENESIS_SLOT to distinguish from a pending deposit request
state.pending_deposits.append(
PendingDeposit(
pubkey=validator.pubkey,
withdrawal_credentials=validator.withdrawal_credentials,
amount=excess_balance,
signature=bls.G2_POINT_AT_INFINITY,
slot=GENESIS_SLOT,
)
)queue_excess_active_balance: We subject excess balance to the activation limit, because otherwise rapid change to voting power of validators (effective_balance recalculation at the end of the epochs) might pose a thread to consensus.
Process Withdrawals
def process_withdrawals(
state: BeaconState,
# [Modified in Gloas:EIP7732]
# Removed `payload`
) -> None:
# [New in Gloas:EIP7732]
# Return early if the parent block is empty
if state.latest_block_hash != state.latest_execution_payload_bid.block_hash:
return
# Get expected withdrawals
expected = get_expected_withdrawals(state)
# Apply expected withdrawals
apply_withdrawals(state, expected.withdrawals)
# Update withdrawals fields in the state
update_next_withdrawal_index(state, expected.withdrawals)
# [New in Gloas:EIP7732]
update_payload_expected_withdrawals(state, expected.withdrawals)
# [New in Gloas:EIP7732]
update_builder_pending_withdrawals(state, expected.processed_builder_withdrawals_count)
update_pending_partial_withdrawals(state, expected.processed_partial_withdrawals_count)
# [New in Gloas:EIP7732]
update_next_withdrawal_builder_index(state, expected.processed_builders_sweep_count)
update_next_withdrawal_validator_index(state, expected.withdrawals)def get_expected_withdrawals(state: BeaconState) -> ExpectedWithdrawals:
withdrawal_index = state.next_withdrawal_index
withdrawals: List[Withdrawal] = []
# [New in Gloas:EIP7732]
# Get builder withdrawals
builder_withdrawals, withdrawal_index, processed_builder_withdrawals_count = (
get_builder_withdrawals(state, withdrawal_index, withdrawals)
)
withdrawals.extend(builder_withdrawals)
# Get partial withdrawals
partial_withdrawals, withdrawal_index, processed_partial_withdrawals_count = (
get_pending_partial_withdrawals(state, withdrawal_index, withdrawals)
)
withdrawals.extend(partial_withdrawals)
# [New in Gloas:EIP7732]
# Get builders sweep withdrawals
builders_sweep_withdrawals, withdrawal_index, processed_builders_sweep_count = (
get_builders_sweep_withdrawals(state, withdrawal_index, withdrawals)
)
withdrawals.extend(builders_sweep_withdrawals)
# Get validators sweep withdrawals
validators_sweep_withdrawals, withdrawal_index, processed_validators_sweep_count = (
get_validators_sweep_withdrawals(state, withdrawal_index, withdrawals)
)
withdrawals.extend(validators_sweep_withdrawals)
return ExpectedWithdrawals(
withdrawals,
# [New in Gloas:EIP7732]
processed_builder_withdrawals_count,
processed_partial_withdrawals_count,
# [New in Gloas:EIP7732]
processed_builders_sweep_count,
processed_validators_sweep_count,
)
def get_builder_withdrawals(
state: BeaconState,
withdrawal_index: WithdrawalIndex,
prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]:
withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1
assert len(prior_withdrawals) <= withdrawals_limit
processed_count: uint64 = 0
withdrawals: List[Withdrawal] = []
for withdrawal in state.builder_pending_withdrawals:
all_withdrawals = prior_withdrawals + withdrawals
has_reached_limit = len(all_withdrawals) >= withdrawals_limit
if has_reached_limit:
break
builder_index = withdrawal.builder_index
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=convert_builder_index_to_validator_index(builder_index),
address=withdrawal.fee_recipient,
amount=withdrawal.amount,
)
)
withdrawal_index += WithdrawalIndex(1)
processed_count += 1
return withdrawals, withdrawal_index, processed_count
def get_pending_partial_withdrawals(
state: BeaconState,
withdrawal_index: WithdrawalIndex,
prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]:
epoch = get_current_epoch(state)
withdrawals_limit = min(
len(prior_withdrawals) + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
MAX_WITHDRAWALS_PER_PAYLOAD - 1,
)
assert len(prior_withdrawals) <= withdrawals_limit
processed_count: uint64 = 0
withdrawals: List[Withdrawal] = []
for withdrawal in state.pending_partial_withdrawals:
all_withdrawals = prior_withdrawals + withdrawals
is_withdrawable = withdrawal.withdrawable_epoch <= epoch
has_reached_limit = len(all_withdrawals) >= withdrawals_limit
if not is_withdrawable or has_reached_limit:
break
validator_index = withdrawal.validator_index
validator = state.validators[validator_index]
balance = get_balance_after_withdrawals(state, validator_index, all_withdrawals)
if is_eligible_for_partial_withdrawals(validator, balance):
withdrawal_amount = min(balance - MIN_ACTIVATION_BALANCE, withdrawal.amount)
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=validator_index,
address=ExecutionAddress(validator.withdrawal_credentials[12:]),
amount=withdrawal_amount,
)
)
withdrawal_index += WithdrawalIndex(1)
processed_count += 1
return withdrawals, withdrawal_index, processed_count
def get_builders_sweep_withdrawals(
state: BeaconState,
withdrawal_index: WithdrawalIndex,
prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]:
epoch = get_current_epoch(state)
builders_limit = min(len(state.builders), MAX_BUILDERS_PER_WITHDRAWALS_SWEEP)
withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1
assert len(prior_withdrawals) <= withdrawals_limit
processed_count: uint64 = 0
withdrawals: List[Withdrawal] = []
builder_index = state.next_withdrawal_builder_index
for _ in range(builders_limit):
all_withdrawals = prior_withdrawals + withdrawals
has_reached_limit = len(all_withdrawals) >= withdrawals_limit
if has_reached_limit:
break
builder = state.builders[builder_index]
if builder.withdrawable_epoch <= epoch and builder.balance > 0:
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=convert_builder_index_to_validator_index(builder_index),
address=builder.execution_address,
amount=builder.balance,
)
)
withdrawal_index += WithdrawalIndex(1)
builder_index = BuilderIndex((builder_index + 1) % len(state.builders))
processed_count += 1
return withdrawals, withdrawal_index, processed_count
def get_validators_sweep_withdrawals(
state: BeaconState,
withdrawal_index: WithdrawalIndex,
prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]:
epoch = get_current_epoch(state)
validators_limit = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP)
withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD
# There must be at least one space reserved for validator sweep withdrawals
assert len(prior_withdrawals) < withdrawals_limit
processed_count: uint64 = 0
withdrawals: List[Withdrawal] = []
validator_index = state.next_withdrawal_validator_index
for _ in range(validators_limit):
all_withdrawals = prior_withdrawals + withdrawals
has_reached_limit = len(all_withdrawals) >= withdrawals_limit
if has_reached_limit:
break
validator = state.validators[validator_index]
balance = get_balance_after_withdrawals(state, validator_index, all_withdrawals)
if is_fully_withdrawable_validator(validator, balance, epoch):
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=validator_index,
address=ExecutionAddress(validator.withdrawal_credentials[12:]),
amount=balance,
)
)
withdrawal_index += WithdrawalIndex(1)
elif is_partially_withdrawable_validator(validator, balance):
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=validator_index,
address=ExecutionAddress(validator.withdrawal_credentials[12:]),
# [Modified in Electra:EIP7251]
amount=balance - get_max_effective_balance(validator),
)
)
withdrawal_index += WithdrawalIndex(1)
validator_index = ValidatorIndex((validator_index + 1) % len(state.validators))
processed_count += 1
return withdrawals, withdrawal_index, processed_countMAX_WITHDRAWALS_PER_PAYLOAD - 1: If it was allowed to consume all 16 slots, a highly active network could fill every block with some type of withdrawals and never let the other ones through.
def apply_withdrawals(state: BeaconState, withdrawals: Sequence[Withdrawal]) -> None:
for withdrawal in withdrawals:
# [Modified in Gloas:EIP7732]
if is_builder_index(withdrawal.validator_index):
builder_index = convert_validator_index_to_builder_index(withdrawal.validator_index)
builder_balance = state.builders[builder_index].balance
state.builders[builder_index].balance -= min(withdrawal.amount, builder_balance)
else:
decrease_balance(state, withdrawal.validator_index, withdrawal.amount)
def update_payload_expected_withdrawals(
state: BeaconState, withdrawals: Sequence[Withdrawal]
) -> None:
state.payload_expected_withdrawals = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawals)
def update_builder_pending_withdrawals(
state: BeaconState, processed_builder_withdrawals_count: uint64
) -> None:
state.builder_pending_withdrawals = state.builder_pending_withdrawals[
processed_builder_withdrawals_count:
]
def update_pending_partial_withdrawals(
state: BeaconState, processed_partial_withdrawals_count: uint64
) -> None:
state.pending_partial_withdrawals = state.pending_partial_withdrawals[
processed_partial_withdrawals_count:
]
def update_next_withdrawal_builder_index(
state: BeaconState, processed_builders_sweep_count: uint64
) -> None:
if len(state.builders) > 0:
# Update the next builder index to start the next withdrawal sweep
next_index = state.next_withdrawal_builder_index + processed_builders_sweep_count
next_builder_index = BuilderIndex(next_index % len(state.builders))
state.next_withdrawal_builder_index = next_builder_index
def update_next_withdrawal_index(state: BeaconState, withdrawals: Sequence[Withdrawal]) -> None:
# Update the next withdrawal index if this block contained withdrawals
if len(withdrawals) != 0:
latest_withdrawal = withdrawals[-1]
state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1)
def update_next_withdrawal_validator_index(
state: BeaconState, withdrawals: Sequence[Withdrawal]
) -> None:
# Update the next validator index to start the next withdrawal sweep
if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD:
# Next sweep starts after the latest withdrawal's validator index
next_validator_index = ValidatorIndex(
(withdrawals[-1].validator_index + 1) % len(state.validators)
)
state.next_withdrawal_validator_index = next_validator_index
else:
# Advance sweep by the max length of the sweep if there was not a full set of withdrawals
next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP
next_validator_index = ValidatorIndex(next_index % len(state.validators))
state.next_withdrawal_validator_index = next_validator_indexProcess Execution Payload Bid
def process_execution_payload_bid(state: BeaconState, block: BeaconBlock) -> None:
signed_bid = block.body.signed_execution_payload_bid
bid = signed_bid.message
builder_index = bid.builder_index
amount = bid.value
# For self-builds, amount must be zero regardless of withdrawal credential prefix
if builder_index == BUILDER_INDEX_SELF_BUILD:
assert amount == 0
assert signed_bid.signature == bls.G2_POINT_AT_INFINITY
else:
# Verify that the builder is active
assert is_active_builder(state, builder_index)
# Verify that the builder has funds to cover the bid
assert can_builder_cover_bid(state, builder_index, amount)
# Verify that the bid signature is valid
assert verify_execution_payload_bid_signature(state, signed_bid)
# Verify commitments are under limit
assert (
len(bid.blob_kzg_commitments)
<= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
)
# Verify that the bid is for the current slot
assert bid.slot == block.slot
# Verify that the bid is for the right parent block
assert bid.parent_block_hash == state.latest_block_hash
assert bid.parent_block_root == block.parent_root
assert bid.prev_randao == get_randao_mix(state, get_current_epoch(state))
# Record the pending payment if there is some payment
if amount > 0:
pending_payment = BuilderPendingPayment(
weight=0,
withdrawal=BuilderPendingWithdrawal(
fee_recipient=bid.fee_recipient,
amount=amount,
builder_index=builder_index,
),
)
state.builder_pending_payments[SLOTS_PER_EPOCH + bid.slot % SLOTS_PER_EPOCH] = (
pending_payment
)
# Cache the signed execution payload bid
state.latest_execution_payload_bid = bid
def can_builder_cover_bid(
state: BeaconState, builder_index: BuilderIndex, bid_amount: Gwei
) -> bool:
builder_balance = state.builders[builder_index].balance
pending_withdrawals_amount = get_pending_balance_to_withdraw_for_builder(state, builder_index)
min_balance = MIN_DEPOSIT_AMOUNT + pending_withdrawals_amount
if builder_balance < min_balance:
return False
return builder_balance - min_balance >= bid_amount
def get_pending_balance_to_withdraw_for_builder(
state: BeaconState, builder_index: BuilderIndex
) -> Gwei:
return sum(
withdrawal.amount
for withdrawal in state.builder_pending_withdrawals
if withdrawal.builder_index == builder_index
) + sum(
payment.withdrawal.amount
for payment in state.builder_pending_payments
if payment.withdrawal.builder_index == builder_index
)Process Operations
def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
assert len(body.deposits) == 0
def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
for operation in operations:
fn(state, operation)
# [Modified in Gloas:EIP7732]
for_ops(body.proposer_slashings, process_proposer_slashing)
for_ops(body.attester_slashings, process_attester_slashing)
# [Modified in Gloas:EIP7732]
for_ops(body.attestations, process_attestation)
# [Modified in Gloas:EIP7732]
for_ops(body.voluntary_exits, process_voluntary_exit)
for_ops(body.bls_to_execution_changes, process_bls_to_execution_change)
# [Modified in Gloas:EIP7732]
# Removed `process_deposit_request`
# [Modified in Gloas:EIP7732]
# Removed `process_withdrawal_request`
# [Modified in Gloas:EIP7732]
# Removed `process_consolidation_request`
# [New in Gloas:EIP7732]
for_ops(body.payload_attestations, process_payload_attestation)We handle Beacon Chain operations of the block in the current slot, and execution layer requests in the next one, because of ePBS (i.e. because we first propagate Beacon block with Execution Payload Bid and validate it, and only then vote on it + builder propagates the Execution Payload, so that handling the next Beacon block we would execute this payload’s requests).
def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None:
header_1 = proposer_slashing.signed_header_1.message
header_2 = proposer_slashing.signed_header_2.message
# Verify header slots match
assert header_1.slot == header_2.slot
# Verify header proposer indices match
assert header_1.proposer_index == header_2.proposer_index
# Verify the headers are different
assert header_1 != header_2
# Verify the proposer is slashable
proposer = state.validators[header_1.proposer_index]
assert is_slashable_validator(proposer, get_current_epoch(state))
# Verify signatures
for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2):
domain = get_domain(
state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot)
)
signing_root = compute_signing_root(signed_header.message, domain)
assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature)
# [New in Gloas:EIP7732]
# Remove the BuilderPendingPayment corresponding to
# this proposal if it is still in the 2-epoch window.
slot = header_1.slot
proposal_epoch = compute_epoch_at_slot(slot)
if proposal_epoch == get_current_epoch(state):
payment_index = SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
elif proposal_epoch == get_previous_epoch(state):
payment_index = slot % SLOTS_PER_EPOCH
state.builder_pending_payments[payment_index] = BuilderPendingPayment()
slash_validator(state, header_1.proposer_index)
def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None:
attestation_1 = attester_slashing.attestation_1
attestation_2 = attester_slashing.attestation_2
assert is_slashable_attestation_data(attestation_1.data, attestation_2.data)
assert is_valid_indexed_attestation(state, attestation_1)
assert is_valid_indexed_attestation(state, attestation_2)
slashed_any = False
indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices)
for index in sorted(indices):
if is_slashable_validator(state.validators[index], get_current_epoch(state)):
slash_validator(state, index)
slashed_any = True
assert slashed_any
def slash_validator(
state: BeaconState,
slashed_index: ValidatorIndex,
whistleblower_index: Optional[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
# [Modified in Electra:EIP7251]
slashing_penalty = validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA
decrease_balance(state, slashed_index, slashing_penalty)
# Apply proposer and whistleblower rewards
proposer_index = get_beacon_proposer_index(state)
if whistleblower_index is None:
whistleblower_index = proposer_index
# [Modified in Electra:EIP7251]
whistleblower_reward = Gwei(
validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA
)
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))def process_attestation(state: BeaconState, attestation: Attestation) -> None:
data = attestation.data
assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state))
assert data.target.epoch == compute_epoch_at_slot(data.slot)
assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot
# [Modified in Gloas:EIP7732]
assert data.index < 2
committee_indices = get_committee_indices(attestation.committee_bits)
committee_offset = 0
for committee_index in committee_indices:
assert committee_index < get_committee_count_per_slot(state, data.target.epoch)
committee = get_beacon_committee(state, data.slot, committee_index)
committee_attesters = {
attester_index
for i, attester_index in enumerate(committee)
if attestation.aggregation_bits[committee_offset + i]
}
assert len(committee_attesters) > 0
committee_offset += len(committee)
# Bitfield length matches total number of participants
assert len(attestation.aggregation_bits) == committee_offset
# Participation flag indices
participation_flag_indices = get_attestation_participation_flag_indices(
state, data, state.slot - data.slot
)
# Verify signature
assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))
# [Modified in Gloas:EIP7732]
if data.target.epoch == get_current_epoch(state):
current_epoch_target = True
epoch_participation = state.current_epoch_participation
payment = state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH]
else:
current_epoch_target = False
epoch_participation = state.previous_epoch_participation
payment = state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH]
proposer_reward_numerator = 0
for index in get_attesting_indices(state, attestation):
# [New in Gloas:EIP7732]
# For same-slot attestations, check if we are setting any new flags.
# If we are, this validator has not contributed to this slot's quorum yet.
will_set_new_flag = False
for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS):
if flag_index in participation_flag_indices and not has_flag(
epoch_participation[index], flag_index
):
epoch_participation[index] = add_flag(epoch_participation[index], flag_index)
proposer_reward_numerator += get_base_reward(state, index) * weight
# [New in Gloas:EIP7732]
will_set_new_flag = True
# [New in Gloas:EIP7732]
# Add weight for same-slot attestations when any new flag is set.
# This ensures each validator contributes exactly once per slot.
if (
will_set_new_flag
and is_attestation_same_slot(state, data)
and payment.withdrawal.amount > 0
):
payment.weight += state.validators[index].effective_balance
# Reward proposer
proposer_reward_denominator = (
(WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT
)
proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator)
increase_balance(state, get_beacon_proposer_index(state), proposer_reward)
# [New in Gloas:EIP7732]
# Update builder payment weight
if current_epoch_target:
state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] = payment
else:
state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] = payment
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.
"""
# Matching source
if data.target.epoch == get_current_epoch(state):
justified_checkpoint = state.current_justified_checkpoint
else:
justified_checkpoint = state.previous_justified_checkpoint
is_matching_source = data.source == justified_checkpoint
# Matching target
target_root = get_block_root(state, data.target.epoch)
target_root_matches = data.target.root == target_root
is_matching_target = is_matching_source and target_root_matches
# [New in Gloas:EIP7732]
if is_attestation_same_slot(state, data):
assert data.index == 0
payload_matches = True
else:
slot_index = data.slot % SLOTS_PER_HISTORICAL_ROOT
payload_index = state.execution_payload_availability[slot_index]
payload_matches = data.index == payload_index
# Matching head
head_root = get_block_root_at_slot(state, data.slot)
head_root_matches = data.beacon_block_root == head_root
# [Modified in Gloas:EIP7732]
is_matching_head = is_matching_target and head_root_matches and payload_matches
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:
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_indicesdef process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None:
voluntary_exit = signed_voluntary_exit.message
domain = compute_domain(
DOMAIN_VOLUNTARY_EXIT, config.CAPELLA_FORK_VERSION, state.genesis_validators_root
)
signing_root = compute_signing_root(voluntary_exit, domain)
# Exits must specify an epoch when they become valid; they are not valid before then
assert get_current_epoch(state) >= voluntary_exit.epoch
# [New in Gloas:EIP7732]
if is_builder_index(voluntary_exit.validator_index):
builder_index = convert_validator_index_to_builder_index(voluntary_exit.validator_index)
# Verify the builder is active
assert is_active_builder(state, builder_index)
# Only exit builder if it has no pending withdrawals in the queue
assert get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0
# Verify signature
pubkey = state.builders[builder_index].pubkey
assert bls.Verify(pubkey, signing_root, signed_voluntary_exit.signature)
# Initiate exit
initiate_builder_exit(state, builder_index)
return
validator = state.validators[voluntary_exit.validator_index]
# Verify the validator is active
assert is_active_validator(validator, get_current_epoch(state))
# Verify exit has not been initiated
assert validator.exit_epoch == FAR_FUTURE_EPOCH
# Verify the validator has been active long enough
assert get_current_epoch(state) >= validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD
# Only exit validator if it has no pending withdrawals in the queue
assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0
# Verify signature
assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature)
# Initiate exit
initiate_validator_exit(state, voluntary_exit.validator_index)
def process_bls_to_execution_change(
state: BeaconState, signed_address_change: SignedBLSToExecutionChange
) -> None:
address_change = signed_address_change.message
assert address_change.validator_index < len(state.validators)
validator = state.validators[address_change.validator_index]
assert validator.withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX
assert validator.withdrawal_credentials[1:] == hash(address_change.from_bls_pubkey)[1:]
# Fork-agnostic domain since address changes are valid across forks
domain = compute_domain(
DOMAIN_BLS_TO_EXECUTION_CHANGE, genesis_validators_root=state.genesis_validators_root
)
signing_root = compute_signing_root(address_change, domain)
assert bls.Verify(address_change.from_bls_pubkey, signing_root, signed_address_change.signature)
validator.withdrawal_credentials = (
ETH1_ADDRESS_WITHDRAWAL_PREFIX + b"\x00" * 11 + address_change.to_execution_address
)
def process_payload_attestation(
state: BeaconState, payload_attestation: PayloadAttestation
) -> None:
data = payload_attestation.data
# Check that the attestation is for the parent beacon block
assert data.beacon_block_root == state.latest_block_header.parent_root
# Check that the attestation is for the previous slot
assert data.slot + 1 == state.slot
# Verify signature
indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation)
assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation)Sync Committee and Bookkeeping
def process_sync_aggregate(state: BeaconState, sync_aggregate: SyncAggregate) -> None:
# Verify sync committee aggregate signature signing over the previous slot block root
committee_pubkeys = state.current_sync_committee.pubkeys
committee_bits = sync_aggregate.sync_committee_bits
if sum(committee_bits) == SYNC_COMMITTEE_SIZE:
# All members participated - use precomputed aggregate key
participant_pubkeys = [state.current_sync_committee.aggregate_pubkey]
elif sum(committee_bits) > SYNC_COMMITTEE_SIZE // 2:
# More than half participated - subtract non-participant keys.
# First determine nonparticipating members
non_participant_pubkeys = [
pubkey for pubkey, bit in zip(committee_pubkeys, committee_bits, strict=True) if not bit
]
# Compute aggregate of non-participants
non_participant_aggregate = eth_aggregate_pubkeys(non_participant_pubkeys)
# Subtract non-participants from the full aggregate
# This is equivalent to: aggregate_pubkey + (-non_participant_aggregate)
participant_pubkey = bls.add(
bls.bytes48_to_G1(state.current_sync_committee.aggregate_pubkey),
bls.neg(bls.bytes48_to_G1(non_participant_aggregate)),
)
participant_pubkeys = [BLSPubkey(bls.G1_to_bytes48(participant_pubkey))]
else:
# Less than half participated - aggregate participant keys
participant_pubkeys = [
pubkey
for pubkey, bit in zip(
committee_pubkeys, sync_aggregate.sync_committee_bits, strict=True
)
if bit
]
previous_slot = max(state.slot, Slot(1)) - Slot(1)
domain = get_domain(state, DOMAIN_SYNC_COMMITTEE, compute_epoch_at_slot(previous_slot))
signing_root = compute_signing_root(get_block_root_at_slot(state, previous_slot), domain)
# Note: eth_fast_aggregate_verify works with a singleton list containing an aggregated key
assert eth_fast_aggregate_verify(
participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature
)
# Compute participant and proposer rewards
total_active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT
total_base_rewards = Gwei(get_base_reward_per_increment(state) * total_active_increments)
max_participant_rewards = Gwei(
total_base_rewards * SYNC_REWARD_WEIGHT // WEIGHT_DENOMINATOR // SLOTS_PER_EPOCH
)
participant_reward = Gwei(max_participant_rewards // SYNC_COMMITTEE_SIZE)
proposer_reward = Gwei(
participant_reward * PROPOSER_WEIGHT // (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)
)
# Apply participant and proposer rewards
all_pubkeys = [v.pubkey for v in state.validators]
committee_indices = [
ValidatorIndex(all_pubkeys.index(pubkey)) for pubkey in state.current_sync_committee.pubkeys
]
for participant_index, participation_bit in zip(
committee_indices, sync_aggregate.sync_committee_bits, strict=True
):
if participation_bit:
increase_balance(state, participant_index, participant_reward)
increase_balance(state, get_beacon_proposer_index(state), proposer_reward)
else:
decrease_balance(state, participant_index, participant_reward)
def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
epoch = get_current_epoch(state)
# Verify RANDAO reveal
proposer = state.validators[get_beacon_proposer_index(state)]
signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal)
# Mix in RANDAO reveal
mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix
def process_eth1_data(state: BeaconState, body: BeaconBlockBody) -> None:
state.eth1_data_votes.append(body.eth1_data)
if (
state.eth1_data_votes.count(body.eth1_data) * 2
> EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH
):
state.eth1_data = body.eth1_dataeth1_data: Is not used; technical debt.
Fork-Choice: get_head(store: Store)
def get_head(store: Store) -> ForkChoiceNode:
# Get filtered block tree that only includes viable branches
blocks = get_filtered_block_tree(store)
# Execute the LMD-GHOST fork-choice
head = ForkChoiceNode(
root=store.justified_checkpoint.root,
# [New in Gloas:EIP7732]
payload_status=PAYLOAD_STATUS_PENDING,
)
while True:
children = get_node_children(store, blocks, head)
if len(children) == 0:
return head
# Sort by latest attesting balance with ties broken lexicographically
head = max(
children,
key=lambda child: (
get_weight(store, child),
child.root,
# [New in Gloas:EIP7732]
get_payload_status_tiebreaker(store, child),
),
)def filter_block_tree(store: Store, block_root: Root, blocks: Dict[Root, BeaconBlock]) -> bool:
block = store.blocks[block_root]
children = [root for root in store.blocks if store.blocks[root].parent_root == block_root]
# If any children branches contain expected finalized/justified checkpoints,
# add to filtered block-tree and signal viability to parent.
if any(children):
filter_block_tree_result = [filter_block_tree(store, child, blocks) for child in children]
if any(filter_block_tree_result):
blocks[block_root] = block
return True
return False
current_epoch = get_current_store_epoch(store)
voting_source = get_voting_source(store, block_root)
# The voting source should be either at the same height as the store's justified checkpoint or
# not more than two epochs ago
correct_justified = (
store.justified_checkpoint.epoch == GENESIS_EPOCH
or voting_source.epoch == store.justified_checkpoint.epoch
or voting_source.epoch + 2 >= current_epoch
)
finalized_checkpoint_block = get_checkpoint_block(
store,
block_root,
store.finalized_checkpoint.epoch,
)
correct_finalized = (
store.finalized_checkpoint.epoch == GENESIS_EPOCH
or store.finalized_checkpoint.root == finalized_checkpoint_block
)
# If expected finalized/justified, add to viable block-tree and signal viability to parent.
if correct_justified and correct_finalized:
blocks[block_root] = block
return True
# Otherwise, branch not viable
return False
def get_voting_source(store: Store, block_root: Root) -> Checkpoint:
"""
Compute the voting source checkpoint in event that block with root ``block_root`` is the head block
"""
block = store.blocks[block_root]
current_epoch = get_current_store_epoch(store)
block_epoch = compute_epoch_at_slot(block.slot)
if current_epoch > block_epoch:
# The block is from a prior epoch, the voting source will be pulled-up
return store.unrealized_justifications[block_root]
else:
# The block is not from a prior epoch, therefore the voting source is not pulled up
head_state = store.block_states[block_root]
return head_state.current_justified_checkpointWe go bottom-up and check that the block build on the justified blocks at that time, and current finilized blocks.
voting_source.epoch + 2 >= current_epoch: you have to declare a branch dead at some point to remove it from your memory and the algorithm. If a branch couldn’t justify a block for 2 epochs – dead.
store.unrealized_justifications[block_root]: if the epoch is 102, then to consider all the branches from the epoch 100 and pass voting_source.epoch + 2 >= current_epoch we use unrealized checkpoint.
What happens WITHOUT unrealized justifications?
When the last block was minted in Epoch 100, the Epoch 100 boundary hadn’t been crossed yet. Therefore,
- its official state.current_justified_checkpoint is still stuck at Epoch 99.
- If the consensus client relies on this official state, the branch scores a 99.
- 99 + 2 is 101, which is NOT greater than or equal to 102. Result: The client unfairly deletes a perfectly viable branch.
What happens WITH unrealized justifications?
When that Epoch 100 block was processed, the client noticed: “Hey, this block contains enough attestations to justify Epoch 100, even though the state hasn’t updated yet.”
- It saved this as the
unrealized_justification. - Now, in Epoch 102, the client “pulls up” the voting source from the stale state (99) to the unrealized justification (100).
- 100 + 2 is 102, which IS greater than or equal to 102. Result: The branch passes the viability check and survives in the fork choice algorithm and a new block can finally be proposed on it. Or it will be removed from the algorithm and memory in the next epoch.
def get_node_children(
store: Store, blocks: Dict[Root, BeaconBlock], node: ForkChoiceNode
) -> Sequence[ForkChoiceNode]:
if node.payload_status == PAYLOAD_STATUS_PENDING:
children = [ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_EMPTY)]
if is_payload_verified(store, node.root):
children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL))
return children
else:
return [
ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING)
for root in blocks
if (
blocks[root].parent_root == node.root
and node.payload_status == get_parent_payload_status(store, blocks[root])
)
]
def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus:
parent = store.blocks[block.parent_root]
parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash
message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash
return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTYThe logic is:
- Start from the justified block
- Get all the children, and push a node with status “PENDING” for each of them
- Go through the LMD GHOST and pick the heaviest (most voted on) block
- Result of that
maxfunction is the newhead; new loop interation - In
get_node_childrennow we fall intonode.payload_status == PAYLOAD_STATUS_PENDING - Push an EMPTY node and if the payload is there and verified, push a FULL node
- We put older (checkpoint to N-2) blocks and the newest one through
get_weightand see if they are EMPTY or FULL in the chain. And the new block (N-1) we put directly throughget_payload_status_tiebreakerto look at the local state and decide on the payload status - After the block is added, when attesting we run
get_headagain and vote for the returned block
def get_weight(store: Store, node: ForkChoiceNode) -> Gwei:
# [New in Gloas:EIP7732]
if is_previous_slot_payload_decision(store, node):
return Gwei(0)
state = store.checkpoint_states[store.justified_checkpoint]
attestation_score = get_attestation_score(store, node, state)
# [Modified in Gloas:EIP7732]
if not should_apply_proposer_boost(store):
# Return only attestation score if proposer boost should not apply
return attestation_score
# Calculate proposer score if proposer boost should apply
proposer_score = Gwei(0)
# [Modified in Gloas:EIP7732]
proposer_boost_node = ForkChoiceNode(
root=store.proposer_boost_root, payload_status=PAYLOAD_STATUS_PENDING
)
# Boost is applied if ``node`` is an ancestor of ``proposer_boost_node``
if is_ancestor(store, proposer_boost_node, node):
proposer_score = get_proposer_score(store)
return attestation_score + proposer_score
def get_attestation_score(store: Store, node: ForkChoiceNode, state: BeaconState) -> Gwei:
unslashed_and_active_indices = [
i
for i in get_active_validator_indices(state, get_current_epoch(state))
if not state.validators[i].slashed
]
return Gwei(
sum(
state.validators[i].effective_balance
for i in unslashed_and_active_indices
if (
i in store.latest_messages
and i not in store.equivocating_indices
and is_ancestor(store, get_supported_node(store, store.latest_messages[i]), node)
)
)
)
def get_supported_node(store: Store, message: LatestMessage) -> ForkChoiceNode:
"""
Return a node supported by the ``message``.
"""
# [New in Gloas:EIP7732]
block = store.blocks[message.root]
if block.slot < message.slot:
if message.payload_present:
payload_status = PAYLOAD_STATUS_FULL
else:
payload_status = PAYLOAD_STATUS_EMPTY
else:
payload_status = PAYLOAD_STATUS_PENDING
# [Modified in Gloas:EIP7732]
return ForkChoiceNode(root=message.root, payload_status=payload_status)
def get_ancestor(store: Store, node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode:
block = store.blocks[node.root]
if block.slot > slot:
# [Modified in Gloas:EIP7732]
parent = ForkChoiceNode(
root=block.parent_root,
payload_status=get_parent_payload_status(store, block),
)
return get_ancestor(store, parent, slot)
return node
def is_ancestor(store: Store, node: ForkChoiceNode, ancestor: ForkChoiceNode) -> bool:
node_ancestor = get_ancestor(store, node, store.blocks[ancestor.root].slot)
if node_ancestor.root != ancestor.root:
return False
# [New in Gloas:EIP7732]
return (
node_ancestor.payload_status == ancestor.payload_status
or ancestor.payload_status == PAYLOAD_STATUS_PENDING
)
def is_previous_slot_payload_decision(store: Store, node: ForkChoiceNode) -> bool:
is_previous_slot = store.blocks[node.root].slot + 1 == get_current_slot(store)
is_payload_decision = node.payload_status in [PAYLOAD_STATUS_EMPTY, PAYLOAD_STATUS_FULL]
return is_previous_slot and is_payload_decisionWhy do we need this logic?
In ePBS Beacon block and execution payload are sent separately. If the builder did not perform his job correctly, we build on EMPTY, otherwise on FULL. When building a block, we use this to determine parent_block_hash, that is, last FULL block’s block_hash. When voting starts, we attest to the current head from get_head, so we localy look at the last slot’s block payload status and if the new block from the proposer is built on the same branch, we will pass node.payload_status == get_parent_payload_status(store, blocks[root]) check in get_node_children and vote for the new block.
if is_previous_slot_payload_decision(store, node): if it’s the previous slot, we go and check our local state to choose the branch.get_attestation_score: we aggregate all the EB of the validators whose latest message is for or after the current loop iteration’s node and is decendent of it.if block.slot < message.slot: if a slot (N) was skipped, we still need to cement N-1 block’s payload status and we do that by looking atmessage.payload_presentwhich in normal conditions is false (attestation to a block happens before the payload is (fully) received), but when a block is skipped, we already know the status by collecting PTC votes over network. So, in N+1 we correctly choose the branch and build on top of it.
def should_extend_payload(store: Store, root: Root) -> bool:
assert store.blocks[root].slot + 1 == get_current_slot(store)
if not is_payload_verified(store, root):
return False
proposer_root = store.proposer_boost_root
payload_is_timely = payload_timeliness(store, root, timely=True)
payload_data_is_available = payload_data_availability(store, root, available=True)
return (
(payload_is_timely and payload_data_is_available)
or proposer_root == Root()
or store.blocks[proposer_root].parent_root != root
or is_parent_node_full(store, store.blocks[proposer_root])
)
def get_payload_status_tiebreaker(store: Store, node: ForkChoiceNode) -> uint8:
if is_previous_slot_payload_decision(store, node):
# To decide on a payload from the previous slot, choose
# between FULL and EMPTY based on `should_extend_payload`
if node.payload_status == PAYLOAD_STATUS_EMPTY:
return 1
if should_extend_payload(store, node.root):
return 2
return 0
else:
return node.payload_statusshould_extend_payload: we might have the payload, but was it built on? And if so, was that decision correct?
Assuming I have already downloaded and verified the payload locally, I will trace down the FULL branch, if any of these are true:
- The Proposer showed up, built on a FULL parent
- The Proposer showed up, but built on a completely different branch, so I’ll ignore them
- The Proposer hasn’t shown up, so I’ll default to my local verification
- The Proposer lied and chose EMPTY, but the PTC caught them: the parent block is FULL
def should_apply_proposer_boost(store: Store) -> bool:
if store.proposer_boost_root == Root():
return False
block = store.blocks[store.proposer_boost_root]
parent_root = block.parent_root
parent = store.blocks[parent_root]
slot = block.slot
# Apply proposer boost if `parent` is not from the previous slot
if parent.slot + 1 < slot:
return True
# Apply proposer boost if `parent` is not weak
if not is_head_weak(store, parent_root):
return True
# If `parent` is weak and from the previous slot, apply
# proposer boost if there are no early equivocations
equivocations = [
root
for root, block in store.blocks.items()
if (
store.block_timeliness[root][PTC_TIMELINESS_INDEX]
and block.proposer_index == parent.proposer_index
and block.slot + 1 == slot
and root != parent_root
)
]
return len(equivocations) == 0
def is_head_weak(store: Store, head_root: Root) -> bool:
# Calculate weight threshold for weak head
justified_state = store.checkpoint_states[store.justified_checkpoint]
reorg_threshold = calculate_committee_fraction(justified_state, config.REORG_HEAD_WEIGHT_THRESHOLD)
# Compute head weight including equivocations
head_state = store.block_states[head_root]
head_block = store.blocks[head_root]
epoch = compute_epoch_at_slot(head_block.slot)
head_node = ForkChoiceNode(root=head_root, payload_status=PAYLOAD_STATUS_PENDING)
head_weight = get_attestation_score(store, head_node, justified_state)
for index in range(get_committee_count_per_slot(head_state, epoch)):
committee = get_beacon_committee(head_state, head_block.slot, CommitteeIndex(index))
head_weight += Gwei(
sum(
justified_state.validators[i].effective_balance
for i in committee
if i in store.equivocating_indices
)
)
return head_weight < reorg_threshold
def calculate_committee_fraction(state: BeaconState, committee_percent: uint64) -> Gwei:
committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH
return Gwei((committee_weight * committee_percent) // 100)
def compute_proposer_score(state: BeaconState) -> Gwei:
committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH
return (committee_weight * config.PROPOSER_SCORE_BOOST) // 100
def get_proposer_score(store: Store) -> Gwei:
justified_checkpoint_state = store.checkpoint_states[store.justified_checkpoint]
return compute_proposer_score(justified_checkpoint_state)Why? Imagine a malicious Proposer for Slot 10. They create a block, but they intentionally withhold it until the very last millisecond. Because it arrives so late, only 15% of the network votes for it. The other 85% of the network votes for Slot 9 (effectively declaring Slot 10 empty). If the Slot 11 Proposer is also malicious, they could build on that terrible Slot 10 block, claim the 40% Proposer Boost, and violently force the network to accept Slot 10, essentially overriding the 85% of honest validators.
REORG_HEAD_WEIGHT_THRESHOLD == 20%
head_weight += Gwei( in is_head_weak: an attacker might have equivocated to intentionaly make a block weak (assuming a small amount of votes or a huge voting power of the attacker).
def notify_ptc_messages(
store: Store, state: BeaconState, payload_attestations: Sequence[PayloadAttestation]
) -> None:
"""
Extracts a list of ``PayloadAttestationMessage`` from ``payload_attestations`` and updates the store with them
These Payload attestations are assumed to be in the beacon block hence signature verification is not needed
"""
if state.slot == 0:
return
for payload_attestation in payload_attestations:
indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation)
for idx in indexed_payload_attestation.attesting_indices:
on_payload_attestation_message(
store,
PayloadAttestationMessage(
validator_index=idx,
data=payload_attestation.data,
signature=BLSSignature(),
),
is_from_block=True,
)
def on_payload_attestation_message(
store: Store, ptc_message: PayloadAttestationMessage, is_from_block: bool = False
) -> None:
"""
Run ``on_payload_attestation_message`` upon receiving a new ``ptc_message`` from
either within a block or directly on the wire.
"""
data = ptc_message.data
# PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found
assert data.beacon_block_root in store.block_states
state = store.block_states[data.beacon_block_root]
# PTC votes can only change the vote for their assigned beacon block, return early otherwise
if data.slot != state.slot:
return
# Get all positions of the attester in the PTC
ptc_indices = []
ptc = get_ptc(state, data.slot)
for ptc_index, validator_index in enumerate(ptc):
if validator_index == ptc_message.validator_index:
ptc_indices.append(ptc_index)
# Check that the attester is from the PTC
assert len(ptc_indices) > 0
# Verify the signature and check that its for the current slot if it is coming from the wire
if not is_from_block:
# Check that the attestation is for the current slot
assert data.slot == get_current_slot(store)
# Verify the signature
assert is_valid_indexed_payload_attestation(
state,
IndexedPayloadAttestation(
attesting_indices=[ptc_message.validator_index],
data=data,
signature=ptc_message.signature,
),
)
# Update the votes for the block
payload_timeliness_vote = store.payload_timeliness_vote[data.beacon_block_root]
payload_data_availability_vote = store.payload_data_availability_vote[data.beacon_block_root]
for ptc_index in ptc_indices:
payload_timeliness_vote[ptc_index] = data.payload_present
payload_data_availability_vote[ptc_index] = data.blob_data_available
def record_block_timeliness(store: Store, root: Root) -> None:
block = store.blocks[root]
seconds_since_genesis = store.time - store.genesis_time
time_into_slot_ms = seconds_to_milliseconds(seconds_since_genesis) % config.SLOT_DURATION_MS
attestation_threshold_ms = get_attestation_due_ms()
# [New in Gloas:EIP7732]
is_current_slot = get_current_slot(store) == block.slot
ptc_threshold_ms = get_payload_attestation_due_ms()
# [Modified in Gloas:EIP7732]
store.block_timeliness[root] = [
is_current_slot and time_into_slot_ms < threshold
for threshold in [attestation_threshold_ms, ptc_threshold_ms]
]
def update_proposer_boost_root(store: Store, head: Root, root: Root) -> None:
is_first_block = store.proposer_boost_root == Root()
# [Modified in Gloas:EIP7732]
is_timely = store.block_timeliness[root][ATTESTATION_TIMELINESS_INDEX]
is_same_dependent_root = get_dependent_root(store, root) == get_dependent_root(store, head)
# Add proposer score boost if the block is timely, not conflicting with an
# existing block, with the same dependent root as the canonical chain head.
if is_timely and is_first_block and is_same_dependent_root:
store.proposer_boost_root = root
def update_checkpoints(
store: Store, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint
) -> None:
"""
Update checkpoints in store if necessary
"""
# Update justified checkpoint
if justified_checkpoint.epoch > store.justified_checkpoint.epoch:
store.justified_checkpoint = justified_checkpoint
# Update finalized checkpoint
if finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
store.finalized_checkpoint = finalized_checkpoint
def compute_pulled_up_tip(store: Store, block_root: Root) -> None:
state = store.block_states[block_root].copy()
# Pull up the post-state of the block to the next epoch boundary
process_justification_and_finalization(state)
store.unrealized_justifications[block_root] = state.current_justified_checkpoint
update_unrealized_checkpoints(
store, state.current_justified_checkpoint, state.finalized_checkpoint
)
# If the block is from a prior epoch, apply the realized values
block_epoch = compute_epoch_at_slot(store.blocks[block_root].slot)
current_epoch = get_current_store_epoch(store)
if block_epoch < current_epoch:
update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint)
def on_tick(store: Store, time: uint64) -> None:
# If the ``store.time`` falls behind, while loop catches up slot by slot
# to ensure that every previous slot is processed with ``on_tick_per_slot``
tick_slot = (time - store.genesis_time) * 1000 // config.SLOT_DURATION_MS
while get_current_slot(store) < tick_slot:
previous_time = (
store.genesis_time + (get_current_slot(store) + 1) * config.SLOT_DURATION_MS // 1000
)
on_tick_per_slot(store, previous_time)
on_tick_per_slot(store, time)
def on_tick_per_slot(store: Store, time: uint64) -> None:
previous_slot = get_current_slot(store)
# Update store time
store.time = time
current_slot = get_current_slot(store)
# If this is a new slot, reset store.proposer_boost_root
if current_slot > previous_slot:
store.proposer_boost_root = Root()
# If a new epoch, pull-up justification and finalization from previous epoch
if current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0:
update_checkpoints(
store, store.unrealized_justified_checkpoint, store.unrealized_finalized_checkpoint
)Always remember that we handle the payload of the previous block in the current one. We run get_head (LMD GHOST) for the previous block first on recieving the block. And then, after on_block, we run get_head to attest for the new block.