fix(CHAIN-4357): add monotonicity guard to FinalizeTask to prevent finalized_head regression#2587
Draft
eric-ships wants to merge 2 commits intomainfrom
Conversation
chore(BASE-000): update main branch to include some other changes
…nalized_head regression BinaryHeap does not enforce ordering between same-variant EngineTask entries, so a stale FinalizeTask with a lower block number can be popped after a higher one has already executed, regressing finalized_head. Add an early-return guard that skips execution when block_number is strictly less than the current finalized_head number.
Collaborator
🟡 Heimdall Review Status
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Comment on lines
+37
to
+39
| if self.block_number < state.sync_state.finalized_head().block_info.number { | ||
| return Ok(()); | ||
| } |
Contributor
There was a problem hiding this comment.
Nit: consider adding a debug! or warn! log when the guard fires. The happy path already logs at info! level (line 75), and silently dropping a stale finalization task makes it harder to diagnose ordering issues in production. Something like:
Suggested change
| if self.block_number < state.sync_state.finalized_head().block_info.number { | |
| return Ok(()); | |
| } | |
| if self.block_number < state.sync_state.finalized_head().block_info.number { | |
| debug!( | |
| target: "engine", | |
| block_number = self.block_number, | |
| finalized_head = state.sync_state.finalized_head().block_info.number, | |
| "skipping stale finalize task" | |
| ); | |
| return Ok(()); | |
| } |
Non-blocking — the fix is correct as-is.
Contributor
Review SummaryThe fix is correct and well-scoped. The monotonicity guard at What's good:
One minor suggestion:
No correctness, safety, or architectural concerns. |
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.
CHAIN-4357 | Immunefi #75698
Context
Immunefi report #75698 identifies a finalized-head regression bug in the consensus engine.
EngineTask::Ordtreats all same-variantFinalizeTaskentries asOrdering::Equal. The engine uses aBinaryHeap<EngineTask>(max-heap), and Rust'sBinaryHeapdoes not guarantee stable ordering for equal elements. When twoFinalizeTasks with differentblock_numbers are enqueued, their relative pop order is non-deterministic.Attack path:
FinalizeTask(block_number=105)is popped first, advancingfinalized_headto 105.FinalizeTask(block_number=100)is popped next. The existing safe-head check passes (safe_head >= 100). The task fetches block 100 and callsSynchronizeTask, which setsfinalized_headback to 100.This is a confirmed valid finding per Andreas Bigger.
Changes
crates/consensus/engine/src/task_queue/tasks/finalize/task.rsAdds a monotonicity guard as the first check in
FinalizeTask::execute(). Ifblock_numberis strictly less than the currentfinalized_head.number, the task returnsOk(())immediately without fetching the block or dispatching a forkchoice update.crates/consensus/engine/src/task_queue/tasks/finalize/task_test.rsblock_not_found_returns_errorto explicitly setfinalized_headbelowblock_numberso the guard does not fire (previously the test implicitly hadfinalized_head == safe_head == 10while requesting block 7, which would now correctly no-op).stale_task_does_not_regress_finalized_head: directly exercises the vulnerability scenario (finalized_head = 10,block_number = 7, no RPC mocks registered). Verifies the task no-ops andfinalized_headremains 10.What was tried / considered
Fix
EngineTask::Ordto orderFinalizeTasks byblock_number(descending): This would make the BinaryHeap always pop the higher-numbered task first, which would prevent the regression in the common case. However, it does not help when a lower-numbered task arrives after the higher one has already been executed and popped. The monotonicity guard is strictly more robust because it handles both ordering and late-arriving tasks.Use
<=instead of<for the guard: Andreas's gist used<=, which would also no-op on re-finalization of the same block number. This is safe in practice (finalized blocks cannot change), but it would break the genesis block finalization tests (finalized_head.number = 0,block_number = 0) where the initial all-zerofinalized_headhash needs to be updated to the real genesis hash. Strict<avoids that edge case while still fully preventing regressions.Test plan
cargo test -p base-consensus-engine -- finalizepasses (7/7 tests)