fix(forkchoice): derive finalized from canonical head, not an independent max#1001
Open
GrapeBaBa wants to merge 1 commit into
Open
Conversation
aa1702d to
17f85ea
Compare
…dent max The store advanced latest_finalized by an independent monotonic max over every imported block's post-state, decoupling it from the head. A fork that finalized a higher slot but then lost head selection left its finalized checkpoint latched in the store. get_attestation_target derives the attestation target against store.latest_finalized, while the state transition validates targets against the canonical state's latest_finalized; once the two finalized slots diverged, is_justifiable_after disagreed and every advancing target was rejected, so finalization froze. Derive the finalized checkpoint from the canonical head's chain instead: take the finalized slot named by the head state, and recover its root as the head's own ancestor at that slot. The checkpoint then always lies on the head chain, the finalized slot a validator attests against matches the slot the state transition validates against, and a checkpoint-sync anchor survives because the head descends from it. The fork choice is not clamped to finalized descendants: leanSpec finalization is order dependent, so a fork that finalizes a higher slot can still lose head selection, and clamping would latch the head onto that losing fork. Deriving finalized from the head keeps it on the head chain without constraining head selection.
17f85ea to
66beecb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fixes #1000.
on_blockadvancedstore.latest_finalizedby an independent monotonic max over every imported block's post-state, decoupled fromstore.latest_justifiedand the head. A fork that finalizes a higher slot but then loses head selection left its finalized checkpoint latched in the store, sostore.latest_finalizedcould sit on a dead branch that is not an ancestor of the head, whilehead/latest_justifiedwere on a different branch whose state finalized is lower.get_attestation_targetdecides target justifiability againststore.latest_finalized, whileprocess_attestationsvalidates targets against the canonical head-state's (lower) finalized. Once the two finalized slots diverge,is_justifiable_afterdisagrees, every advancing target is rejected, and finalization freezes — first observed as a network-wide finalization freeze in a long-running multi-subnet devnet.The original forkchoice store (#53) and the 3sf-mini reference derive finalized from the chosen (head) state; #194 changed it to the independent max (see #1000 for the full root-cause trace).
Fix
Derive
store.latest_finalizedfrom the canonical head's chain after head selection:The checkpoint then always lies on the head chain (a fork that loses head selection cannot pin its finalized), and crucially
store.latest_finalized.slot == head-state.latest_finalized.slot, so the slot a validator attests against matches the slot the state transition validates against — the desync that froze finalization is gone. This matches 3sf-mini, where the finalized checkpoint is read from the chosen head's state.Taking the root as the head's ancestor (rather than copying
head-state.latest_finalized.root) also recovers the correct root for a checkpoint-sync anchor: a state can never name its own block as finalized, so the anchor state carries a placeholder root while the head's ancestor at the anchor slot is the real anchor block.