Ethereum gives a strong guarantee that no honest node will ever reorg a finalized checkpoint. Moreover, if two conflicting checkpoints are finalized, FFG (Ethereum’s finality gadget) implies that at least 1/3 of the total stake is slashable. That is the usual crypto-economic safety statement.

This post is about a narrower question: when Ethereum says that a checkpoint is finalized, what state is actually protected by that guarantee?

That question already has some subtlety today when the epoch-boundary block is missing. Gloas makes it more delicate, because beacon-block availability and payload availability can now diverge. In particular, EIP-7732 forces us to be precise about what a finalized checkpoint does and does not commit to.

Terminology

The discussion below uses the following terms:

  • A target checkpoint is the checkpoint an attestation votes for in FFG.
  • A checkpoint is justified once enough attestations support it.
  • A checkpoint is finalized once a later justified checkpoint finalizes it through the FFG rules.
  • The post-state of a block is the beacon state after applying that block’s beacon-chain transition.
  • In Gloas, a slot may be full, empty, or missed: full means beacon block plus payload are available, empty means the beacon block is available but no payload was included, and missed means no beacon block was available.

Attestation targets before Gloas

In a well-behaved chain, validators in slot 92 attest after the block for that slot has arrived:

graph RL F["92"]:::green E["..."]:::lightblue C["65"]:::lightblue B["64"]:::orange D["..."]:::lightblue G["32"]:::red H["..."]:::lightblue F --> E E --> C C --> B B --> D D --> G G --> H classDef lightblue fill:#ADD8E6 classDef green fill:#90EE90 classDef orange fill:#FFA500 classDef red fill:#FF0000

An attestation for the green block at slot 92 uniquely determines the target block at slot 64. If enough such votes are collected during epoch 2, then when the chain crosses into epoch 3 at slot 96, block 64 becomes justified. At that point the previously justified checkpoint, block 32, becomes finalized.

After slot 96 and before slot 128, an honest node asked for its finalized checkpoint through the standard Beacon API endpoint will return the post-state of block 32. The guarantee applies to the contents of that finalized checkpoint:

Honest nodes will never consider a chain that does not contain block 32. Even if another chain is later finalized, or if 100% of the validator set is malicious, an honest node will never accept a chain that excludes block 32.

If another finalized chain does exclude block 32, then at least 1/3 of the total stake is slashable.

Missing epoch-boundary blocks

Now consider the case where the epoch-boundary block at slot 64 is missing or was reorged out:

graph RL F["92"]:::green E["..."]:::lightblue C["65"]:::lightblue B["63"]:::orange I["64"]:::white_dashed D["..."]:::lightblue G["32"]:::red H["..."]:::lightblue F --> E E --> C C --> B I --> B C ~~~ I B --> D D --> G G --> H classDef white_dashed fill:#FFFFFF,stroke:#5F9EA0,stroke-width:2px,stroke-dasharray: 5 5 classDef lightblue fill:#ADD8E6 classDef green fill:#90EE90 classDef orange fill:#FFA500 classDef red fill:#FF0000

Here, a vote for block 92 implicitly sets block 63 as the target, because 63 is the last block in the previous epoch. If enough such votes are collected, block 63 becomes justified when the chain crosses slot 96, and it becomes finalized when the chain later crosses into epoch 4 at slot 128.

But this case contains more information than the happy path. Validators are not only voting for block 63. They are also signaling that slot 64 was empty. So the effective guarantee is stronger:

Honest nodes will never consider a chain that does not contain block 63 with an empty slot 64. They will reject both:

  • chains that do not contain block 63, and
  • chains that contain a block at slot 64.

If another finalized chain either excludes block 63 or includes a block at slot 64, then at least 1/3 of the total stake is slashable.

The same logic extends to multiple missing slots. If the last available block were 62 instead, then validators would be finalizing both block 62 and the absence of blocks at slots 63 and 64.

The finalized endpoint before Gloas

What should a node return as the finalized state in the missing-boundary case above?

Returning only the post-state of the last available block, block 63, loses information: it does not reflect that slot 64 was known to be empty. The historical solution is to return the post-state of block 63, but advanced to slot 64. That state captures both facts:

  • the last block in the epoch was 63, and
  • slot 64 was missed or reorged away.

This is why checkpoint sync standardized on the epoch-boundary state rather than the raw post-state of the last block. See this Beacon API discussion.

Gloas changes the question

So far the subtlety was about whether a block existed at the epoch boundary. Gloas introduces a different ambiguity: the beacon block and the execution payload can now diverge.

In Gloas, a forkchoice node may see a slot as:

  • missed, as today, when no beacon block is available;
  • full, when both the beacon block and its payload are available; or
  • empty, when the beacon block is available but no payload was included.

This matters because attesters do not attest to the payload of the current slot, but they do indirectly attest to payload availability for earlier slots. For the full rationale, see the annotated forkchoice spec. Here we only need the minimum needed to understand finalization.

Happy path under Gloas

Consider again the same happy-path picture:

graph RL F["92"]:::green E["..."]:::lightblue C["65"]:::lightblue B["64"]:::orange D["..."]:::lightblue G["32"]:::red H["..."]:::lightblue F --> E E --> C C --> B B --> D D --> G G --> H classDef lightblue fill:#ADD8E6 classDef green fill:#90EE90 classDef orange fill:#FFA500 classDef red fill:#FF0000

An attestation for block 92 still uniquely identifies the target block at slot 64. Under Gloas, it also effectively identifies the payload status of slot 64, except for the committee attesting during slot 64 itself. Those validators vote for 64 as both head and target, but they do not yet make a claim about the payload of 64. Every later committee in the epoch does make such a claim.

So by the time the chain reaches slot 96, the attestations contain enough information to justify both:

  • the beacon block at slot 64, and
  • the payload status associated with slot 64.

However, the beacon state does not store that full information. To preserve it directly on-chain, the spec would need to extend checkpoints themselves, for example:

class Checkpoint(Container):
    epoch: Epoch
    root: Root
    block_hash: Hash32

That approach was considered too invasive for clients, so the checkpoint structure stayed unchanged. As a result, only the beacon-block part of the justification/finalization information is stored in the beacon state.

After moving into epoch 3 at slot 96, the justified checkpoint contains enough information to justify the beacon block at slot 64, but not enough protocol-state information to justify its payload.

This is the key distinction:

Under Gloas, attestations may contain enough information to reconstruct payload availability, but the protocol-finalized object is still the checkpoint root stored in the beacon state. In other words, the beacon block may be finalized even when the payload-derived state is not.

This leads to a couple of awkward but necessary API decisions:

  • In the Engine API, the safe block hash should refer to the payload of slot 63, not slot 64.
  • In the Beacon API state endpoint, the finalized state should be the post-state after applying the beacon block at slot 64, but before applying its payload.

That behavior correctly encodes the protocol guarantee. Finality applies to the beacon block at slot 64, not to the payload-derived state at slot 64.

After slot 64 is finalized, honest nodes will never accept a chain that excludes the beacon block at slot 64. But they may still accept chains that differ on the payload of slot 64.

Missing boundary blocks under Gloas

Now return to the case where slot 64 has no beacon block:

graph RL F["92"]:::green E["..."]:::lightblue C["65"]:::lightblue B["63"]:::orange I["64"]:::white_dashed D["..."]:::lightblue G["32"]:::red H["..."]:::lightblue F --> E E --> C C --> B I --> B C ~~~ I B --> D D --> G G --> H classDef white_dashed fill:#FFFFFF,stroke:#5F9EA0,stroke-width:2px,stroke-dasharray: 5 5 classDef lightblue fill:#ADD8E6 classDef green fill:#90EE90 classDef orange fill:#FFA500 classDef red fill:#FF0000

In this case all earlier payloads may be available, but the beacon block at slot 64, and therefore any payload for slot 64, are absent.

Here the attestations actually say quite a lot. Even the committee at slot 64 votes for block 63 together with its payload, by setting attestation.data.index = 1. Later committees support that same payload by building on top of block 65. So from the attestations alone, one can recover strong evidence about the payload state of block 63.

But once again that is not the same thing as protocol finality. The checkpoint stored in the beacon state still only commits to the checkpoint root. It does not commit to the payload status that nodes may reconstruct off-chain from the attestations.

So even in this case, where effectively 100% of attesters reveal payload information, the payload of block 63 is still not what the protocol finalizes.

The finalized endpoint under Gloas

This is where the pre-Gloas rule breaks down.

Before Gloas, advancing the post-state of block 63 to slot 64 was safe, because that advanced state captured the finalized fact that slot 64 was empty. Under Gloas, advancing the state can now implicitly choose between contending payload interpretations.

Two bad options appear:

  • Advancing the post-beacon-block state of 63 to slot 64 can produce a state that contends with the canonical chain built on the payload of 63.
  • Returning the full post-payload state of 63 would assert that the payload of 63 is finalized, which the node is not actually tracking in the checkpoint.

So the correct rule is now much stricter:

The only safe finalized state to return is the post-beacon-block state of the block whose root is the justified/finalized checkpoint root. Under Gloas, that state should not be advanced to the epoch boundary.

This is the central operational consequence of the post.

Why not use local view?

A node might still ask: if I locally track payload information well enough, why not expose the stronger state?

In principle, a node that records enough off-chain information from on-chain attestations could reconstruct a stronger payload-aware view. In the happy case it could return the full post-payload state for slot 64. In the missing-boundary case it could return the post-payload state for slot 63 advanced to slot 64.

The problem is not whether that reconstruction is often right. The problem is that it is stronger than what the beacon state itself finalizes.

If different nodes locally reconstruct different payload states for the same finalized checkpoint root, then they can serve incompatible answers while still agreeing on what the protocol finalized. That is acceptable nowhere in the Engine API, and undesirable even in checkpoint sync.

The Engine API consequence is the important one:

For Engine API calls, the safe hash should always be the parent hash of the bid included in the justified block.

For checkpoint sync or debug endpoints, the impact is smaller, but not zero. Different payload interpretations may change execution requests and therefore alter some proofs or cause a syncing node to follow the wrong branch initially.

There is also a spec consequence for on_attestation.

Today, store_target_checkpoint_state returns the post-CL state advanced to the epoch boundary, and that state is then used when validating the attestation signature. But once FFG/LMD consistency has already ensured the same beacon-block root, the remaining ambiguity is exactly the Gloas payload ambiguity discussed above.

That state is later used to compute the committee:

def get_indexed_attestation(state: BeaconState, attestation: Attestation) -> IndexedAttestation:
    """
    Return the indexed attestation corresponding to ``attestation``.
    """
    attesting_indices = get_attesting_indices(state, attestation)

    return IndexedAttestation(
        attesting_indices=sorted(attesting_indices),
        data=attestation.data,
        signature=attestation.signature,
    )

This eventually depends on get_beacon_committee, which depends on the epoch seed and the active validator indices. The seed is unaffected by whether the target payload is present, but the active validator set can change if execution requests in the payload trigger deposits, exits, withdrawals, or consolidations.

That only becomes visible when enough slots are missing between the target and the attestation slot for those changes to matter, but in that case the difference is real: the target state with payload and the target state without payload can imply different active validator indices.

So the spec should likely change in two ways:

  • The state used for full attestation verification should be the target state for the attestation’s beacon_block_root, not the checkpoint state cached in the store.
  • There should be test cases covering an attestation after at least one epoch of missing blocks, where the attested block is full in one branch and empty in another, and the target states differ accordingly.

Analysis of dependent root

Beacon nodes typically use the notion of dependent root to decide if two beacon states across two different branches share some invariants, like the proposer lookahead, or the beacon committees, etc. These dependent roots are chosen to be the last beacon block root of the epoch. Pre Gloas, sharing that last beacon block root would have guaranteed the same pre-state on the next epoch for any two branches, but this is no longer the case as the payload for the last beacon block may actually change the pre-state for the next block (in the next epoch). So we want to analyze what exactly can change from epoch e to epoch e+1.

Let B be the latest beacon block on epoch e and let P be its payload. Let E and F be the states obtained by processing slots to e+1 from the post state of B and P respectively. We want to undertand the main possible differences between E and F. We have the obvious changes like latest_block_hash and the such, but we are mostly interested in operations that can change the beacon committees, proposer lookahead, etc. These changes will come from processing the execution requests on P, as these changes will be included in F but not on E.

Deposit requests

Builder deposits are processed immediately and are added to the registry, so F may have more builders with their balances. But this does not affect committees/proposers and validator duties in general.

Validator deposits are appended to the state as PendingDeposit when processing P, their slot is that of B.

Withdrawal requests

Full exits will call initiate_validator_index which in turns calls compute_exit_epoch_and_update_churn with the exit balance. The earliest possible exit epoch is e+5 and the state’s earliest exit epoch is updated to this. An exit will set a validator exit epoch to this exit epoch, which at least will be e+5 and its withdrawable epoch to at least e+261.

Pending partial withdrawals are appended. Again these withdrawable epochs are at least e+261.

Consolidation requests

On switch to compounding requests a pending deposit with GENESIS_SLOT slot is added for all the balance above the minimum activation balance.

For succesful consolidation requests, the exit epoch of the source validator is set to at least e+5, the state’s earliest consolidation epoch is set to at least e+5 and the validator’s withdrawable epoch is set to at least e+261. A pending consolidation is added to the state.

Epoch processing

So far we have analized the main differences in the state from applying the requests from P or not on top of B. The two final states would differ by executing the epoch transition to these two different states. Epoch transition will be different from

Registry updates

If P has included exits and there were some validators that fell below the ejection balance, these validators may be set to a different exit epoch, at any rate this exit epoch will be at least e+5.

Pending deposits

None of the applied pending deposits with slot > 0 will be applied as the slot cannot be finalized.

The GENESIS_SLOT deposits coming from switching a validator to a compounding validator in P will be applied if the churn has not been consumed. In this case the balance of the validator will be increased again, so there is no difference with B’s state, except that the validator has switched to a compounding validator. If the churn has been reached, then the balance after P for that validator will be decreased to 32ETH, and a pending deposit will remain.

Pending consolidations

No pending consolidations from P are processed since the withdrawable epoch has not been reached

Effective balance updates

This is where there may be problems! A switch to consolidation request from a validator that actually had more than 32ETH will result in the effective balance of this validator to be different since the effective balance would have changed from the max 32ETH before. However, this requires the validator to have had more than 32.25ETH to start with

We see thus that modulo the potential problem of a validator with more than 32.25ETH switching to compounding on P, there can’t be any difference in effective balances nor active validator sets on the transition from e to e+1, so both E and F states share the same active validator indices and both share the same effective balances.

This carries on for the same reasons as above for at least 5 more epochs. So as long as there has not been more than 4 epochs with missing blocks, these states remain with the same active validators and effective balances (notice that switch to compounding validators included from P could not really increase effective balance unless there are blocks included).

Effect on validation

Validations that require the proposer lookahead are not changed, the proposer lookahead from F or E is the exact same. Validations that require active validators and active validator balances also cannot change from F to E.

The only change can be if both F and E are then advanced, without any blocks being included, to future epochs. We see that this requires at least 5 epochs for these states to differ. So a possible problem arises when there are 5 epochs of missing blocks and suddenly the chain recovers and immediately justifies the block B used as target. Future chains will have a different set of justified balances if they use F advanced or if they use E advanced.