Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5d383ec
FCU to canonical ancestor silently ignored, leaving descendants canon…
svlachakis Mar 19, 2026
4de70a8
fix comments
svlachakis Mar 19, 2026
3e6e6b9
fix comment
svlachakis Mar 19, 2026
2c05551
Merge branch 'master' into canonical-fix
svlachakis Mar 19, 2026
4c73b74
Merge remote-tracking branch 'origin/canonical-fix' into canonical-fix
svlachakis Mar 19, 2026
4f89834
change approach
svlachakis Mar 19, 2026
9dad401
remove empty line
svlachakis Mar 19, 2026
c090be2
HealCanonicalChain implementation & tests
svlachakis Mar 19, 2026
55d1487
fix build
svlachakis Mar 19, 2026
28b3961
ePBS FCU fix
svlachakis Mar 20, 2026
3aed814
Merge branch 'master' into canonical-fix
svlachakis Mar 20, 2026
d7bb41a
Merge branch 'master' into canonical-fix
svlachakis Mar 20, 2026
663d628
Merge branch 'master' into canonical-fix
svlachakis Mar 21, 2026
7d28f8a
Merge branch 'master' into canonical-fix
svlachakis Mar 22, 2026
c3583e1
Merge branch 'master' into canonical-fix
svlachakis Mar 23, 2026
3f6cb8a
geth parity with https://github.com/ethereum/go-ethereum/blob/745b0a8…
svlachakis Mar 23, 2026
4231c21
Merge branch 'master' into canonical-fix
svlachakis Mar 23, 2026
883eafd
GetBlockHashOnMainOrBestDifficultyHash now returns null when
svlachakis Mar 24, 2026
95cb3e4
Merge branch 'master' into canonical-fix
svlachakis Mar 24, 2026
e83b46d
revert change for WasProcessed=false
svlachakis Mar 24, 2026
d9024ad
Merge branch 'master' into canonical-fix
kamilchodola Mar 24, 2026
590c37b
test: add beacon sync + reorg stale marker reproduction test
kamilchodola Mar 24, 2026
1da5cf6
test: add failing gap test for beacon sync race condition
kamilchodola Mar 24, 2026
a8cf7c7
fix: skip gaps in upward scan instead of breaking
kamilchodola Mar 24, 2026
447a0e9
Merge remote-tracking branch 'origin/master' into canonical-fix
svlachakis Mar 25, 2026
292985c
PR cleanup
svlachakis Mar 25, 2026
de523da
remove misleading comment
svlachakis Mar 25, 2026
06a1567
minor copilot comments
svlachakis Mar 25, 2026
2ab930b
Merge branch 'master' into canonical-fix
svlachakis Mar 26, 2026
25f24ac
Merge branch 'master' into canonical-fix
svlachakis Mar 26, 2026
07dfcc0
review comments
svlachakis Mar 27, 2026
e2de610
Merge remote-tracking branch 'origin/canonical-fix' into canonical-fix
svlachakis Mar 27, 2026
d498043
Merge branch 'master' into canonical-fix
svlachakis Mar 27, 2026
e2a9322
Canonical fix refactor (#10972)
LukaszRozmej Mar 27, 2026
402195d
Lukasz review
svlachakis Mar 27, 2026
b7a54d1
fixes
LukaszRozmej Mar 27, 2026
62ba845
Revert "fixes"
LukaszRozmej Mar 27, 2026
b646a6a
Lukasz review - Refactoring
svlachakis Mar 27, 2026
0ccac8b
Merge remote-tracking branch 'origin/canonical-fix' into canonical-fix
svlachakis Mar 27, 2026
d1bd021
fixes
svlachakis Mar 27, 2026
c2f3d48
Merge branch 'master' into canonical-fix
svlachakis Mar 27, 2026
fb43b3b
revert blocktree registration
svlachakis Mar 27, 2026
6f0ccd9
Merge branch 'master' into canonical-fix
svlachakis Mar 27, 2026
b830b19
Merge branch 'master' into canonical-fix
svlachakis Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Nethermind/Nethermind.Api/IInitConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ public interface IInitConfig : IConfig

[ConfigItem(Description = "[TECHNICAL] True when in runner test. Disable some wait.", DefaultValue = "false", HiddenFromDocs = true)]
bool InRunnerTest { get; set; }

[ConfigItem(
Description = "On startup, walk backward from the current head for up to `HealCanonicalChainDepth` " +
"blocks via parentHash and repair any incorrect `HasBlockOnMainChain` markers left by " +
"the beacon-sync path. Also clears stale canonical markers above the head. " +
"Use this flag once after observing a canonical-mismatch (wrong `eth_getBlockByNumber` results).",
DefaultValue = "false")]
Comment thread
svlachakis marked this conversation as resolved.
Outdated
bool HealCanonicalChain { get; set; }

[ConfigItem(
Description = "Number of blocks to walk back from the head when `HealCanonicalChain` is enabled.",
DefaultValue = "8192")]
Comment thread
svlachakis marked this conversation as resolved.
Outdated
long HealCanonicalChainDepth { get; set; }
}

public enum DiagnosticMode
Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Api/InitConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class InitConfig : IInitConfig
public int BackgroundTaskMaxNumber { get; set; } = 2048;
public bool InRunnerTest { get; set; } = false;
public string? DataDir { get; set; }
public bool HealCanonicalChain { get; set; } = false;
public long HealCanonicalChainDepth { get; set; } = 8192;

[Obsolete("Use DiagnosticMode with MemDb instead")]
public bool UseMemDb
Expand Down
590 changes: 590 additions & 0 deletions src/Nethermind/Nethermind.Blockchain.Test/BlockTreeTests.cs

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/Nethermind/Nethermind.Blockchain/BlockTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,14 @@ as it does not require the step of resolving number -> hash */
return level.BlockInfos[0].BlockHash;
}

// Post-merge: TD never increases, so the best-TD fallback cannot distinguish canonical from orphaned.
if (SpecProvider.TerminalTotalDifficulty is not null
&& (Head?.TotalDifficulty ?? UInt256.Zero) >= SpecProvider.TerminalTotalDifficulty)
{
return null;
}

// Pre-merge: the block with the highest total difficulty is canonical by PoW rule.
UInt256 bestDifficultySoFar = UInt256.Zero;
Hash256 bestHash = null;
for (int i = 0; i < level.BlockInfos.Length; i++)
Expand Down Expand Up @@ -1014,6 +1022,10 @@ public void UpdateMainChain(IReadOnlyList<Block> blocks, bool wereProcessed, boo
}
}

// Clear stale canonical markers above the new head left by beacon sync.
// Covers both same-height FCU (previousHeadNumber <= lastNumber) and ePBS FCU to ancestor.
ClearStaleMarkersAbove(Math.Max(previousHeadNumber, lastNumber), batch);

for (int i = 0; i < blocks.Count; i++)
{
Block block = blocks[i];
Expand All @@ -1035,6 +1047,84 @@ public void UpdateMainChain(IReadOnlyList<Block> blocks, bool wereProcessed, boo
OnUpdateMainChain?.Invoke(this, new OnUpdateMainChainArgs(blocks, wereProcessed));
}

public void HealCanonicalChain(Hash256 startHash, long maxBlockDepth)
Comment thread
svlachakis marked this conversation as resolved.
Outdated
{
BlockHeader? start = FindHeader(startHash, BlockTreeLookupOptions.None);
if (start is null) return;

using BatchWrite batch = _chainLevelInfoRepository.StartBatch();

long repairedAbove = ClearStaleMarkersAbove(start.Number, batch);
long repairedBelow = RepairMarkersBelow(start, maxBlockDepth, batch);

if (Logger.IsInfo) Logger.Info($"Canonical chain heal complete: {repairedAbove + repairedBelow} level(s) repaired ({repairedAbove} stale above head cleared, {repairedBelow} incorrect markers fixed).");
}

private long ClearStaleMarkersAbove(long fromExclusive, BatchWrite batch)
{
long cleared = 0L;
for (long levelNumber = fromExclusive + 1; ; levelNumber++)
{
ChainLevelInfo? level = LoadLevel(levelNumber);
if (level is null) break;
if (level.HasBlockOnMainChain)
{
level.HasBlockOnMainChain = false;
_chainLevelInfoRepository.PersistLevel(levelNumber, level, batch);
cleared++;
}
}
return cleared;
}

private long RepairMarkersBelow(BlockHeader start, long maxBlockDepth, BatchWrite batch)
{
long repairedCount = 0L;
long blocksWalked = 0L;
BlockHeader? current = start;

while (current is not null && blocksWalked < maxBlockDepth)
Comment thread
svlachakis marked this conversation as resolved.
Outdated
{
ChainLevelInfo? level = LoadLevel(current.Number);
if (level is not null)
{
int? index = level.FindIndex(current.Hash!);
if (index is null)
{
if (Logger.IsWarn) Logger.Warn($"Canonical heal: block {current.Hash} at height {current.Number} not found in any BlockInfo slot — repair halted.");
break;
}

bool needsRepair = index.Value != 0 || !level.HasBlockOnMainChain;

if (index.Value != 0)
level.SwapToMain(index.Value);

level.HasBlockOnMainChain = true;

if (needsRepair)
{
_chainLevelInfoRepository.PersistLevel(current.Number, level, batch);
repairedCount++;
}
}

if (current.IsGenesis) break;

BlockHeader? parent = FindHeader(current.ParentHash!, BlockTreeLookupOptions.None);
if (parent is null)
{
if (Logger.IsWarn) Logger.Warn($"Canonical heal: parent {current.ParentHash} of block {current.Number} not found — chain may be pruned, repair halted.");
break;
}

current = parent;
blocksWalked++;
}

return repairedCount;
}

private void TryUpdateSyncPivot()
{
BlockHeader? newPivotHeader = null;
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Blockchain/BlockTreeOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ public void UpdateBeaconMainChain(BlockInfo[]? blockInfos, long clearBeaconMainC
_overlayTree.UpdateBeaconMainChain(blockInfos, clearBeaconMainChainStartPoint);

public void RecalculateTreeLevels() => _overlayTree.RecalculateTreeLevels();
public void HealCanonicalChain(Hash256 startHash, long maxBlockDepth) => _overlayTree.HealCanonicalChain(startHash, maxBlockDepth);
public (long BlockNumber, Hash256 BlockHash) SyncPivot
{
get => _baseTree.SyncPivot;
Expand Down
8 changes: 8 additions & 0 deletions src/Nethermind/Nethermind.Blockchain/IBlockTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ AddBlockResult Insert(Block block, BlockTreeInsertBlockOptions insertBlockOption

void RecalculateTreeLevels();

/// <summary>
/// Repairs canonical chain markers starting from <paramref name="startHash"/>, walking backward
/// up to <paramref name="maxBlockDepth"/> blocks. Also removes stale canonical markers above
/// <paramref name="startHash"/> left by the beacon-sync path. Safe to run at startup after
/// observing canonical mismatches (wrong <c>eth_getBlockByNumber</c> results).
/// </summary>
void HealCanonicalChain(Hash256 startHash, long maxBlockDepth);
Comment thread
svlachakis marked this conversation as resolved.
Outdated

/// <summary>
/// Sync pivot is mainly concerned with old blocks and receipts.
/// After sync pivot, blocks and headers should be continuous.
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Blockchain/ReadOnlyBlockTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ public int DeleteChainSlice(in long startNumber, long? endNumber = null, bool fo
public bool IsBetterThanHead(BlockHeader? header) => _wrapped.IsBetterThanHead(header);
public void UpdateBeaconMainChain(BlockInfo[]? blockInfos, long clearBeaconMainChainStartPoint) => throw new InvalidOperationException($"{nameof(ReadOnlyBlockTree)} does not expect {nameof(UpdateBeaconMainChain)} calls");
public void RecalculateTreeLevels() => throw new InvalidOperationException($"{nameof(ReadOnlyBlockTree)} does not expect {nameof(RecalculateTreeLevels)} calls");
public void HealCanonicalChain(Hash256 startHash, long maxBlockDepth) => throw new InvalidOperationException($"{nameof(ReadOnlyBlockTree)} does not expect {nameof(HealCanonicalChain)} calls");
Comment thread
svlachakis marked this conversation as resolved.
Outdated
public (long BlockNumber, Hash256 BlockHash) SyncPivot
{
get => _wrapped.SyncPivot;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ public void UpdateBeaconMainChain(BlockInfo[]? blockInfos, long clearBeaconMainC
public void RecalculateTreeLevels()
=> throw new NotSupportedException();

public void HealCanonicalChain(Hash256 startHash, long maxBlockDepth)
Comment thread
svlachakis marked this conversation as resolved.
Outdated
=> throw new NotSupportedException();

public (long BlockNumber, Hash256 BlockHash) SyncPivot
{
get => throw new NotSupportedException();
Expand Down
19 changes: 16 additions & 3 deletions src/Nethermind/Nethermind.Init/Steps/ReviewBlockTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Nethermind.Blockchain.Synchronization;
using Nethermind.Blockchain.Visitors;
using Nethermind.Consensus.Processing;
using Nethermind.Core.Crypto;
using Nethermind.Logging;
using Nethermind.State;

Expand All @@ -29,13 +30,25 @@ ILogManager logManager

public Task Execute(CancellationToken cancellationToken)
{
if (initConfig.ProcessingEnabled)
HealCanonicalChainIfEnabled();
return initConfig.ProcessingEnabled
? RunBlockTreeInitTasks(cancellationToken)
: Task.CompletedTask;
}

private void HealCanonicalChainIfEnabled()
{
if (!initConfig.HealCanonicalChain) return;

Hash256? startHash = blockTree.Head?.Hash;
if (startHash is not null)
{
return RunBlockTreeInitTasks(cancellationToken);
if (_logger.IsInfo) _logger.Info($"Healing canonical chain from head {startHash} (depth {initConfig.HealCanonicalChainDepth})...");
blockTree.HealCanonicalChain(startHash, initConfig.HealCanonicalChainDepth);
}
else
{
return Task.CompletedTask;
if (_logger.IsWarn) _logger.Warn("HealCanonicalChain requested but no head block found — skipping.");
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.PerfTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ public int DeleteChainSlice(in long startNumber, long? endNumber)
{
return _blockTree.DeleteChainSlice(startNumber, endNumber);
}

public void HealCanonicalChain(Hash256 startHash, long maxBlockDepth) { }
}

private const string DbBasePath = @"C:\perf_db";
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.TxPool.Test/TestBlockTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ public void ForkChoiceUpdated(Hash256? finalizedBlockHash, Hash256? safeBlockBlo
public bool IsBetterThanHead(BlockHeader? header) => false;
public void UpdateBeaconMainChain(BlockInfo[]? blockInfos, long clearBeaconMainChainStartPoint) { }
public void RecalculateTreeLevels() { }
public void HealCanonicalChain(Hash256 startHash, long maxBlockDepth) { }
}
Loading