diff --git a/src/Nethermind/Nethermind.Synchronization.Test/ForwardHeaderProviderTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/ForwardHeaderProviderTests.cs index dbda18297e0..1cd7aa616fb 100644 --- a/src/Nethermind/Nethermind.Synchronization.Test/ForwardHeaderProviderTests.cs +++ b/src/Nethermind/Nethermind.Synchronization.Test/ForwardHeaderProviderTests.cs @@ -428,6 +428,48 @@ public async Task Faults_on_get_headers_faulting() await headerTask.Should().ThrowAsync(); } + [Test] + public async Task Reports_weak_peer_on_timeout_cancellation() + { + await using IContainer node = CreateNode(); + Context ctx = node.Resolve(); + + ISyncPeer syncPeer = Substitute.For(); + syncPeer.TotalDifficulty.Returns(UInt256.MaxValue); + syncPeer.HeadNumber.Returns(1000); + syncPeer.GetBlockHeaders(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns?>>(x => throw new OperationCanceledException()); + + PeerInfo peerInfo = new(syncPeer); + ctx.ConfigureBestPeer(peerInfo); + + IForwardHeaderProvider forwardHeader = ctx.ForwardHeaderProvider; + (await forwardHeader.GetBlockHeaders(0, 128, CancellationToken.None)).Should().BeNull(); + ctx.PeerPool.Received().ReportWeakPeer(peerInfo, AllocationContexts.ForwardHeader); + } + + [Test] + public async Task Throws_on_sync_cancellation() + { + await using IContainer node = CreateNode(); + Context ctx = node.Resolve(); + + using CancellationTokenSource cts = new(); + cts.Cancel(); + + ISyncPeer syncPeer = Substitute.For(); + syncPeer.TotalDifficulty.Returns(UInt256.MaxValue); + syncPeer.HeadNumber.Returns(1000); + syncPeer.GetBlockHeaders(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns?>>(x => throw new OperationCanceledException()); + + ctx.ConfigureBestPeer(syncPeer); + + IForwardHeaderProvider forwardHeader = ctx.ForwardHeaderProvider; + Func act = () => forwardHeader.GetBlockHeaders(0, 128, cts.Token); + await act.Should().ThrowAsync(); + } + [Flags] private enum Response { diff --git a/src/Nethermind/Nethermind.Synchronization/Blocks/PowForwardHeaderProvider.cs b/src/Nethermind/Nethermind.Synchronization/Blocks/PowForwardHeaderProvider.cs index 9db02fb7c01..61f63f98d0a 100644 --- a/src/Nethermind/Nethermind.Synchronization/Blocks/PowForwardHeaderProvider.cs +++ b/src/Nethermind/Nethermind.Synchronization/Blocks/PowForwardHeaderProvider.cs @@ -175,6 +175,12 @@ private void OnNewBestPeer(PeerInfo newBestPeer) syncPeerPool.ReportWeakPeer(bestPeer, AllocationContexts.ForwardHeader); return null; } + catch (OperationCanceledException) when (!cancellation.IsCancellationRequested) + { + // Request was cancelled due to timeout in protocol handler, not because the sync was cancelled + syncPeerPool.ReportWeakPeer(bestPeer, AllocationContexts.ForwardHeader); + return null; + } catch (EthSyncException e) { if (_logger.IsDebug) _logger.Debug($"Failed to download forward header from {bestPeer}, {e}"); diff --git a/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs b/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs index a5b93e6758c..84e6865a9e7 100644 --- a/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs +++ b/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs @@ -1606,5 +1606,40 @@ void VerifyAllTrieExceptGenesis() } } } + + [Test] + public void BlockCommitSet_IsSealed_after_Seal_with_null_root() + { + // This test verifies the fix for networks that have an empty genesis state. + // When the state trie is empty, the root is null, but the commit set should still be sealed. + BlockCommitSet commitSet = new(0); + + commitSet.IsSealed.Should().BeFalse(); + + commitSet.Seal(null); + + commitSet.IsSealed.Should().BeTrue(); + commitSet.StateRoot.Should().Be(Keccak.EmptyTreeHash); + } + + [Test] + public void Consecutive_block_commits_work_when_first_has_null_root() + { + // This test simulates a scenario where the genesis block has an empty state (no allocations). + // Block 0 commits with null root, then block 1 should be able to commit without assertion failure. + using TrieStore fullTrieStore = CreateTrieStore(); + + // Block 0: empty state (genesis with no allocations) + using (ICommitter _ = fullTrieStore.BeginStateBlockCommit(0, null)) { } + + // Block 1: should not throw or assert, even though previous block had null root + TrieNode trieNode = new(NodeType.Leaf, Keccak.Zero); + Action commitBlock1 = () => + { + using (ICommitter _ = fullTrieStore.BeginStateBlockCommit(1, trieNode)) { } + }; + + commitBlock1.Should().NotThrow(); + } } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/BlockCommitPackage.cs b/src/Nethermind/Nethermind.Trie/Pruning/BlockCommitPackage.cs index 9092f499ded..1df8cfa11a2 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/BlockCommitPackage.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/BlockCommitPackage.cs @@ -14,11 +14,18 @@ public class BlockCommitSet(long blockNumber) : IComparable public TrieNode? Root { get; private set; } public Hash256 StateRoot => Root?.Keccak ?? Keccak.EmptyTreeHash; - public bool IsSealed => Root is not null; + private bool _isSealed; + + /// + /// A commit set is sealed once has been called, regardless of whether the root is null. + /// A null root is valid for an empty state trie (e.g., genesis blocks with no allocations). + /// + public bool IsSealed => _isSealed; public void Seal(TrieNode? root) { Root = root; + _isSealed = true; } public override string ToString() => $"{BlockNumber}({Root})"; diff --git a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs index 66c57f32656..657f87d1d79 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs @@ -463,7 +463,7 @@ public IBlockCommitter BeginBlockCommit(long blockNumber) private void FinishBlockCommit(BlockCommitSet set, TrieNode? root) { if (_logger.IsTrace) _logger.Trace($"Enqueued blocks {_commitSetQueue.Count}"); - // Note: root is null when the state trie is empty. It will therefore make the block commit set not sealed. + // Note: root can be null when the state trie is empty (e.g., genesis with no allocations). set.Seal(root); set.Prune();