Skip to content

fix(fork-choice): thread proposerBoostRoot into getPayloadStatusTiebreaker - #8944

Merged
ensi321 merged 5 commits into
nc/epbs-fcfrom
te/fix_should_extend_payload
Feb 24, 2026
Merged

fix(fork-choice): thread proposerBoostRoot into getPayloadStatusTiebreaker#8944
ensi321 merged 5 commits into
nc/epbs-fcfrom
te/fix_should_extend_payload

Conversation

@twoeths

@twoeths twoeths commented Feb 23, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a bug in the Gloas (ePBS) fork choice where proposerBoostRoot was hardcoded to null at every call site of getPayloadStatusTiebreaker inside maybeUpdateBestChildAndDescendant.

Root cause: shouldExtendPayload checks four conditions in order. Condition 2 is proposerBoostRoot === null → return true. With null always passed, FULL always wins the EMPTY vs FULL tiebreaker for previous-slot blocks, regardless of PTC votes. This breaks the core ePBS invariant: when a payload is not timely, the chain should extend via EMPTY.

Fix: Thread proposerBoostRoot explicitly through the call chain:

  • maybeUpdateBestChildAndDescendant(parentIndex, childIndex, currentSlot, proposerBoostRoot)
  • applyScoreChanges → passes proposerBoost?.root ?? null
  • ProtoArray.onBlock and ProtoArray.onExecutionPayload → accept and forward the param
  • ForkChoice.onBlock and ForkChoice.onExecutionPayload → pass this.proposerBoostRoot

The getPayloadStatusTiebreaker body (return shouldExtend ? 2 : 0) is already spec-correct and is unchanged.

Test plan

  • All existing fork-choice unit tests pass (pnpm vitest run packages/fork-choice)
  • Add a test case to gloas.test.ts that sets a proposerBoostRoot and verifies EMPTY is preferred when shouldExtendPayload returns false (payload not timely, proposer boost applies to this block)

🤖 Generated with Claude Code

cc @ensi321 @nflaig

…eaker

`getPayloadStatusTiebreaker` calls `shouldExtendPayload` to decide
whether to prefer FULL over EMPTY for a previous-slot block. However,
`proposerBoostRoot` was hardcoded to `null` at every call site inside
`maybeUpdateBestChildAndDescendant`, causing `shouldExtendPayload` to
always short-circuit on the "no proposer boost" condition and return
`true` — making FULL always win the tiebreaker regardless of PTC votes.

This breaks the core ePBS invariant: when a payload is not timely
(insufficient PTC votes and proposer boost applies), the chain should
extend via the EMPTY variant, not FULL.

Fix by threading `proposerBoostRoot` through the call chain:
- `maybeUpdateBestChildAndDescendant` now accepts `proposerBoostRoot`
- `applyScoreChanges` passes `proposerBoost?.root ?? null`
- `onBlock` and `onExecutionPayload` in ProtoArray accept and forward it
- `ForkChoice` passes `this.proposerBoostRoot` to both ProtoArray methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@twoeths
twoeths requested a review from a team as a code owner February 23, 2026 08:25
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @twoeths, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a significant bug within the Gloas (ePBS) fork choice mechanism. Previously, a hardcoded null value for proposerBoostRoot caused an incorrect tie-breaking decision, favoring full payloads even when they were untimely. The fix involves correctly propagating the proposerBoostRoot parameter throughout the relevant call chain, ensuring that the fork choice logic accurately applies proposer boost rules and maintains the critical ePBS invariant of preferring EMPTY payloads for untimely blocks.

Highlights

  • Bug Fix in Gloas Fork Choice: Addressed a critical bug where proposerBoostRoot was hardcoded to null in getPayloadStatusTiebreaker calls, leading to incorrect tie-breaking for previous-slot blocks.
  • ePBS Invariant Restoration: Corrected the behavior to ensure that when a payload is not timely, the chain correctly extends via an EMPTY payload, upholding a core ePBS invariant.
  • proposerBoostRoot Threading: Explicitly threaded the proposerBoostRoot parameter through ForkChoice and ProtoArray methods, including onBlock, onExecutionPayload, and maybeUpdateBestChildAndDescendant, to ensure its correct usage in tie-breaking logic.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/fork-choice/src/forkChoice/forkChoice.ts
    • Passed this.proposerBoostRoot to protoArray.onBlock when a new block is processed.
    • Passed this.proposerBoostRoot to protoArray.onExecutionPayload when an execution payload is processed.
  • packages/fork-choice/src/protoArray/protoArray.ts
    • Updated the onBlock method signature to accept proposerBoostRoot: RootHex | null.
    • Updated the onExecutionPayload method signature to accept proposerBoostRoot: RootHex | null.
    • Updated the maybeUpdateBestChildAndDescendant method signature to accept proposerBoostRoot: RootHex | null.
    • Modified calls to maybeUpdateBestChildAndDescendant within onBlock and onExecutionPayload to pass the proposerBoostRoot parameter.
    • Modified calls to getPayloadStatusTiebreaker to use the provided proposerBoostRoot instead of a hardcoded null.
    • Initialized proposerBoostRoot as null in createFromBlock when calling maybeUpdateBestChildAndDescendant.
Activity
  • All existing fork-choice unit tests have passed.
  • A new test case is planned for gloas.test.ts to validate EMPTY preference when proposerBoostRoot is set and shouldExtendPayload returns false.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

The pull request correctly addresses a bug in the ePBS fork choice by threading the proposerBoostRoot through the call chain to ensure it is available for the payload status tie-breaker logic. This fix is essential for maintaining the invariant that the chain should extend via the EMPTY variant when a payload is not timely and a child block with proposer boost extends that EMPTY variant.

However, the signature changes in ProtoArray.onBlock, ProtoArray.onExecutionPayload, and ProtoArray.maybeUpdateBestChildAndDescendant introduce breaking changes for existing call sites. Specifically, the unit tests in packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts and packages/fork-choice/test/unit/protoArray/protoArray.test.ts (as seen in the provided file context) still call these methods without the new required parameter, which will lead to compilation errors. Providing a default value of null for the new proposerBoostRoot parameter would maintain backward compatibility and resolve these issues.

Comment thread packages/fork-choice/src/protoArray/protoArray.ts
Comment thread packages/fork-choice/src/protoArray/protoArray.ts
Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
twoeths and others added 2 commits February 23, 2026 15:38
Pass null for proposerBoostRoot at the three protoArray.onBlock() calls
in the ProtoArray initialization path — no proposer boost applies during
initialization from stored state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l sites in tests

Pass null for proposerBoostRoot at all protoArray.onBlock() and
protoArray.onExecutionPayload() call sites in test and perf files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@twoeths
twoeths marked this pull request as draft February 23, 2026 08:54
twoeths and others added 2 commits February 23, 2026 16:00
@twoeths
twoeths marked this pull request as ready for review February 23, 2026 09:53

@ensi321 ensi321 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm. Note that this does not update our proposer boost logic but only to fix the bug of tie breaker.

It also does not affect Fulu code path because tie breaker is not called in fulu due to different weight and different root.

@ensi321
ensi321 merged commit 22f209c into nc/epbs-fc Feb 24, 2026
16 of 17 checks passed
@ensi321
ensi321 deleted the te/fix_should_extend_payload branch February 24, 2026 00:45
@codecov

codecov Bot commented Feb 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.37%. Comparing base (60d71bf) to head (1456512).
⚠️ Report is 13 commits behind head on nc/epbs-fc.

Additional details and impacted files
@@             Coverage Diff             @@
##           nc/epbs-fc    #8944   +/-   ##
===========================================
  Coverage       52.37%   52.37%           
===========================================
  Files             848      848           
  Lines           63198    63190    -8     
  Branches         4676     4676           
===========================================
- Hits            33098    33094    -4     
+ Misses          30032    30028    -4     
  Partials           68       68           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants