Skip to content

fix: guard ClearStaleMarkersAbove behind forceUpdateHeadBlock#11004

Merged
asdacap merged 1 commit intomasterfrom
fix/guard-clear-stale-markers-hot-path
Mar 31, 2026
Merged

fix: guard ClearStaleMarkersAbove behind forceUpdateHeadBlock#11004
asdacap merged 1 commit intomasterfrom
fix/guard-clear-stale-markers-hot-path

Conversation

@asdacap
Copy link
Copy Markdown
Contributor

@asdacap asdacap commented Mar 31, 2026

Fixes a performance regression from #10876 that significantly slows down forward sync on nodes that are far behind.

Summary

  • FCU Canonical chain corruption after beacon sync (3 fixes + startup healing) #10876 added an unconditional upward scan (ClearStaleMarkersAbove) in UpdateMainChain to clear stale canonical markers left by beacon sync. The scan walks upward from head until LoadLevel returns null on every UpdateMainChain call.
  • During forward sync, BlockDownloader calls UpdateMainChain(block, false) per block. The scan traverses all pre-downloaded header levels above — O(K) per block, where K can be millions of levels on a node syncing from scratch.
  • When BlockchainProcessor falls behind, it calls UpdateMainChain(blocks, true) per processed block. The scan clears valid beacon-synced markers ahead of the processing front, causing O(K²) total work and temporarily breaking canonical lookups for blocks above.
  • The scan is only needed during FCU reorgs — the only production path that passes forceUpdateHeadBlock: true (ForkchoiceUpdatedHandler).

Fix: Guard the call with if (forceUpdateHeadBlock) so forward sync and forward block processing skip the scan entirely.

Changes

One line in BlockTree.cs: wrap ClearStaleMarkersAbove(...) with if (forceUpdateHeadBlock).

All 15 existing #10876 regression tests pass unchanged (they use forceUpdateHeadBlock: true for the FCU step). Two new tests verify:

  1. Forward processing preserves beacon-synced markers above
  2. FCU forward reorg to longer chain still clears stale markers

Types of changes

  • Optimization
  • Bugfix (a non-breaking change that fixes an issue)

Testing

🤖 Generated with Claude Code

…d hot-path scan

PR #10876 added an unconditional upward scan in UpdateMainChain that
clears stale canonical markers left by beacon sync. The scan runs on
every call and walks until LoadLevel returns null, which is O(K) per
block during forward sync (BlockDownloader) and O(K²) total when
BlockchainProcessor falls behind — also incorrectly clearing valid
beacon-synced markers ahead of the processing front.

The scan is only needed during FCU reorgs, which is the only path that
passes forceUpdateHeadBlock: true. Guard the call so forward sync and
forward block processing skip the scan entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@asdacap asdacap force-pushed the fix/guard-clear-stale-markers-hot-path branch from 5f8057c to 9b2cbac Compare March 31, 2026 07:44
@asdacap asdacap marked this pull request as ready for review March 31, 2026 07:58
@LukaszRozmej
Copy link
Copy Markdown
Member

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Mar 31, 2026

Claude finished @LukaszRozmej's task in 3m 2s —— View job


Code Review

  • Gather context
  • Read changed files
  • Read relevant rules
  • Analyze correctness and performance
  • Post review feedback

Summary

The fix is correct and well-targeted. The one-line guard properly restores the pre-#10876 hot-path behavior for both BlockDownloader (forward sync) and BlockchainProcessor (forward block processing), while keeping the stale-marker clearing active on the only path that actually needs it.


BlockTree.cs — Core change

src/Nethermind/Nethermind.Blockchain/BlockTree.cs:1031

if (forceUpdateHeadBlock)
    ClearStaleMarkersAbove(Math.Max(previousHeadNumber, lastNumber), batch);

Correctness analysis across all call sites:

Caller forceUpdateHeadBlock Effect after fix
ForkchoiceUpdatedHandler.cs:258 true Clears stale markers — correct
BlockTreeOverlay.cs:28 true Clears stale markers at init — correct
BlockchainProcessor.cs:502 false (default) Skips scan — correct
BlockDownloader.cs:553 false (default) Skips scan — correct
NonProcessingProducedBlockSuggester.cs:24 false (default) Skips scan — correct

The complexity claims are accurate: BlockDownloader calling UpdateMainChain([block], false) per block was causing O(K) per-block scans (K = levels above head), and BlockchainProcessor processing blocks above a beacon-synced region was causing O(K²) total work plus temporarily clearing valid markers needed for canonical lookups.

Minor comment inaccuracy (cosmetic, not a bug):

"The scan is only needed during FCU reorgs — the only production path that passes forceUpdateHeadBlock: true (ForkchoiceUpdatedHandler)."

BlockTreeOverlay.cs:28 also passes true. The claim in the PR description is slightly overstated, but it doesn't affect correctness since BlockTreeOverlay is an initialization step and behaves correctly with the guard.


BlockTreeTests.cs — New tests

Test 1: UpdateMainChain_WhenForwardProcessingWithBeaconSyncedDescendants_DoesNotClearMarkers

The test correctly models the regression scenario: FCU sets head to block 1, forward sync marks blocks 2–4, then forward processing of block 2 (forceUpdateHeadBlock: false) must not erase markers for blocks 3 and 4. Assertions on chain[2] and chain[3] are appropriate.

Test 2: UpdateMainChain_WhenFcuForwardReorgToLongerChain_ClearsStaleMarkersAboveNewHead

The test correctly verifies that FCU (forceUpdateHeadBlock: true) to chain B (height 3) clears the stale chain-A marker at height 4 via ClearStaleMarkersAbove(max(1, 3) = 3, batch). The math checks out.

Minor naming nitWhenFcuForwardReorgToLongerChain is slightly misleading: chain B stops at height 3 while chain A reaches height 4, so B is not strictly longer. Something like WhenFcuReorgToDifferentChain would be more accurate. This is cosmetic only.


Overall

The fix is minimal, focused, and correct. The new tests cover both the "preservation" and "clearing" behaviours. All existing #10876 regression tests remain valid since they go through the FCU (forceUpdateHeadBlock: true) path. No concerns beyond the cosmetic items noted above.

@asdacap asdacap merged commit 395755d into master Mar 31, 2026
436 of 437 checks passed
@asdacap asdacap deleted the fix/guard-clear-stale-markers-hot-path branch March 31, 2026 09:51
svlachakis pushed a commit that referenced this pull request Apr 3, 2026
fix: guard ClearStaleMarkersAbove behind forceUpdateHeadBlock to avoid hot-path scan

PR #10876 added an unconditional upward scan in UpdateMainChain that
clears stale canonical markers left by beacon sync. The scan runs on
every call and walks until LoadLevel returns null, which is O(K) per
block during forward sync (BlockDownloader) and O(K²) total when
BlockchainProcessor falls behind — also incorrectly clearing valid
beacon-synced markers ahead of the processing front.

The scan is only needed during FCU reorgs, which is the only path that
passes forceUpdateHeadBlock: true. Guard the call so forward sync and
forward block processing skip the scan entirely.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants