Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,48 @@ public async Task Faults_on_get_headers_faulting()
await headerTask.Should().ThrowAsync<InvalidOperationException>();
}

[Test]
public async Task Reports_weak_peer_on_timeout_cancellation()
{
await using IContainer node = CreateNode();
Context ctx = node.Resolve<Context>();

ISyncPeer syncPeer = Substitute.For<ISyncPeer>();
syncPeer.TotalDifficulty.Returns(UInt256.MaxValue);
syncPeer.HeadNumber.Returns(1000);
syncPeer.GetBlockHeaders(Arg.Any<long>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns<Task<IOwnedReadOnlyList<BlockHeader>?>>(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<Context>();

using CancellationTokenSource cts = new();
cts.Cancel();

ISyncPeer syncPeer = Substitute.For<ISyncPeer>();
syncPeer.TotalDifficulty.Returns(UInt256.MaxValue);
syncPeer.HeadNumber.Returns(1000);
syncPeer.GetBlockHeaders(Arg.Any<long>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns<Task<IOwnedReadOnlyList<BlockHeader>?>>(x => throw new OperationCanceledException());

ctx.ConfigureBestPeer(syncPeer);

IForwardHeaderProvider forwardHeader = ctx.ForwardHeaderProvider;
Func<Task> act = () => forwardHeader.GetBlockHeaders(0, 128, cts.Token);
await act.Should().ThrowAsync<OperationCanceledException>();
}

[Flags]
private enum Response
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
35 changes: 35 additions & 0 deletions src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ public class BlockCommitSet(long blockNumber) : IComparable<BlockCommitSet>
public TrieNode? Root { get; private set; }
public Hash256 StateRoot => Root?.Keccak ?? Keccak.EmptyTreeHash;

public bool IsSealed => Root is not null;
private bool _isSealed;

/// <summary>
/// A commit set is sealed once <see cref="Seal"/> 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).
/// </summary>
public bool IsSealed => _isSealed;

public void Seal(TrieNode? root)
{
Root = root;
_isSealed = true;
}

public override string ToString() => $"{BlockNumber}({Root})";
Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down