Skip to content

cl/phase1/forkchoice: align getFilterBlockTree with consensus spec#21317

Closed
sudeepdino008 wants to merge 1 commit into
mainfrom
sudeep/fix-getheadtree-spec-justification-main
Closed

cl/phase1/forkchoice: align getFilterBlockTree with consensus spec#21317
sudeepdino008 wants to merge 1 commit into
mainfrom
sudeep/fix-getheadtree-spec-justification-main

Conversation

@sudeepdino008

Copy link
Copy Markdown
Member

Summary

Cherry-pick / port of #21310 to main, combined with the finalized ancestor-descent check that was already on main (from #18956, commit f3ad70f1f7).

main already has a partial spec-faithful rewrite of getFilterBlockTree — the finalized side now does ancestor descent (correct per spec) — but the justified side still deviates from filter_block_tree / get_voting_source in two ways. This PR fixes those.

Spec reference

filter_block_tree:

voting_source = get_voting_source(store, block_root)
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
)

get_voting_source:

if current_epoch > block_epoch:
    return store.unrealized_justifications[block_root]   # pull-up
else:
    return head_state.current_justified_checkpoint        # realized

Changes

  1. Voting source selection: was unrealized-with-fallback unconditional. Now branched on current_epoch > block_epoch as the spec requires — unrealized only for prior-epoch blocks; current-epoch (or future) blocks use the block's realized current_justified_checkpoint.

  2. correct_justified: was voting_source.Epoch >= justifiedCheckpoint.Epoch with an is_previous_epoch_justified-gated +2 fallback. Now the three flat disjuncts straight from the spec: epoch == GENESIS or voting_source.epoch == justified.epoch or voting_source.epoch + 2 >= current_epoch. (The previous >= accepts cases spec rejects; the previous gated +2 rejects cases spec accepts.)

  3. correct_finalized: unchanged — keeps the ancestor-descent check from f3ad70f1f7 (finalizedCheckpoint.Root == f.Ancestor(blockRoot, finalizedSlot).Root).

Background

Original observed symptom on bloatnet was ~30-block execution unwinds every ~6 minutes (every epoch boundary). Root cause: getFilterBlockTree rejected the leaf at each new epoch's first slot, Caplin's head fell back to an older block, and execution unwound to follow. Both pre-existing deviations contributed:

  • Unconditional unrealized fed a stale checkpoint into the check for current-epoch leaves.
  • Strict Equal (or >= with the gated fallback) rejected leaves whose voting source's epoch was within 2 of current_epoch but not equal to justified.epoch.

Issue: #21301
Original performance-branch PR: #21310

Test plan

  • Spec-faithful: code matches the consensus-specs Python source line-for-line
  • go build ./cl/phase1/forkchoice/ clean
  • Bloatnet runtime validation on performance (cl/phase1/forkchoice: align getFilterBlockTree with consensus spec #21310): 1 hour at chain tip, 5+ epoch boundaries, zero unwinds, zero filterTree rejects, Caplin lagSlots=0 on all 172 FCU events
  • CI green on main
  • Caplin unit tests pass

@sudeepdino008 sudeepdino008 force-pushed the sudeep/fix-getheadtree-spec-justification-main branch from 8663209 to 7f1b3d0 Compare May 21, 2026 06:50
The post-GLOAS rewrite (#18956, f3ad70f) restored ancestor-descent
for the finalized check but left two justified-side deviations from
the spec's filter_block_tree / get_voting_source:

1. Voting source selection used unrealized unconditionally with a
   realized fallback. Spec get_voting_source branches strictly on
   current_epoch > block_epoch — unrealized only for prior-epoch
   blocks; current-epoch blocks must use the block's realized
   current_justified_checkpoint.

2. correct_justified used voting_source.epoch >= justified.epoch with
   an is_previous_epoch_justified-gated +2 fallback. Spec is the
   three flat disjuncts: epoch == GENESIS, voting_source.epoch ==
   justified.epoch, or voting_source.epoch + 2 >= current_epoch.

The finalized ancestor-descent check from f3ad70f is preserved
unchanged.

Observed effect on bloatnet: epoch-boundary head regression that
caused ~30-block execution unwinds every ~6 minutes is eliminated.
Validated by 1 hour at chain tip on bloatnet covering 5+ epoch
boundaries — zero unwinds, zero filterTree rejects, Caplin lagSlots=0
on all 172 forkchoice updates in the window.

Refs: #21301, #21310
@sudeepdino008 sudeepdino008 force-pushed the sudeep/fix-getheadtree-spec-justification-main branch from 7f1b3d0 to 9402e29 Compare May 21, 2026 06:59
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.

1 participant