Skip to content

Conversation

@fradamt
Copy link
Contributor

@fradamt fradamt commented Dec 30, 2025

There's a few changes. Firstly, there's a fix to the mechanism for setting the proposer boost root: we only update it if block.proposer_index matches the expected proposer of your current head_state. This way you can't nullify proposer boost by just creating forks with a divergent proposer sequence. Also small refactor with the updating logic moved to update_proposer_boost_root, and the block timeliness logic in record_block_timeliness. All of this is essentially unrelated to epbs.

The other changes are to make sure that the trustless payment mechanism works even if a proposer is willing to equivocate. What we want to avoid is in particular this situation:

  1. A builder reveals because they see a block B with > 40% of the weight, and no equivocations
  2. The proposer reveals an equivocating block B'
  3. The next proposer (maybe colluding) extends B' instead of B
  4. Proposer boost applies to B', and in addition B' has a little bit of adversarial attestation weight, so B gets reorged

The changes to handle this are:

  1. store.block_timeliness now records two timeliness indicators, the usual one for the attestation deadline and another one for the ptc deadline (whether the beacon block was received by the ptc deadline). As explained later, this is because when deciding whether something is an equivocation we only want to consider blocks that are received sufficiently early, so that the next proposer should have seen them. A late released block might not be seen by the next proposer, so we can't enforce them to reorg it, and moreover late released blocks are not dangerous because they can't accrue honest attestation weight.
  2. should_apply_proposer_boost is added to determine whether the proposer boost should be applied to the fork-choice. It is always applied when there are no equivocations. If there are, it is applied only when set to blocks with a parent older than one slot or not weak (more than 20% of votes), i.e. in particular it is not applied to weak, equivocating blocks from the previous slot. The idea is that these should always be reorged by the proposer, and if they don't do it we do not give them a proposer boost. This is why it's important to only consider equivocations that are seen sufficiently early, so we don't unfairly penalize the proposer.
  3. get_proposer_head has a more aggressive behavior when there is an equivocation in the previous slot: in that case, it always reorgs a weak head regardless of the other conditions (exactly the behavior mentioned above). Here we consider any equivocations, even if received late: it is only when attesting that the timeliness matters.
  4. is_head_weak now counts the weight of the block as get_weight (the usual weight) + the weight of equivocating validators from the committee of block.slot. This is so that, if a proposer sees a head as not weak (> 20% weight), it stays so for attesters even if there are some equivocations. The attesters then apply proposer boost normally, even if there are equivocations.

Assuming < 20% adversary, we get builder reveal safety:
Builder reveals when seeing block B with > 40% weight and no equivocations
=> if the proposer extends an equivocation B', proposer boost doesn't apply to B': B' is weak because it is late and thus does not accrue honest weight, and attesters detect the existence of an equivocation (B is timely).
=> no matter what the proposer does, the block stays canonical

Honest proposers are also still able to take advantage of proposer boost:

  • Proposer does not see any equivocation
    => attesters do not record any equivocations as timely (in record_block_timeliness)
    => proposer boost applies normally (should_apply_proposer_boost returns True)
  • Proposer sees equivocations, weak head (< 20% weight)
    => it extends the parent (get_proposer_head returns the parent)
    => proposer boost applies since the proposal's parent is earlier than the previous slot (should_apply_proposer_boost returns True)
    => the reorg succeeds since head is weak
  • Proposer sees equivocations, not weak head (> 20% weight)
    => it extends the head (get_proposer_head returns the head)
    => attesters also see the head as not weak (equivocations do not decrease the weight counted by is_head_weak)
    => proposer boost applies normally (should_apply_proposer_boost returns True)

*Note*: the function `is_head_weak` now also counts weight from equivocating
validators from the committees of the head slot. This ensures that the counted
weight and the output of `is_head_weak` are monotonic: more attestations can
only increase the weight and change the output from `False` to `True`, not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

increasing the weight should change the output from True to False not otherwise.

### New `record_block_timeliness`

```python
def record_block_timeliness(store: Store, root: Root) -> None:
Copy link
Contributor

@terencechain terencechain Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not New, but rather Modified right? the only difference here is PTC_TIMELINESS_INDEX

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right it's new in the sense that I just added the helper in this PR, but it's not new to the gloas spec

Co-authored-by: satushh <satushh@gmail.com>
fradamt and others added 3 commits January 5, 2026 16:12
Co-authored-by: Justin Traglia <95511699+jtraglia@users.noreply.github.com>
jtraglia and others added 4 commits January 5, 2026 13:16
Change signature from is_parent_strong(store, parent_root) to
is_parent_strong(store, root) where root is the child block.
The function now derives parent_root internally.

This change is needed for gloas/ePBS where we need to know
which child block to determine the parent's payload status
(FULL vs EMPTY) via get_parent_payload_status.

Also adds is_parent_strong to gloas fork-choice using
get_attestation_weight with proper ForkChoiceNode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After merge, get_attestation_weight was renamed to get_attestation_score
with an additional state parameter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Member

@jtraglia jtraglia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks @fradamt!

@jtraglia jtraglia merged commit 71d1151 into ethereum:master Jan 5, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants