Skip to content

refactor(fork-choice): simplify maybeUpdateBestChildAndDescendant Gloas edge case - #8951

Merged
ensi321 merged 3 commits into
nc/epbs-fcfrom
te/fix_epbs_maybe_update_best_child_and_desc
Feb 25, 2026
Merged

refactor(fork-choice): simplify maybeUpdateBestChildAndDescendant Gloas edge case#8951
ensi321 merged 3 commits into
nc/epbs-fcfrom
te/fix_epbs_maybe_update_best_child_and_desc

Conversation

@twoeths

@twoeths twoeths commented Feb 24, 2026

Copy link
Copy Markdown
Member

Motivation

isEmptyVsFullEdgeCase() is not in the spec — it was an implementation helper that bypassed the standard weight → blockRoot tiebreaking order before calling getPayloadStatusTiebreaker(). Per spec, pre-Gloas nodes must always be tiebroken by weight first, then blockRoot. The guard was obscuring this and making the code harder to follow.

Description

  • Removes the private isEmptyVsFullEdgeCase() helper from ProtoArray
  • Removes the isEdgeCase guard that was short-circuiting weight and blockRoot comparisons
  • The guard was structurally unnecessary: two nodes that reach getPayloadStatusTiebreaker() already have equal weight and equal blockRoot by natural fall-through; that scenario can only arise for Gloas blocks (EMPTY vs FULL variants of the same block)
  • Adds explicit isGloasBlock() assertions before the tiebreaker to make the invariant clear and fail loudly if violated
  • Condenses weight/root if-else branches to ternaries for readability

Spec reference: https://github.com/ethereum/consensus-specs/blob/69a2582d5d62c914b24894bdb65f4bd5d4e49ae4/specs/gloas/fork-choice.md?plain=1#L442

AI Assistance Disclosure

🤖 Generated with Claude Code

cc @nflaig @ensi321

…as edge case

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@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 refactors the maybeUpdateBestChildAndDescendant method within ProtoArray to align more closely with the Ethereum consensus specification for Gloas fork-choice. It streamlines the logic by removing an unnecessary internal helper and a conditional guard that obscured the standard weight and blockRoot tie-breaking rules. The changes clarify the flow for Gloas-specific payload status tie-breaking and enhance code readability.

Highlights

  • Code Simplification: Removed the isEmptyVsFullEdgeCase() helper function, which was an implementation detail not aligned with the spec.
  • Logic Streamlining: Eliminated the isEdgeCase guard, simplifying the logic for weight and blockRoot comparisons.
  • Spec Compliance & Safety: Introduced explicit isGloasBlock() assertions to enforce the invariant that only Gloas blocks should reach the payload status tiebreaker.
  • Readability Improvement: Refactored conditional assignments into more concise ternary operators for improved readability.

🧠 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/protoArray/protoArray.ts
    • Removed the isEmptyVsFullEdgeCase private method.
    • Eliminated the isEdgeCase guard from the block comparison logic.
    • Converted if-else statements for newChildAndDescendant assignment into ternary operators.
    • Added isGloasBlock assertions to validate block types before payload status tie-breaking.
Activity
  • No human activity has been recorded on this pull request yet.
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

This is an excellent refactoring that significantly simplifies the fork-choice logic in maybeUpdateBestChildAndDescendant. By removing the isEmptyVsFullEdgeCase helper and its associated guard, the code now relies on a more natural and easier-to-follow fall-through mechanism for tie-breaking. The addition of explicit isGloasBlock() assertions is a great improvement for robustness, making the code's invariants clear and ensuring it fails loudly if they are violated. The changes make the implementation cleaner and more aligned with the specification. Great work!

@twoeths
twoeths marked this pull request as ready for review February 24, 2026 03:50
@twoeths
twoeths requested a review from a team as a code owner February 24, 2026 03:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0c3b62c19

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
…tChildAndDescendant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/fork-choice/src/protoArray/protoArray.ts
* For older blocks: returns node.payloadStatus
*
* Note: pre-gloas logic won't reach here. Since it is impossible to have two nodes with same weight and root
* Note: pre-gloas logic won't reach here. Pre-Gloas blocks have different roots, so they are always resolved by the root tiebreaker before reaching here.

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.

Suggested change
* Note: pre-gloas logic won't reach here. Pre-Gloas blocks have different roots, so they are always resolved by the root tiebreaker before reaching here.
* Note: pre-gloas logic won't reach here. Pre-Gloas blocks have different roots, so they are always resolved by the weight and root tiebreaker before reaching here.

Comment on lines +1226 to +1229
// Per spec modified-get_weight (gloas/fork-choice.md#L442): a Gloas EMPTY/FULL block from the
// previous slot (n-1) has effective weight = 0 regardless of accumulated attestations.
// The isGloasBlock() guard prevents incorrectly zeroing pre-Gloas FULL blocks, which also have
// payloadStatus=FULL but must use their actual accumulated weight.

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.

I think these comments are very lengthy. I think it should be more concise:

Suggested change
// Per spec modified-get_weight (gloas/fork-choice.md#L442): a Gloas EMPTY/FULL block from the
// previous slot (n-1) has effective weight = 0 regardless of accumulated attestations.
// The isGloasBlock() guard prevents incorrectly zeroing pre-Gloas FULL blocks, which also have
// payloadStatus=FULL but must use their actual accumulated weight.
// Gloas: nodes from previous slot (n-1) with EMPTY/FULL variant post-gloas have weight hardcoded to 0.

const childTiebreaker = this.getPayloadStatusTiebreaker(childNode, currentSlot, proposerBoostRoot);
// Same effective weight and same root — must be Gloas EMPTY vs FULL from n-1
if (!isGloasBlock(childNode)) {
throw new ProtoArrayError({

@ensi321 ensi321 Feb 25, 2026

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.

I think throwing error here changes our Fulu behaviour.

The current behaviour on unstable is if we have two nodes with same weight and same root, we should do changeToChild:

// tie-breaker of equal weights by root
if (childNode.blockRoot >= bestChildNode.blockRoot) {
newChildAndDescendant = changeToChild;
} else {
newChildAndDescendant = noChange;
}

I think theoretically two nodes with same weight and same root should not happen, but I am not educated to 100% sure throwing error here wouldn't cause issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I removed it in the latest commit, don't think it will ever change the behaviour of pre-gloas through since 1 node cannot have 2 child nodes with the same root, that's enforced through VariantIndices typing

});
}

// Tie-breaker by payload status (EMPTY vs FULL)

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.

The comment from L1256 should move to here. .

…escendant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ensi321
ensi321 merged commit 2fdf1ec into nc/epbs-fc Feb 25, 2026
16 checks passed
@ensi321
ensi321 deleted the te/fix_epbs_maybe_update_best_child_and_desc branch February 25, 2026 03:13
@codecov

codecov Bot commented Feb 25, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@             Coverage Diff             @@
##           nc/epbs-fc    #8951   +/-   ##
===========================================
  Coverage       52.37%   52.37%           
===========================================
  Files             848      848           
  Lines           63047    63047           
  Branches         4670     4670           
===========================================
  Hits            33020    33020           
  Misses          29959    29959           
  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